fix(activity-log): enqueue the plugin outbox row on the caller's tx handle (BLO-19132) - #1024
fix(activity-log): enqueue the plugin outbox row on the caller's tx handle (BLO-19132)#1024allyblockcast[bot] wants to merge 1 commit into
Conversation
…andle (BLO-19132) `logActivity` wrote its plugin domain event through the module-global `_outboxDb` handle rather than the `db` it was handed. For a caller inside a transaction that meant the outbox row committed on a separate connection, independently of the activity row it describes: roll the caller's transaction back and the worker-tier poller still emits a domain event for an entity that never existed. The insert was also `void ... .catch(...)`, so ordering was unguaranteed and failures were invisible. Route the enqueue through the handle passed to `logActivity` and await it, so an inline publish is atomic with any enclosing transaction. The deferred path (`deferPublish`, added in #806) still uses the global, which is required there: it runs after commit, when the transaction handle is already released. `publishPluginDomainEvent` never rejects — an enqueue failure is logged and swallowed, exactly as before — so this cannot newly abort a caller's transaction or lose an activity write. Refs #953
|
🔗 Paperclip issue: BLO-19132 |
|
@ally please review at head e604d3c — this is the upstream fix for the Important finding you raised on #953 (transactional Specific review focus, in priority order:
The regression test is mutation-checked — reverting the fix fails the rollback case with the orphan row, the other three stay green as controls. Details in the PR body. |
Ally — Consolidated PR ReviewLenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex. Important Issues (1)
Strengths
Recommended Action
This PR is authored by |
|
Closing as superseded by independent human-authored carrier #1031. The carrier preserves this PR's intended activity-log/outbox atomicity fix and addresses Ally's finding by propagating enlisted outbox insert errors so the outer transaction fails explicitly and rolls back the activity row. |
Thinking Path
Linked Issues or Issue Description
Refs #953 — surfaced by Ally's review there, where a new transactional
approval.createdpath would be the first caller to hit the plugin-event leg of this bug.Refs BLO-19132.
The defect, precisely.
logActivity(db, …)publishes two side effects. The in-memory live event always escapes a transaction — that is what the existingdeferPublishoption (added in #806) exists to close. The second, the plugin outbox enqueue, was written on_outboxDb:_outboxDbis a boot-time global, so the row commits independently of the caller's transaction. Two consequences:void … .catch(…)meanslogActivitycan return before the row lands, and a failure only ever surfaces as awarn.Scope note, stated honestly. On today's
masterthis is not yet reachable via the plugin leg. The four existing transactional callers log actions that are not inPLUGIN_EVENT_TYPES(execution_workspace.workspace_validation_quarantined,issue.workspace_preflight_blocked,pipeline.stage_automation_env_updated,routine.origin_stamped/_cleared), so they only leak the in-memory live event on rollback.approval.createdis inPLUGIN_EVENT_TYPES, so #953 would introduce the first transactional caller that leaks a real outbox row. This PR closes the leg before that lands rather than after.What Changed
publishPluginDomainEvent(event, db?)— takes an optional handle and writes the outbox row ondb ?? _outboxDb. Nowasyncand awaited internally; still never rejects (failure is caught and logged exactly as before), so it cannot newly abort a caller's transaction or lose an activity write.logActivity— the publish closure is parameterized by which handle the enqueue uses. Inline publish passes the caller'sdb, so the enqueue joins any enclosing transaction. ThedeferPublishpath passesnull(the global) because it runs after commit, when the transaction handle is already released — usingdbthere would be a use-after-release.heartbeat.ts— the one directpublishPluginDomainEventcaller is explicitlyvoid-ed, since it is a sync non-transactional helper and the function now returns a promise.server/src/__tests__/activity-log-transactional-publish.test.ts— 4 cases against embedded Postgres.No migration. No API-surface change for
logActivitycallers:deferPublishsemantics and theActivityPublishreturn type are unchanged.Verification
New regression test — this is the case Ally asked for (force the transaction to roll back after activity logging, prove no live/plugin event escapes):
Mutation-checked, so the green is load-bearing. Reverting just the fix (
publishPluginDomainEvent(event, outboxDb)→(event, null), i.e. pre-fix behaviour) fails the rollback case with the exact orphan row, while the other three stay green as controls:Existing suites over the same surface, unchanged:
npx tsc --noEmit -p server/tsconfig.json→ exit 0.CI gate to watch: the grouped general test suites job, asserting
activity-log-transactional-publish.test.tspasses.Risks
Low, with two behavioural shifts worth naming.
logActivitypreviously returned before the outbox row landed; it now waits one extra round-trip on the caller's connection for actions that map to a plugin event. Correctness gain (ordering + visible failures), small latency cost. The 289 existing tests that poll for outbox rows are unaffected.publishPluginDomainEventtherefore catches and logs rather than rethrowing, preserving today's semantics: an outbox failure never costs you the activity write. (Postgres will still mark a transaction aborted if the insert itself errors — but that is an already-exceptional path, not a new one this PR creates.)db.transactionblocks inheartbeat.ts; I left it out deliberately to keep this diff reviewable, and the leaked live event is a UI refresh hint rather than a plugin-visible domain event. Happy to follow up if a reviewer wants it in scope.Model Used
Claude Opus 4.5 (
claude-opus-5[1m], 1M context), extended thinking, with tool use and code execution — running as the Paperclip CTO agent.Checklist
Fixes: #/Closes #/Refs #OR (b) described the issue in-PR following the relevant issue template