|
1 | 1 | import type { |
2 | 2 | CompletedWaitpointRecord, |
3 | | - ReadClient, |
4 | 3 | ResolveCompletedWaitpointsArgs, |
5 | 4 | RunStore, |
6 | 5 | } from "@internal/run-store"; |
@@ -64,17 +63,23 @@ const RUN_OUTPUT_CHUNK_SIZE = 100; |
64 | 63 | * `findRunsByIds` is the store's own grouped replacement for `Promise.all(ids.map(findRun))`, |
65 | 64 | * and it forces `id` into the projection so the map keys correctly even though this select |
66 | 65 | * names only `output`. |
| 66 | + * |
| 67 | + * Takes no read client on purpose. The router reads the owning store's REPLICA when no client is |
| 68 | + * passed, and forces its PRIMARY for any client that is not replica-branded -- so accepting one |
| 69 | + * would let a caller put a wide `TaskRun.output` read on the writer by reflex. This read does not |
| 70 | + * need read-your-writes: the child run committed its output before it completed the waitpoint that |
| 71 | + * unblocked this parent, so replica lag cannot hide it. That is the opposite of the envelope read |
| 72 | + * in the legacy arm, which reads a waitpoint completed moments earlier and must use the writer. |
67 | 73 | */ |
68 | 74 | export function createRunOutputsReader( |
69 | | - runStore: Pick<RunStore, "findRunsByIds">, |
70 | | - client?: ReadClient |
| 75 | + runStore: Pick<RunStore, "findRunsByIds"> |
71 | 76 | ): (taskRunIds: string[]) => Promise<Map<string, string>> { |
72 | 77 | return async (taskRunIds) => { |
73 | 78 | const outputs = new Map<string, string>(); |
74 | 79 |
|
75 | 80 | for (let i = 0; i < taskRunIds.length; i += RUN_OUTPUT_CHUNK_SIZE) { |
76 | 81 | const chunk = taskRunIds.slice(i, i + RUN_OUTPUT_CHUNK_SIZE); |
77 | | - const rows = await runStore.findRunsByIds(chunk, { select: { output: true } }, client); |
| 82 | + const rows = await runStore.findRunsByIds(chunk, { select: { output: true } }); |
78 | 83 |
|
79 | 84 | for (const [id, row] of rows) { |
80 | 85 | // A row present with a null output is the same absence as a missing row: either way the |
@@ -218,27 +223,34 @@ async function readDeferredOutputs( |
218 | 223 | deps: CompletedWaitpointResolverDeps |
219 | 224 | ): Promise<Map<string, string>> { |
220 | 225 | const runIds = new Set<string>(); |
221 | | - let deferring: CompletedWaitpointRecord | undefined; |
| 226 | + let read: NonNullable<CompletedWaitpointResolverDeps["readRunOutputs"]> | undefined; |
222 | 227 |
|
223 | 228 | for (const record of records) { |
224 | 229 | const runId = deferredRunIdOf(record); |
225 | | - if (runId !== undefined) { |
226 | | - runIds.add(runId); |
227 | | - deferring ??= record; |
| 230 | + if (runId === undefined) { |
| 231 | + continue; |
228 | 232 | } |
229 | | - } |
230 | 233 |
|
231 | | - if (runIds.size === 0) { |
232 | | - return new Map(); |
| 234 | + // Checked here rather than after the pass, so the message names a record that really does |
| 235 | + // defer rather than whichever one happened to be first. A missing reader is a wiring fault, |
| 236 | + // so every deferring record is equally implicated and the first is as informative as any. |
| 237 | + if (!deps.readRunOutputs) { |
| 238 | + throw new Error( |
| 239 | + `Waitpoint ${record.id} defers its output to run ${runId}, but the resolver was built with no run-output reader.` |
| 240 | + ); |
| 241 | + } |
| 242 | + |
| 243 | + read = deps.readRunOutputs; |
| 244 | + runIds.add(runId); |
233 | 245 | } |
234 | 246 |
|
235 | | - if (!deps.readRunOutputs) { |
236 | | - throw new Error( |
237 | | - `Waitpoint ${deferring?.id} defers its output to run ${deferring?.completedByTaskRunId}, but the resolver was built with no run-output reader.` |
238 | | - ); |
| 247 | + // Set exactly when a record deferred, so this is the same condition as an empty `runIds` -- |
| 248 | + // and unlike `runIds.size`, it carries the reader with it. |
| 249 | + if (read === undefined) { |
| 250 | + return new Map(); |
239 | 251 | } |
240 | 252 |
|
241 | | - return deps.readRunOutputs([...runIds]); |
| 253 | + return read([...runIds]); |
242 | 254 | } |
243 | 255 |
|
244 | 256 | // The run a record defers its output to, or undefined when it carries its own output or has |
|
0 commit comments