Skip to content

Commit c3ee464

Browse files
V48 Gate 3 (implementation-only): fix executions-row race causing FK violations on every synthesis run
Every deposit synthesis run was logging repeated "Failed to persist stream event" (postgres 23503: execution_events.run_id FK not present in executions) and the client's post-completion GET /api/executions/history/<runId> could 404. Root cause: enablePipelineStreaming's "ensure an executions row exists" insert was fire-and-forget (not awaited), while createStreamingExecution emits its first status event synchronously right after — so the very first (and any early) execution_events insert could race ahead of the executions row actually landing. Fix (pipeline-stream-integration.ts): track the ensure-row insert as a promise and have the event-persistence subscriber await it before persisting the first event (no extra round trip — the insert was already in flight), plus a self-healing backstop that, on any 23503, ensures the row and retries the event once. This is the shared streaming integration used by every pipeline (deposit, read, terminal), so the fix applies universally, not just to deposit synthesis. Added two regression tests: one proves an event given a slow executions insert does not persist until the insert resolves (previously it would race ahead), the other proves a genuine 23503 self-heals via ensure+retry. Package suite green (23/23); typecheck clean.
1 parent 2b13e03 commit c3ee464

2 files changed

Lines changed: 147 additions & 29 deletions

File tree

packages/pipelines-generics/src/streaming/__tests__/pipeline-stream-integration.test.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,19 @@ import { enablePipelineStreaming, sourceSafeStreamEvent } from '../../streaming/
44

55
// Mock ORM model so we can assert persistence without a real DB
66
const createdEvents: any[] = [];
7+
let failNextCreateWithFkViolation = false;
78
jest.mock('@bitcode/orm', () => ({
89
ExecutionEventsModel: class {
910
constructor() {}
1011
async create(row: any) {
12+
if (failNextCreateWithFkViolation) {
13+
failNextCreateWithFkViolation = false;
14+
const err: any = new Error(
15+
'insert or update on table "execution_events" violates foreign key constraint "execution_events_run_id_fkey"',
16+
);
17+
err.code = '23503';
18+
throw err;
19+
}
1120
createdEvents.push(row);
1221
return { id: String(createdEvents.length), ...row };
1322
}
@@ -41,6 +50,89 @@ describe('pipeline-stream-integration', () => {
4150
});
4251
});
4352

53+
describe('pipeline-stream-integration — executions-row FK race (QA: "Failed to persist stream event" 23503)', () => {
54+
it('gates the first persisted event on the executions-row insert completing, instead of racing it', async () => {
55+
let resolveInsert: () => void;
56+
const insertPromise = new Promise<void>((resolve) => {
57+
resolveInsert = resolve;
58+
});
59+
const fakeSupabase: any = {
60+
from: (table: string) => {
61+
expect(table).toBe('executions');
62+
return {
63+
insert: async () => {
64+
await insertPromise; // simulates a slow executions-row insert round trip
65+
return { error: null };
66+
},
67+
};
68+
},
69+
};
70+
71+
const exec = new Execution('pipeline:race-test');
72+
const runId = '11111111-1111-4111-8111-111111111111';
73+
const streamer = enablePipelineStreaming(exec, {
74+
runId,
75+
userId: 'user-race',
76+
supabase: fakeSupabase,
77+
streamToDatabase: true,
78+
streamToSSE: false,
79+
});
80+
81+
const before = createdEvents.length;
82+
const writeDone = streamer.writeData(
83+
JSON.stringify({ type: 'status', message: 'started', timestamp: Date.now() }),
84+
);
85+
86+
// While the executions-row insert is still in flight, the event must NOT
87+
// have persisted yet (this is the FK-violation race: previously the event
88+
// insert fired immediately, unordered relative to the executions insert).
89+
await new Promise((r) => setTimeout(r, 10));
90+
expect(createdEvents.length).toBe(before);
91+
92+
resolveInsert!();
93+
await writeDone;
94+
await new Promise((r) => setTimeout(r, 10));
95+
96+
expect(createdEvents.length).toBe(before + 1);
97+
expect(createdEvents[createdEvents.length - 1].run_id).toBe(runId);
98+
});
99+
100+
it('self-heals a 23503 FK violation by ensuring the executions row and retrying once', async () => {
101+
let insertCallCount = 0;
102+
const fakeSupabase: any = {
103+
from: (table: string) => {
104+
expect(table).toBe('executions');
105+
return {
106+
insert: async () => {
107+
insertCallCount += 1;
108+
return { error: null };
109+
},
110+
};
111+
},
112+
};
113+
failNextCreateWithFkViolation = true;
114+
115+
const exec = new Execution('pipeline:retry-test');
116+
const runId = '22222222-2222-4222-8222-222222222222';
117+
const streamer = enablePipelineStreaming(exec, {
118+
runId,
119+
userId: 'user-retry',
120+
supabase: fakeSupabase,
121+
streamToDatabase: true,
122+
streamToSSE: false,
123+
});
124+
125+
const before = createdEvents.length;
126+
await streamer.writeData(JSON.stringify({ type: 'status', message: 'hello', timestamp: Date.now() }));
127+
await new Promise((r) => setTimeout(r, 10));
128+
129+
expect(createdEvents.length).toBe(before + 1);
130+
expect(createdEvents[createdEvents.length - 1].run_id).toBe(runId);
131+
// The upfront best-effort insert, plus the retry-triggered ensure-row insert.
132+
expect(insertCallCount).toBeGreaterThanOrEqual(2);
133+
});
134+
});
135+
44136
describe('sourceSafeStreamEvent (telemetry source-safety law, V48)', () => {
45137
const RAW_RESPONSE = '```json\n{\n "analysis": "secret plan prose",\n "steps": ["step one"]\n}\n```';
46138

packages/pipelines-generics/src/streaming/pipeline-stream-integration.ts

Lines changed: 55 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -121,45 +121,71 @@ export function enablePipelineStreaming(
121121
(config.structuredToDatabase !== true || process?.env?.BITCODE_PIPELINE_LEGACY_EVENTS_DB === '1');
122122

123123
if (legacyExecutionEventsEnabled && config.supabase) {
124-
// Best-effort: ensure an executions row exists if runId looks like a UUID
125-
try {
126-
const uuidRe = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
127-
const isUuid = uuidRe.test(String(config.runId || ''));
128-
if (isUuid) {
129-
const now = new Date().toISOString();
130-
// Try insert; ignore duplicate errors
131-
config.supabase
132-
.from('executions')
133-
.insert({
134-
id: config.runId,
135-
user_id: config.userId,
136-
status: 'running',
137-
type: 'agentic-execution:asset-pack',
138-
started_at: now,
139-
created_at: now,
140-
updated_at: now
141-
} as any)
142-
.then(() => {})
143-
.catch((e: any) => {
144-
if (!String(e?.message || '').toLowerCase().includes('duplicate')) {
145-
// swallow non-duplicate as best-effort
146-
}
147-
});
124+
const uuidRe = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
125+
const isUuid = uuidRe.test(String(config.runId || ''));
126+
127+
// Ensure an `executions` row exists before any `execution_events` row can
128+
// reference it (execution_events.run_id has a FK to executions.id). Idempotent
129+
// (duplicate insert = row already exists, either way).
130+
const ensureExecutionsRowExists = async (): Promise<void> => {
131+
if (!isUuid) return;
132+
const now = new Date().toISOString();
133+
try {
134+
await config.supabase.from('executions').insert({
135+
id: config.runId,
136+
user_id: config.userId,
137+
status: 'running',
138+
type: 'agentic-execution:asset-pack',
139+
started_at: now,
140+
created_at: now,
141+
updated_at: now,
142+
} as any);
143+
} catch (e: any) {
144+
if (!String(e?.message || '').toLowerCase().includes('duplicate')) {
145+
// swallow non-duplicate as best-effort; the retry-on-FK-violation below
146+
// is the guaranteed backstop.
147+
}
148148
}
149-
} catch {}
149+
};
150+
151+
// Fire immediately (don't block registerStreamer/emitEvent below), but track
152+
// the promise so the FIRST event to persist can await it — otherwise the
153+
// initial "Pipeline execution started" status event (emitted synchronously
154+
// right after this function returns, e.g. by createStreamingExecution) races
155+
// this insert and can hit a 23503 FK violation before the row lands.
156+
let pendingEnsureRow: Promise<void> | null = ensureExecutionsRowExists();
150157

151158
const eventsModel = new ExecutionEventsModel(config.supabase);
152-
159+
153160
// Subscribe to stream events and persist them
154161
streamer.subscribe(async (event) => {
155-
try {
156-
await eventsModel.create({
162+
if (pendingEnsureRow) {
163+
await pendingEnsureRow;
164+
pendingEnsureRow = null;
165+
}
166+
const persist = () =>
167+
eventsModel.create({
157168
run_id: config.runId,
158169
event_type: event.type,
159170
event_data: sourceSafeStreamEvent(event),
160171
created_at: new Date().toISOString(),
161172
});
162-
} catch (error) {
173+
try {
174+
await persist();
175+
} catch (error: any) {
176+
// Self-healing backstop: if the executions row still doesn't exist (any
177+
// residual race — slow network, replica lag, concurrent dispatch), ensure
178+
// it and retry once before giving up.
179+
if (error?.code === '23503') {
180+
try {
181+
await ensureExecutionsRowExists();
182+
await persist();
183+
return;
184+
} catch (retryError) {
185+
console.error('Failed to persist stream event after ensuring executions row:', retryError);
186+
return;
187+
}
188+
}
163189
console.error('Failed to persist stream event:', error);
164190
}
165191
});

0 commit comments

Comments
 (0)