fix(server): release settled Discord work during drain

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-25 10:01:57 -05:00
1 parent 7d89e7691f
commit 954a0faa75
3 files changed
+20 -2

No files matched your search

@@ -29,3 +29,17 @@ Discord command registration and receiver discovery require the same causal
check at their own parents, then propagation through every dependent stack PR. check at their own parents, then propagation through every dependent stack PR.
The full original recurring-work inventory, milestone producer audit, older E2E The full original recurring-work inventory, milestone producer audit, older E2E
failure and inbound cleanup/deletion policy finding remain outstanding. failure and inbound cleanup/deletion policy finding remain outstanding.
## Discord command registration — slice 19 / #17557
The strengthened outer-rollback/drain case reproduces active=1 after commit in
/tmp/sleep19-drain-before.log. The repair preserves the dirty scope and saved
deadlines, but reports pending dirty work only while dispatch is allowed. The
shared coordinator still holds unresolved transactions independently.
All 67 registration/store cases pass in /tmp/sleep19-drain-tests.log, including
90 seconds of no SQL/provider calls and an unchanged idle snapshot during drain,
followed by registration on resume without another producer. Production server
typecheck passes in /tmp/sleep19-drain-typecheck.log. These tests use the migrated
paperclip_sleep19_drain_20261025_test database. Remote CI/review must run on the
new head; this is not evidence of provider suspension.
@@ -88,7 +88,9 @@ export function registerDiscordCommandWork(
if (due.length > batch.entries.length) earliest = Date.now() + 1_000; if (due.length > batch.entries.length) earliest = Date.now() + 1_000;
if (errors.length) throw new AggregateError(errors, "Discord command registration could not settle"); if (errors.length) throw new AggregateError(errors, "Discord command registration could not settle");
}, },
hasPending: async () => dirty.size > 0, // Preserve hints/deadlines for resume without holding settled intent during drain.
// The coordinator independently fences unresolved database transactions.
hasPending: async () => canRun() && dirty.size > 0,
nextRunAt: next, nextRunAt: next,
}); });
} }
@@ -765,7 +765,9 @@ suite("Discord command ownership store (real PostgreSQL, no network)", () => {
await sqlQuiet(60_000); await sqlQuiet(60_000);
draining = true; draining = true;
await db.transaction(async tx => activate(tx as unknown as typeof db)); await db.transaction(async tx => activate(tx as unknown as typeof db));
await sqlQuiet(90_000); expect(f.calls).toEqual([]); await vi.waitFor(async () => { await vi.advanceTimersByTimeAsync(1000); expect(idleWorkSnapshot().active).toBe(0); });
const drained = idleWorkSnapshot();
await sqlQuiet(90_000); expect(idleWorkSnapshot()).toEqual(drained); expect(f.calls).toEqual([]);
draining = false; draining = false;
await vi.advanceTimersByTimeAsync(30_000); await vi.advanceTimersByTimeAsync(30_000);
await vi.waitFor(() => expect(f.calls).toEqual(["GET", "POST"])); await vi.waitFor(() => expect(f.calls).toEqual(["GET", "POST"]));