Skip to content

fix: harden durable job submission - #20

Open
EstandarMustaq wants to merge 4 commits into
mainfrom
agent/rfc2-review-hardening
Open

fix: harden durable job submission#20
EstandarMustaq wants to merge 4 commits into
mainfrom
agent/rfc2-review-hardening

Conversation

@EstandarMustaq

Copy link
Copy Markdown
Member

Summary

  • Persist tenant-scoped job submission receipts and derive public queue/type/retry fields internally.
  • Split Workbench, Settlements, and Legacy database boundaries and make legacy dispatch replay-safe.
  • Protect metrics, expand dependency status, and validate all OpenAPI schema references.

Validation

  • pnpm guardian:check
  • pnpm openapi:check
  • pnpm build
  • pnpm test (27 tests)

Notes

Queue execution, retries, schedules, status, and metrics remain covered by the existing worker contract.

@EstandarMustaq

Copy link
Copy Markdown
Member Author

A remediação foi implementada e validada localmente e no CI disponível. Solicito nova revisão sobre segurança, isolamento de tenant, idempotência, contratos, migrações e interoperabilidade entre módulos.\n\n@codex review

@EstandarMustaq
EstandarMustaq requested review from a team and itsgaspr and removed request for a team July 18, 2026 12:08

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6772d4bb79

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread prisma/schema.prisma
@@ -0,0 +1,28 @@
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Use the documented migration URL for Prisma

With the new environment split, .env.example no longer defines DATABASE_URL and instead documents WORKBENCH_MIGRATION_DATABASE_URL for privileged migrations, but this datasource still reads only DATABASE_URL. In deployments following the new contract, pnpm prisma:migrate will fail before creating the workbench schema/roles, or it will accidentally run with a runtime URL if an old DATABASE_URL remains. Point the Prisma datasource/script at WORKBENCH_MIGRATION_DATABASE_URL or explicitly bridge it in the migrate command.

Useful? React with 👍 / 👎.

correlation_id: correlationId,
request: input.request,
}));
const jobId = `job_${digest(`${input.tenantId}:create-job:${keyDigest}`).slice(0, 48)}`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep job IDs fresh after receipt expiry

Because the queue job ID is derived only from tenant and idempotency key, a key reused after its receipt expires gets the same BullMQ jobId. The cleanup path deletes only the Postgres receipt, while JobStoreService.enqueue keeps completed and failed BullMQ jobs (removeOnComplete/removeOnFail are false), so BullMQ will treat the new submission as a duplicate and return the old job instead of enqueueing fresh work. Include a per-receipt component in the job ID or expire/remove the queue job on the same retention schedule.

Useful? React with 👍 / 👎.

}

async submit(input: SubmissionInput, enqueue: (jobId: string) => Promise<WorkerJob>): Promise<SubmissionResult> {
const idempotencyKey = this.requiredHeader(input.idempotencyKey, 'Idempotency-Key', 16, 128);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Match the idempotency-key limits in the public contract

The public OpenAPI parameter now used by /api/jobs still allows Idempotency-Key values with length 1–255 and no character pattern, but this code rejects anything shorter than 16, longer than 128, or outside [A-Za-z0-9._:-]. Clients generated from the published contract, and the legacy endpoints that share that parameter, can therefore send documented keys that job submission rejects with 400. Either tighten the OpenAPI parameter for this endpoint or accept the documented range here.

Useful? React with 👍 / 👎.

Comment on lines +74 to +76
const receipt = this.pool
? await this.reservePostgres(input.tenantId, keyDigest, requestHash, jobId, correlationId, input.actorId)
: this.reserveMemory(input.tenantId, keyDigest, requestHash, jobId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Return 503 when receipt storage is unavailable

The new public contract advertises 503 for durable receipt storage failures, but only the queue enqueue is translated to ServiceUnavailableException; Postgres errors from reserving or completing the receipt escape this call path as generic errors and Nest will surface them as 500s. During a workbench DB outage, missing function, or permission regression, clients lose the intended retryable 503 signal for exactly the durable storage case this endpoint adds.

Useful? React with 👍 / 👎.

) {
return this.translate(async () => {
this.requiredHeaders(idempotencyKey, correlationId);
this.requireLegacyQueue();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Allow legacy idempotent replays without the queue

When WORKBENCH_QUEUES temporarily excludes legacy, this check now runs before the legacy manager can look up an existing receipt for the same idempotency key. That means a retry/replay of an already accepted export (and the same pattern on imports) returns 409 The legacy processing queue is not enabled even though no new enqueue is needed, breaking the idempotency contract during queue maintenance or partial rollouts. Move the queue-enabled check to only the branch that is about to enqueue a newly QUEUED receipt.

Useful? React with 👍 / 👎.

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