From 686e41547355919e3ee788739cc9c1c639a44d35 Mon Sep 17 00:00:00 2001 From: lyon Date: Thu, 25 Jun 2026 10:45:51 +0800 Subject: [PATCH] fix: decouple postgres write readiness from durable reads --- internal/db/runtime-store.test.ts | 33 +++++++++++++++++++++++++++++++ internal/db/runtime-store.ts | 8 ++++++-- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/internal/db/runtime-store.test.ts b/internal/db/runtime-store.test.ts index ad055157..8e6c75fe 100644 --- a/internal/db/runtime-store.test.ts +++ b/internal/db/runtime-store.test.ts @@ -966,6 +966,37 @@ test("configured postgres runtime allocates Workbench projectedSeq idempotently assert.ok(queryClient.calls.some((call) => call.sql.startsWith("SELECT pg_advisory_xact_lock"))); }); +test("configured postgres runtime writes Workbench facts without re-running blocked read readiness", async () => { + const queryClient = createFakePostgresClient({ migrationReady: true, countErrorCode: "53300" }); + const store = createConfiguredCloudRuntimeStore({ + env: { + HWLAB_CLOUD_RUNTIME_ADAPTER: "postgres", + HWLAB_CLOUD_RUNTIME_DURABLE: "true" + }, + dbUrl: "postgres://hwlab_redacted@db.example.invalid:5432/hwlab", + queryClient, + now: () => "2026-06-25T02:50:00.000Z" + }); + + const readiness = await store.readiness(); + assert.equal(readiness.blocker, RUNTIME_DURABLE_ADAPTER_QUERY_BLOCKED); + assert.equal(readiness.connection.queryResult, "query_blocked"); + queryClient.calls.length = 0; + + const write = await store.writeWorkbenchFacts({ + facts: { + sessions: [{ sessionId: "ses_write_after_read_blocked", ownerUserId: "usr_write_after_read_blocked", status: "running", updatedAt: "2026-06-25T02:50:01.000Z" }] + } + }); + + assert.equal(write.written, true); + assert.equal(queryClient.calls.filter((call) => isReadinessQuery(call.sql)).length, 0); + assert.equal(queryClient.calls.some((call) => call.sql.startsWith("CREATE INDEX IF NOT EXISTS")), false); + assert.ok(queryClient.calls.some((call) => call.sql === "BEGIN")); + assert.ok(queryClient.calls.some((call) => call.sql === "COMMIT")); + assert.ok(queryClient.calls.some((call) => call.sql.startsWith("INSERT INTO workbench_sessions"))); +}); + test("configured postgres runtime reuses proven readiness for Workbench durable reads", async () => { const queryClient = createFakePostgresClient({ migrationReady: true }); const store = createConfiguredCloudRuntimeStore({ @@ -982,6 +1013,7 @@ test("configured postgres runtime reuses proven readiness for Workbench durable sessions: [{ sessionId: "ses_ready_cached", ownerUserId: "usr_ready", status: "running", updatedAt: "2026-06-20T10:00:01.000Z" }] } }); + await store.readiness(); queryClient.calls.length = 0; const first = await store.queryWorkbenchFacts({ families: ["sessions"], limit: 10 }); @@ -1029,6 +1061,7 @@ test("configured postgres runtime can query thin Workbench session summaries", a }] } }); + await store.readiness(); queryClient.calls.length = 0; const loaded = await store.queryWorkbenchFacts({ families: ["sessions"], sessionProjection: "summary", limit: 10 }); diff --git a/internal/db/runtime-store.ts b/internal/db/runtime-store.ts index d0011c1e..3ce2e8d8 100644 --- a/internal/db/runtime-store.ts +++ b/internal/db/runtime-store.ts @@ -1533,8 +1533,12 @@ export class PostgresCloudRuntimeStore { } async assertReadyForWrites() { - const readiness = await this.readiness(); - if (readiness.ready !== true) { + if (!this.dbUrl && !this.queryClient) { + const readiness = this.blockedSummary({ + blocker: RUNTIME_DURABLE_ADAPTER_UNCONFIGURED, + reason: "Postgres runtime adapter is selected but HWLAB_CLOUD_DB_URL is not injected" + }); + this.lastReadiness = readiness; throw new HwlabProtocolError("Postgres durable runtime adapter is not ready for writes", { code: ERROR_CODES.internalError, data: {