Skip to content

Use a single database for primary, cache, queue, cable#24

Merged
raghubetina merged 1 commit into
mainfrom
chore/single-database
May 18, 2026
Merged

Use a single database for primary, cache, queue, cable#24
raghubetina merged 1 commit into
mainfrom
chore/single-database

Conversation

@raghubetina
Copy link
Copy Markdown
Contributor

Summary

Rails 8 ships config/database.yml with four separately-configured roles in production (primary, cache, queue, cable) so Solid Cache, Solid Queue, and Solid Cable can each live in their own database. The default already points all four roles at the same DATABASE_URL with separate migration paths — sharing one Render Postgres saves money but keeps the conceptual overhead of four config blocks, three empty stub schema files, and three nonexistent migration directories.

The migrations actually live in db/migrate/ and db/schema.rb already contains all the SC/SQ/SCable tables, so the multi-role config was purely cosmetic. This PR collapses it to a single role.

Changes:

  • config/database.yml: replace the four-role production block with a single <<: *default + url: pair.
  • config/cache.yml: drop database: cache so SolidCache::Record inherits ApplicationRecord's connection.
  • config/cable.yml: drop the connects_to block so Solid Cable inherits the primary connection.
  • Delete db/cable_schema.rb, db/cache_schema.rb, db/queue_schema.rb — empty (version: 0) stubs only meaningful when each role has its own database.

The same single-DB pattern is already in production on aplace.app and has been stable since the async-mode rollout.

Test plan

  • RAILS_ENV=production bin/rails runner confirms ApplicationRecord, SolidQueue::Record, SolidCache::Record, and SolidCable::Record all report connection_db_config.name == "primary".
  • Cross-checked the Rails multi-DB guide for cleanup items: no connects_to, database_selector, database_resolver, shard_selector, or per-DB schema cache files anywhere in config/ or app/.
  • No remaining references to cache_migrate, queue_migrate, or cable_migrate directories (which never existed on disk).
  • On first deploy after merge, confirm a fresh Render Postgres has all SC/SQ/SCable tables created in the primary database from db/schema.rb.

Rails 8 ships database.yml with four separately-configured roles in
production (primary, cache, queue, cable) so Solid Cache, Solid Queue,
and Solid Cable can each live in their own database. The default
already points all four roles at the same DATABASE_URL with separate
migration paths — sharing one Render Postgres saves money but keeps
the conceptual overhead of four config blocks, three empty stub
schema files, and three nonexistent migration directories
(db/cache_migrate, db/queue_migrate, db/cable_migrate). The
migrations actually live in db/migrate/ and db/schema.rb already
contains all the SC/SQ/SCable tables, so the multi-role config was
purely cosmetic.

Collapse to one role for student projects on Render's free tier:

- config/database.yml: replace the four-role production block with
  a single `<<: *default` + `url:` pair.
- config/cache.yml: drop `database: cache` so SolidCache::Record
  inherits ApplicationRecord's connection.
- config/cable.yml: drop the `connects_to` block so SolidCable
  inherits the primary connection.
- Delete db/cable_schema.rb, db/cache_schema.rb, db/queue_schema.rb;
  they were empty (`version: 0` stubs) and only matter when each role
  has its own database.

Verified with `RAILS_ENV=production bin/rails runner` that
ApplicationRecord, SolidQueue::Record, SolidCache::Record, and
SolidCable::Record all bind to the `primary` config. The same
single-DB pattern is already in production on aplace.app and has
been stable since the async-mode rollout.
raghubetina added a commit that referenced this pull request May 18, 2026
This branch's puma.rb adds `solid_queue_mode :async if ENV["SOLID_QUEUE_IN_PUMA"]`,
but the lockfile resolved to solid_queue 1.1.4, which has no such DSL method.
Booting in production crashes immediately with:

  config/puma.rb:47:in 'Puma::DSL#_load_from': undefined method
  'solid_queue_mode' for an instance of Puma::DSL (NoMethodError)

Async supervisor mode was re-introduced in solid_queue 1.3.0
(rails/solid_queue#644); the earlier 0.4 implementation was
removed in 0.7. Pinning to ~> 1.3 documents the minimum that
matches this branch's puma.rb and updates the lockfile to 1.4.0.

Caught by deploying a marketplace smoke-test app built from this
branch + #22 + #24 to Render free tier.
raghubetina added a commit that referenced this pull request May 18, 2026
* Run Solid Queue in async mode on Render free tier

Companion to #22 (WEB_CONCURRENCY=0). Free tier (512MB) can't fit
SQ's default fork mode, which spawns supervisor + worker +
dispatcher + scheduler subprocesses for ~460MB of RSS overhead on
top of Puma. The container OOM-cycles every ~7-15 min — the deploy
still looks "live" but HTTP returns 502s during each restart
window.

`solid_queue_mode :async` (documented switch in
lib/puma/plugin/solid_queue.rb of the gem) tells the plugin to
run worker/dispatcher/scheduler as threads inside the Puma master
process. The 4 SQ subprocesses collapse into ~50MB of thread
overhead in Puma. Container RSS drops from ~530MB (cycling into
OOM) to a flat ~300MB.

Trade-off per SQ's own docs ("Only use async if you know what
you're doing and have strong reasons to"):
- Less isolation between SQ and Puma. A leaky/hung SQ thread
  affects request serving. For low-traffic student projects this
  is the right call; revisit if upgrading to a paid plan.
- No inter-process parallelism for jobs. Free tier is already
  0.1 vCPU, so this isn't real lost throughput.
- Recurring tasks and concurrency controls still work unchanged.

The DB pool bump (5 → 8) is non-optional: previously the pool was
sized for Puma's 3 request threads only; now the same process
also runs 3 SQ worker threads + dispatcher + scheduler. Under any
load the old pool=5 would throw ConnectionTimeoutError. The 5 ->
8 default still respects DB_POOL env override for projects that
need finer control.

Diagnosed and validated on raghubetina/aplace:
raghubetina/aplace#58 — RSS metric showed
the predicted plateau at ~300MB with zero growth after deploy.

* Pin solid_queue ~> 1.3 for solid_queue_mode :async support

This branch's puma.rb adds `solid_queue_mode :async if ENV["SOLID_QUEUE_IN_PUMA"]`,
but the lockfile resolved to solid_queue 1.1.4, which has no such DSL method.
Booting in production crashes immediately with:

  config/puma.rb:47:in 'Puma::DSL#_load_from': undefined method
  'solid_queue_mode' for an instance of Puma::DSL (NoMethodError)

Async supervisor mode was re-introduced in solid_queue 1.3.0
(rails/solid_queue#644); the earlier 0.4 implementation was
removed in 0.7. Pinning to ~> 1.3 documents the minimum that
matches this branch's puma.rb and updates the lockfile to 1.4.0.

Caught by deploying a marketplace smoke-test app built from this
branch + #22 + #24 to Render free tier.
@raghubetina raghubetina merged commit 0fa1dc5 into main May 18, 2026
1 check passed
@raghubetina raghubetina deleted the chore/single-database branch May 18, 2026 16:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant