Skip to content

Commit e2e4138

Browse files
committed
fix(execution): keep an event in the buffer when a pressure offload fails
Durable compaction runs before an event is queued, so a storage or metadata failure dropped it from replay entirely — a reconnecting client would never see it, even though the live path carried on. Offloading under pressure is only an optimization that keeps a heavy run from exhausting its budget, so when the value cannot be persisted, fall back to buffering it inline: exactly what the run would have done before pressure engaged.
1 parent 84d9164 commit e2e4138

2 files changed

Lines changed: 47 additions & 12 deletions

File tree

apps/sim/lib/execution/event-buffer.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -785,6 +785,26 @@ describe('execution event buffer', () => {
785785
expect(persistedEntries.length).toBeGreaterThan(0)
786786
})
787787

788+
/**
789+
* Offloading under pressure is an optimization. If the value cannot be
790+
* persisted durably, the event must still reach the replay buffer inline —
791+
* dropping it would leave a reconnecting client permanently missing it.
792+
*/
793+
it('buffers the event inline when a pressure offload cannot be persisted', async () => {
794+
mockRedis.incrby.mockResolvedValue(100000)
795+
const payload = 'x'.repeat(2 * 1024 * 1024)
796+
797+
// No workspace/workflow ids, so durable persistence of an offloaded value
798+
// fails the way a storage outage would.
799+
const writer = createExecutionEventWriter('exec-1')
800+
for (let i = 0; i < 20; i++) {
801+
await writer.write(makeEvent(payload)).catch(() => {})
802+
}
803+
await writer.flush().catch(() => {})
804+
805+
expect(persistedEntries).toHaveLength(20)
806+
})
807+
788808
it('preserves requested UserFile base64 when buffering terminal events', async () => {
789809
mockRedis.incrby.mockResolvedValue(100)
790810
const base64 = Buffer.from('hello').toString('base64')

apps/sim/lib/execution/event-buffer.ts

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1022,17 +1022,37 @@ export function createExecutionEventWriter(
10221022
}
10231023
}
10241024

1025+
/**
1026+
* Compact an event for the buffer, degrading if pressure offloading fails.
1027+
*
1028+
* Offloading under pressure is an optimization: it keeps a heavy run from
1029+
* exhausting its budget. When durable storage rejects the write, losing the
1030+
* event from replay entirely is a worse outcome than carrying it inline, so
1031+
* fall back to the shared cap — exactly what the run would have done before
1032+
* pressure engaged.
1033+
*/
1034+
const compactForBuffer = async (event: ExecutionEvent) => {
1035+
const valueThresholdBytes = getValueThresholdBytes()
1036+
const options = { ...context, executionId, requireDurablePayloads: true }
1037+
if (valueThresholdBytes === undefined) return compactEventForBuffer(event, options)
1038+
try {
1039+
return await compactEventForBuffer(event, { ...options, valueThresholdBytes })
1040+
} catch (error) {
1041+
logger.warn('Pressure offload failed; buffering the event inline instead', {
1042+
executionId,
1043+
eventType: event.type,
1044+
error: toError(error).message,
1045+
})
1046+
return compactEventForBuffer(event, options)
1047+
}
1048+
}
1049+
10251050
const writeCore = async (event: ExecutionEvent): Promise<ExecutionEventEntry> => {
10261051
if (nextEventId === 0 || nextEventId > maxReservedId) {
10271052
await reserveIds(1)
10281053
}
10291054
const eventId = nextEventId++
1030-
const compactEvent = await compactEventForBuffer(event, {
1031-
...context,
1032-
executionId,
1033-
requireDurablePayloads: true,
1034-
valueThresholdBytes: getValueThresholdBytes(),
1035-
})
1055+
const compactEvent = await compactForBuffer(event)
10361056
const entry: ExecutionEventEntry = { eventId, executionId, event: compactEvent }
10371057
bufferedBytes += getJsonSize(entry) ?? 0
10381058
pending.push(entry)
@@ -1075,12 +1095,7 @@ export function createExecutionEventWriter(
10751095
await reserveIds(1)
10761096
}
10771097
const eventId = nextEventId++
1078-
const compactEvent = await compactEventForBuffer(event, {
1079-
...context,
1080-
executionId,
1081-
requireDurablePayloads: true,
1082-
valueThresholdBytes: getValueThresholdBytes(),
1083-
})
1098+
const compactEvent = await compactForBuffer(event)
10841099
const entry: ExecutionEventEntry = { eventId, executionId, event: compactEvent }
10851100
bufferedBytes += getJsonSize(entry) ?? 0
10861101
pending.push(entry)

0 commit comments

Comments
 (0)