Wheels 4.0 — background jobs without Redis #2532
bpamiri
announced in
Announcements
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
-
Wheels 4.0 ships a first-class job queue that lives in your existing database. This post is a guided tour: the Job CFC surface, the CLI daemon, what makes the implementation correct, the multi-tenancy adjacency, and where Redis still wins.
The Redis tax on small-to-medium apps
The pattern is well-trodden —
delayed_jobin Rails, Laravel'sdatabasequeue driver. Awheels_jobstable, columns for payload / run-at / attempts / lock; workers poll, claim, execute. The trade is throughput for one less moving part: no separate process, no separate failure mode, no separate runbook. For the median Wheels app, the DB-queue ceiling is well above their floor.Job CFC surface
// app/jobs/SendWelcomeEmailJob.cfc component extends="wheels.Job" { function config() { super.config(); this.queue = "mailers"; this.maxRetries = 5; this.baseDelay = 2; this.maxDelay = 3600; } public void function perform(struct data = {}) { sendEmail(to=data.email, subject="Welcome!"); } }Backoff:
Min(baseDelay * 2^attempt, maxDelay). With the defaults above: 2, 4, 8, 16, 32 seconds, ceilings at 1 hour. Override$calculateBackoff()for linear / constant / custom.Three enqueue methods on the instance:
job.enqueue(data={email: user.email}); job.enqueueIn(seconds=300, data={email: user.email}); job.enqueueAt(runAt=reminderDate, data={email: user.email});CLI daemon
Exits cleanly on SIGTERM, so systemd / supervisord / Compose
restart: unless-stoppedconfigs are trivial. Stdout logs to your log stack — journald / Loki / Elasticsearch.What makes the implementation correct
Claim is optimistic. Worker reads the next ready row, updates state to
processingwith aWHERE state = 'pending'guard, checks the affected-row count. If another worker grabbed it first, the count is zero and the loser moves on. No advisory locks, no external coordinator.Timeout recovery handles crashes. Each claimed job records a worker heartbeat; if a job has been
processinglonger than its configured timeout without a heartbeat update, a sweeper requeues it. A killed-mid-job worker does not orphan its work.Retries are exponential by default, per job class. When the budget is exhausted, the job is marked
failedand sits there until you decide — retry manually, dig into the payload, or purge.Auto-created
wheels_jobstable. First enqueue or first worker run callsJob.cfc::$ensureJobTable(). No migration to ship.Multi-tenancy without the payload ceremony
This is where it pays off. #1951 resolves the active tenant at the request layer. A request for tenant A enqueues a job → the row lands in tenant A's database (or schema). The worker, pointed at that datasource, resolves the tenant context the same way the request did.
You do not stuff tenant IDs into the payload. You do not write a tenant-aware job base class. The "did I remember to set
tenant_id?" bug class disappears.For pool-per-tenant deployments — one worker per tenant database — jobs route themselves naturally. For shared-pool with tenant-resolved datasources, enqueue and dequeue use the same resolution logic, so it composes by construction.
When to pick Redis anyway
Most SaaS apps are nowhere near these limits. If your current Redis instance serves the queue and nothing else, and queue depth rarely exceeds a few thousand, you are paying Redis taxes for capacity you will not use this decade.
SSE adjacency
#1940 — pub/sub SSE channels. A completed job can publish; every connected browser gets the update. No websocket server, no Pusher account, no third-party dependency. DB-backed queue + SSE pub/sub covers a large slice of what you would previously have needed Redis + a websocket gateway for.
Production operations
wheels jobs workunder systemd / supervisord / Composerestart: unless-stopped.wheels jobs status --format=jsonfrom cron / Prometheus textfile, alert onfailed > 0orpending > N.Links
Question for the thread
If Redis is in your stack only for the job queue, what would it take to remove it? Specifically — what's the throughput peak, pub/sub need, or latency requirement keeping it pinned in your runbook? Useful input for 4.0.x tuning priorities.
Read the full post: https://blog.wheels.dev/posts/background-jobs-without-redis/
Beta Was this translation helpful? Give feedback.
All reactions