Repository navigation
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved defects bypass keyed-job deduplication during transactions and redrives, leave handlers blocking the queue beyond their deadlines, and permit expiry values above the 24-hour limit.
4 open issues
What changed in this PR
This PR adds durable-job infrastructure behind a JobQueue port, preparing the worker for future knowledge-processing queues. It introduces no production queues yet; API sending and the operator console remain deferred.
Changes:
- Adds queue contracts, an in-memory implementation and shared contract tests.
- Introduces a pg-boss provider with owner-role installation, runtime operation and guarded job-state reads.
- Integrates queue startup and shutdown into the worker, with dependency boundaries and documentation.
| File | Description |
|---|---|
tsconfig.json |
Registers the pg-boss provider project. |
packages/providers-pgboss/tsconfig.json |
Configures provider compilation. |
packages/providers-pgboss/src/pgboss.test.ts |
Tests contracts, permissions and durable lifecycle behavior. |
packages/providers-pgboss/src/index.ts |
Implements pg-boss installation and the queue adapter. |
packages/providers-pgboss/package.json |
Declares provider exports and dependencies. |
packages/jobs/src/testing/job-queue-contract.ts |
Adds shared queue contract tests. |
packages/jobs/src/testing/index.ts |
Exports testing helpers. |
packages/jobs/src/platform-queues.ts |
Introduces the empty platform queue registry. |
packages/jobs/src/job-queue.ts |
Defines the port, validation and in-memory implementation. |
packages/jobs/src/job-queue.test.ts |
Tests in-memory behavior and queue definitions. |
packages/jobs/src/index.ts |
Exports queue APIs and the registry. |
packages/jobs/package.json |
Exposes the testing entry point. |
package-lock.json |
Locks provider and transitive dependencies. |
docs/development.md |
Documents durable jobs and local worker startup. |
docs/architecture/system-atlas.html |
Adds the provider and worker connection. |
docs/architecture/modules/worker.yaml |
Records the worker’s queue-provider dependency. |
docs/architecture/modules/providers-pgboss.yaml |
Describes the provider’s responsibilities and connections. |
docs/architecture/modules/order.yaml |
Places the provider in the architecture listing. |
apps/worker/tsconfig.json |
References the provider project. |
apps/worker/src/main.ts |
Opens the queue and stops it before database shutdown. |
apps/worker/package.json |
Adds the provider dependency and development command. |
.dependency-cruiser.cjs |
Enforces pg-boss vendor boundaries. |
Review effort: Balanced
9 tasks
The review of #179 found that genai_runtime got DML on the pgboss tables, so the API could read, change or delete every organisation's jobs without platform access. - The worker runs as genai_worker, a member of genai_runtime with DML on pgboss. genai_runtime gets USAGE and EXECUTE on the read functions only, and a queue the API sends to grants only what sending needs. - Sending dead letters back goes through the worker. - A handler takes the organisation from its record, never the job. - Work longer than 24 hours is split into jobs, with progress in its record. - ADR 0022's line says the grants enforce it.
- The API reads job state through a JobReads port that does not start pg-boss, since genai_runtime cannot read pgboss.version. - The test list says which tests come with the first real queue (issue 135) and the console screen (issue 181). - ADR 0022 said production connects with DATABASE_RUNTIME_URL; no environment sets it, and deployed apps switch role per connection.
kentare
force-pushed
the
docs/durable-jobs-adr
branch
from
October 29, 2026 12:52
36c6b6e to
c5c0fc4
Compare
- A JobQueue port in @genai/jobs, with an in-memory queue and a contract suite in @genai/jobs/testing that both queues pass. - providers-pgboss: the owner role installs schema pgboss, creates each queue with its <queue>.dead dead-letter queue and deletes jobs 7 days after they end and 14 days when nobody takes them, and grants genai_runtime what sending and working need. Counts and dead letters are read only with platform access. - The worker opens the queue and works the platform queues, of which there are none yet; npm run dev -w apps/worker runs it locally. Part of #155.
…expiry at 24 hours - defineQueue refuses an expiry above 24 hours, as ADR 0041 and pg-boss do, instead of failing when the worker creates the queue. - A key is held by jobs sent earlier in the same transaction, and short and stately queues hold one created job per key, a job without a key under the empty key, as pg-boss's indexes do. - Dead letters keep their key and priority, and sending one back leaves it failed in place while another job holds its key. - An attempt that outlives its expiry is failed and its signal aborted, and the worker takes nothing more until the handler returns. - Contract tests for each, run on both queues. Part of #155.
The review of ADR 0041 found that genai_runtime had DML on the pgboss tables, so one query in the API could read, change or delete every organisation's jobs without platform access. Grants now enforce it: - The migration creates genai_worker: no login, not a superuser, without BYPASSRLS, a member of genai_runtime, and refuses to run if it is a superuser or bypasses row-level security, as for genai_runtime. The owner may switch to it. - The worker's store pool switches to genai_worker (a new role option on createPostgresStores); the API keeps genai_runtime. - installJobQueue grants DML on the pgboss tables, sequences and functions to the worker's role only, and gives readers (genai_runtime) USAGE on the schema and EXECUTE on the two read functions, revoking anything else they had and PUBLIC's default EXECUTE on pg-boss's functions. - The console's reads move to PgBossJobReads, behind a JobReads port, since a role without table rights cannot start pg-boss. - infra/README.md, docs/development.md, .env.example and the atlas say which role does what. Part of #155.
kentare
force-pushed
the
feat/durable-jobs
branch
from
October 29, 2026 13:01
81028f3 to
ab23618
Compare
6 tasks done
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

What and why
Work that must outlive a process needs a durable queue, and genki has none. ADR 0041 (#179) chooses pg-boss. This PR builds the queue itself, with no real queue on it yet: the knowledge queues come with #135.
JobQueueport in@genai/jobs(packages/jobs/src/job-queue.ts): send with a key, priority, start time and an optional transaction; work with heartbeats; schedule with a time zone; cancel; dead letters, send-back and counts.defineQueuestates each queue's policy, expiry (at most 24 hours), heartbeat (at least 10 s), retries, dead-letter queue and payload schema, and a payload value with whitespace or longer than 128 characters is refused, so job data holds keys only. The console's reads are a port of their own,JobReads. An in-memory queue for tests and dev mode.MessageQueuestays.@genai/jobs/testingthat the in-memory queue and pg-boss both run, so the in-memory queue does what pg-boss does with keys, dead letters and expiry.packages/providers-pgbosson pg-boss 12.38.0. The owner role installs and migrates schemapgboss, creates each queue with its<queue>.deaddead-letter queue and sets deletion on every queue (7 days after a job ends, 14 days for one nobody takes). Every other instance runs withmigrateandcreateSchemaoff. A queue cannot change its policy, dead-letter queue or heartbeat in place.packages/providers-postgres/src/schema.ts) createsgenai_worker: no login, not a superuser, withoutBYPASSRLS, a member ofgenai_runtime, and it refuses to run if the role is a superuser or bypasses row-level security, as forgenai_runtime.installJobQueuegrants DML on thepgbosstables, sequences and functions togenai_workeronly.genai_runtime, which the API uses, getsUSAGEon the schema andEXECUTEon the twoSECURITY DEFINERread functions, which refuse withoutapp.platform_accessand inside a tenant transaction, and nothing else; the install revokes any other right it had, andPUBLIC's defaultEXECUTEon pg-boss's functions. Since a role without table rights cannot start pg-boss, the console reads throughPgBossJobReads.apps/worker/src/main.ts) switches its store pool togenai_worker(a newroleoption oncreatePostgresStores), installs and worksplatformQueues(packages/jobs/src/platform-queues.ts, empty for now) and stops the queue first on shutdown. The API keepsgenai_runtime.npm run dev -w apps/workerruns the worker locally;dev:alldoes not start it.pg-bossandpgbossin the vendor rules of.dependency-cruiser.cjs,docs/architecture/modules/providers-pgboss.yaml(new),worker.yaml,order.yamland the System Atlas page, a "Durable jobs" paragraph indocs/development.md, the worker's role ininfra/README.md, andDATABASE_RUNTIME_URLper app in.env.example.Part of #155. Stacked on #179: the base is
docs/durable-jobs-adruntil it merges.Deploy: no infra change. Both apps log in as the app identity and switch role per connection, and the migration creates
genai_workerand lets the app identity switch to it. It runsgrant genai_runtime to genai_workeras the app identity, which needs admin option ongenai_runtime; the identity has it if its own migration createdgenai_runtime, which is not checked in test or production yet. After the first deploy to test,select pg_has_role('genai_worker', 'genai_runtime', 'MEMBER')is true andselect has_table_privilege('genai_runtime', 'pgboss.job', 'select')is false.Not in this PR: the console screen for queue counts and dead letters, its routes and
platform.jobs.redrivewith a reason and audit (#181); the API sending jobs, since no queue it sends to exists yet; the tests "a handler run twice leaves the same result", "a client of an outside service makes one request per attempt" and "a handler acts for the organisation of the record it loads when the job's data names another", which need a real queue, handler and record.How it was tested
npm run check(461 test files, 4945 tests pass).npx vitest run packages/jobs packages/providers-pgboss: the contract suite runs against the in-memory queue and against pg-boss onpostgres:16-alpineasgenai_worker: "a queue refuses a payload its schema does not admit", "job payloads hold no content", "works a sent job once with its data", "a job sent in a rolled-back transaction does not exist", "a job that keeps failing ends in the dead-letter queue", "sending a dead letter back works the job again", "lists dead letters per organisation", "never works a cancelled job", "holds one queued job per key on a short queue", "holds one queued job per key inside a transaction", "sending a dead letter back keeps its key, and leaves it while another job holds the key", "takes jobs in priority order", "an attempt that runs past its expiry is aborted and retried". The transaction, send-back and expiry tests fail on the in-memory queue as first pushed (819b852) and pass on pg-boss.pg-boss on the platform server's roles: "only the owner installs pgboss, and the API and the worker run with migrate and createSchema off", "genai_runtime cannot read, change or delete a job" (select, update, delete and insert on
pgboss.joband select onpgboss.queuegive permission denied, starting pg-boss fails, and the onlypgbossfunctions it may execute are the two read functions; it first grantsgenai_runtimeDML by hand, so it also shows the install revokes it), "the worker sends and works jobs as genai_worker", "the console reads jobs as genai_runtime, only with platform access", "an ended job is deleted after 7 days" (readsdeletion_secondsandretention_secondsfor the queue and its dead-letter queue), "refuses to change a queue policy in place", "a killed worker's job is retried after its heartbeat" (ends the worker's pool mid-job), "a nightly schedule sends one job per occurrence with two instances".npx vitest run packages/providers-postgres/src/isolation.test.ts(owner withCREATEROLE, not a superuser): "runs the worker as genai_worker, a member of genai_runtime under the same row-level security" (on every tenant table the worker sees whatgenai_runtimesees, with and without a tenant), "the migration refuses a genai_worker that is a superuser or bypasses row-level security".Each grant and role rule switched off fails its test: the worker without DML (4 tests and the contract setup), readers keeping table rights (1), readers without
EXECUTE(1),PUBLICkeepingEXECUTE(1), the role check (2),genai_workeroutsidegenai_runtime(1), the store pool ignoring the role (1).In memory: "refuses a heartbeat shorter than pg-boss allows", "refuses a queue that expires before its heartbeat", "refuses an expiry longer than 24 hours", "refuses a queue name that reads as a dead-letter queue", "sends one job per occurrence in the time zone".
Checked by hand at 1562837, before
genai_worker, on a fresh local database fromdocker compose(pgvector pg17):npm run dev -w apps/workerstarted with pg-boss and 0 queues and created schemapgboss(version 46). A throwaway script, not in this PR, then installed four demo queues throughinstallJobQueueand sent and worked jobs throughPgBossJobQueue. Jobs for two organisations were worked, a job sent in a rolled-back transaction never appeared, a job delayed by 10 minutes waited, and a schedule sent one job per minute. A job that failed 3 times went to its.deadqueue.countsanddeadLettersunder platform access listed it with its organisation and error, and after the failure was fixed,redrivesent it back and it completed on the first attempt. pg-boss's own dashboard (@pg-boss/dashboard1.12.1) showed the same queues and jobs.npm run checkpassesChecklist
packages/providers-*docs/architecture/modulesare updated if a contract, boundary or module changed