Skip to content

service-queue: the publish idempotency window never expires on Postgres/MySQL — String(row.created_at) >= windowStart compares a Date.toString() against ISO text #13993

Description

@zhuangjianguo

Found by the driver-materialisation consumer census on #13973 (class (c) — genuinely wrong on one side). That sweep is not addressed by this card and does not close it.

The site

packages/services/service-queue/src/db-queue-adapter.ts:250-263, in DbQueueAdapter#publish:

const windowStart = new Date(now.getTime() - this.opts.idempotencyWindowMs).toISOString();
const existing = await this.engine.find(QUEUE_TABLE, { where: { queue, idempotency_key: opts.idempotencyKey }, ... });
const blocking = (existing ?? []).find((row: any) => {
  if (row.status === 'pending' || row.status === 'running') return true;
  return String(row.created_at ?? '') >= windowStart;   // <-- here
});
if (blocking) return String(blocking.id);

windowStart is canonical ISO-8601 text: "2026-08-30T10:19:25.947Z".

Why it is wrong on the production default driver

created_at is a builtin audit column. It is not in datetimeFields, so no declared-field coercion reaches it, and SqlDriver#formatOutput repairs it only inside its if (this.isSqlite) arm. Pinned in packages/drivers/driver-sql/src/sql-driver-13567-audit-stamp-materialisation.test.ts: the live dialects hand updated_at/created_at out of the record read door as a JS Date; SQLite hands back canonical ISO-Z text.

So on Postgres and MySQL the left side of that comparison is

String(new Date()) === "Sun Aug 30 2026 18:19:25 GMT+0800 (China Standard Time)"

and the comparison is a lexicographic string compare of a value beginning with an ASCII letter against a value beginning with an ASCII digit. 'S' (0x53) >= '2' (0x32) — and the same holds for every weekday name and every ISO year this century. The predicate is unconditionally true.

Effect: on Postgres/MySQL every terminal (completed / dlq) row matching the idempotency key blocks a re-publish forever, instead of only while it is inside idempotencyWindowMs. publish() returns the old message id and silently enqueues nothing. The window the ADR-0057 retention comment at the top of this file is built around (the declared retention ... is measured on the very same created_at axis and is >= this window) does not exist on those dialects.

On SQLite both sides are canonical ISO-Z text, lexicographic order equals chronological order, and the check is correct. Every test this seam has drives SQLite or memory, which is why it has stayed green.

Why this is not a ?? fallback

Per #13973's standing prohibition, the open question is which side owes the canonical spelling, not how to make the consumer tolerant. Two candidate shapes:

A regression pin for either needs a Date on the read side; that discriminating input exists in CI only inside @objectstack/driver-sql today (see #13973's point-4 measurement), so the pin most likely belongs in this package driving a hand-made Date, the same split sql-driver-13567-audit-stamp-materialisation.test.ts documents.

Re-run

rg -n 'String\(row\.created_at' packages/services/service-queue/src/

Backlink: #13973 (census), #13382 (the OCC seam, the same shape). Neither is addressed here.

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions