Repository navigation
feat(queue): Deliver local dev work through BullMQ on Redis - #2361
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Sentry Evalsjunior-behavioral
Base commit junior-integration
Base commit |
|
I tested the current head locally. The BullMQ delivery path works: the Redis integration tests pass, I do not think the PR should proceed with the current worker lifecycle. The development consumers start inside This creates a concrete Nitro HMR problem. An in-process reload can evaluate the app again and register another set of workers while the old workers remain active. Old code can then receive new jobs. A full child-process restart cleans up through process exit, but that should not be the normal resource lifecycle. I suggest that local BullMQ consumers run in one sibling worker process managed by
The PR also sets concurrency to 20 for every BullMQ worker. There are three queue topics, so one process can admit up to 60 active jobs. For development, I suggest one worker process and a default BullMQ concurrency of 4 per queue. Please allow a positive integer override through an environment variable such as This can remain a development-only change. The shared queue contract and production Vercel Queue behavior do not need to change. I would keep concurrent-worktree queue isolation outside this PR. The blocking issue here is clear ownership and cleanup of the local workers, plus a safe development concurrency default. |
e0289fb to
920d5c4
Compare
920d5c4 to
2866f66
Compare
|
Thanks for the update. I checked the new head locally. It addresses the main points from the first review:
I still think this needs changes before it can proceed.
The PR also currently conflicts with |
2866f66 to
f6423a0
Compare
Add a BullMQ queue provider behind the QueueProvider contract from #2338. JUNIOR_QUEUE_PROVIDER selects it, and the default stays vercel. pnpm dev selects bullmq with the compose.yaml Redis, so local work no longer needs Vercel Queues access or an OIDC token. bullmq and redis are optional peer dependencies. Junior imports them by a name in a variable so bundlers leave them out of the production server, and check-vercel-output.mjs fails if they reach a deployed function. Co-Authored-By: Thomas Jiang <20501235+thomasjiangcy@users.noreply.github.com> Co-Authored-By: Thomas Jiang <thomas.jiang@sentry.io> Co-Authored-By: Thomas Jiang <20501235+thomasjiangcy@users.noreply.github.com>
Nitro now only sends BullMQ jobs. scripts/dev-server.mjs starts one sibling worker process (scripts/dev-queue-worker.mjs) that creates the example app and calls the new startQueueWorker(app). The worker owns its BullMQ workers, queues, Redis client, state adapter and database. On SIGINT or SIGTERM it waits up to 10 seconds for running jobs, then closes all of them. A new Junior build restarts Nitro and the worker. A change to the example app's server, plugins, dashboard or app files restarts only the worker. Nitro HMR no longer leaves old workers taking jobs. The worker is a separate process, so pnpm dev keeps state in the local queue Redis unless JUNIOR_STATE_ADAPTER is set, and refuses memory state with BullMQ. BullMQ concurrency defaults to 4 per queue topic. JUNIOR_QUEUE_WORKER_CONCURRENCY overrides it, and the worker logs the value at startup. The queue provider contract gains an optional startWorker(), and registerDevConsumer becomes optional. The Vercel provider and production behavior do not change. Co-Authored-By: Thomas Jiang <20501235+thomasjiangcy@users.noreply.github.com>
The dev server now kills the queue worker process if it has not exited 10 seconds after SIGTERM. BullMQ reuses its first close promise, so the in-process forced close could not bound shutdown. The BullMQ provider keeps no module state. Each send opens and closes its own Redis client, and each worker owns its client and BullMQ workers. createApp() returns a JuniorApp with startQueueWorker(), which replaces the app-to-consumers WeakMap. Worker cleanup now tries every step even when an earlier one fails. Co-Authored-By: Thomas Jiang <20501235+thomasjiangcy@users.noreply.github.com>
f6423a0 to
d57ecec
Compare
node-redis leaves a client open and reconnecting after a failed connect(). Destroy it before the error propagates, so a send or worker start against a Redis that is down does not leak a client.
|
106 screenshot changes — 0 changed · 106 added · 0 removed |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit f5a7b3f. Configure here.
…und work A worker or Nitro restart that waited for the old process could start a new process after shutdown began. Shutdown now blocks new starts. The queue worker now waits for waitUntil work, such as title work, before it closes state and the database.

Follow-up to #2338.
pnpm devno longer needs Vercel Queues access or a Vercel OIDC token. It delivers conversation work, plugin tasks and snapshot jobs through BullMQ on the Redis incompose.yaml. Production and tests still use Vercel Queues.What changes
chat/queue/bullmq.ts: aQueueProviderbacked by BullMQ. The Nitro process only sends jobs. BullMQ workers run in one separate worker process (see below).scripts/dev-server.mjsstartsscripts/dev-queue-worker.mjsnext to Nitro. It importsapps/example/server.ts(the samecreateApp()options as Nitro) and calls the newstartQueueWorker(app)from@sentry/junior. The worker owns its BullMQ workers, queues, Redis client, state adapter and database. OnSIGINT/SIGTERMit stops taking jobs, waits up to 10 seconds for running ones, then closes each worker and queue, the Redis client, state and the database. A second signal exits at once.packages/junior/dist, dashboarddist) restarts Nitro and the worker. A change toapps/example/server.ts,plugins.ts,dashboard.tsorapps/example/app/restarts only the worker. Nitro-only HMR does not touch it. The supervisor starts the new worker only after the old one has exited. If the worker crashes,pnpm devstays up and starts it again on the next watched change.QueueProvidergains an optionalstartWorker(consumers)that returns aQueueWorkerwithclose().registerDevConsumerbecomes optional; only the Vercel provider implements it. The Vercel provider and production behavior do not change.pnpm devpicks BullMQ andJUNIOR_STATE_ADAPTERis unset, it uses Redis state on the queue Redis (and pointsREDIS_URLthere, so a shared previewREDIS_URLfromdev:envis never used by default).JUNIOR_STATE_ADAPTER=memorywith BullMQ stopspnpm devwith an error.chat/queue/config.ts:JUNIOR_QUEUE_PROVIDER(verceldefault, orbullmq),JUNIOR_QUEUE_REDIS_URLandJUNIOR_QUEUE_WORKER_CONCURRENCY.getQueueProvider()reads it.scripts/dev-server.mjs: defaultspnpm devtobullmqonredis://localhost:6382. Explicit env wins. Other local commands (pnpm cli, ACP) and test/eval config keep Vercel, because only the dev server starts workers.apps/example/.env.exampleis unchanged, since tests and evals load it.bullmqandredisare optional peer dependencies of@sentry/junior(dev deps ofpackages/junioronly; the example app does not list them, so thejunior initscaffold stays the same). Pinned to versions older thanminimumReleaseAge.msgpackr-extract: falseinallowBuilds.Mapping to the contract
idempotencyKey→ hashedjobId(BullMQ forbids:in ids, and Junior keys contain it).delaySeconds→delay.retentionSeconds→ the worker drops jobs older than it.retry()returning{ acknowledge: true }ends the job. Otherwise BullMQ retries with backoff (cap 50 attempts, since conversation work usesmaxDeliveries: null).JUNIOR_QUEUE_WORKER_CONCURRENCYoverrides it (positive integer). The worker logs the value at startup inqueue.bullmq.worker.started.createCallbackreturns 404: nothing pushes to those routes with BullMQ.Known differences from Vercel Queues (dev only, documented in
chat/queue/README.md)visibilityTimeoutSecondsruns out.Production bundle
A literal
import("bullmq")made Nitro bundle a ~1.7 MB BullMQ chunk plus native msgpackr files into the production function.bullmq.tsnow imports by a variable name.check-vercel-output.mjsfails ifbullmqormsgpackrreach a deployed function.Verification (head
920d5c483, rebased onmainat7b5558468)pnpm devwith no.env.local, noVERCEL_OIDC_TOKEN, and no state setting: onedev-queue-worker.mjsprocess starts and logsapp.queue.worker_concurrency=4.POST /api/conversations→ 200accepted→conversation.work.lease.acquiredin the worker → the turn runs until the model call fails on auth (no gateway key in the sandbox) →conversation.work.completed.touch apps/example/plugins.ts: the worker restarted with a new pid, the Nitro pid stayed the same, and only one worker process was running afterwards.SIGTERMto the worker: exit code 0 in about 0.5s. Withprocess.exitremoved, the process still exited by itself, so no Redis, database or timer handles stay open.SIGINTto the dev server group stopped Nitro, the worker and the watchers.tests/integration/queue-bullmq.test.tsagainst real Redis: dedupe with:keys, retries then acknowledge, concurrency, and a new case thatclose()waits for a running job.@sentry/juniorunit + component suites (312 files, 2339 tests),chat-configtests (concurrency default, override and invalid value),load-env-files.test.mjs,init-cli.test.ts, typecheck,@sentry/juniorlint,ast-grep:lint,package:lint, Prettier,file-length:check.nitro build(Vercel preset) of the example app, thencheck-vercel-output.mjspasses. The fullapps/examplepnpm buildneeds Vercel credentials forjunior snapshot create, so I rannitro builddirectly.Not in this PR
createCallbackoptional.sentry local runstarts for Nitro. Its logs print in thepnpm devterminal.Open questions
bullmq.tsstill caches the Redis client and queues in module state, but the worker'sclose()now releases and resets them.app.tskeeps aWeakMapfrom each app to its queue consumers sostartQueueWorker(app)can find them without changingserver.ts(which thejunior initscaffold test pins). Is that acceptable, or would you rathercreateApp()return the consumers some other way?@sentry/junior, so it isn't duplicated. I didn't add a Nitro close hook for it.pnpm devdefault to BullMQ (as in this PR), or should it stay opt-in until more people have tried it?--
View Junior Session [Sentry]