@sixb/pipeline-worker 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +34 -0
- package/dist/errors.d.ts +34 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/events.d.ts +15 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/execute-run-step.d.ts +19 -0
- package/dist/execute-run-step.d.ts.map +1 -0
- package/dist/execute-sql-step.d.ts +18 -0
- package/dist/execute-sql-step.d.ts.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +654 -0
- package/dist/index.js.map +17 -0
- package/dist/run-pipeline-job.d.ts +9 -0
- package/dist/run-pipeline-job.d.ts.map +1 -0
- package/dist/run-step.d.ts +14 -0
- package/dist/run-step.d.ts.map +1 -0
- package/dist/step-inputs.d.ts +16 -0
- package/dist/step-inputs.d.ts.map +1 -0
- package/dist/tsconfig.json +5 -0
- package/dist/types.d.ts +59 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/worker.d.ts +13 -0
- package/dist/worker.d.ts.map +1 -0
- package/package.json +57 -0
- package/src/errors.ts +110 -0
- package/src/events.ts +189 -0
- package/src/execute-run-step.ts +98 -0
- package/src/execute-sql-step.ts +115 -0
- package/src/index.ts +2 -0
- package/src/run-pipeline-job.ts +130 -0
- package/src/run-step.ts +154 -0
- package/src/step-inputs.ts +84 -0
- package/src/types.ts +88 -0
- package/src/worker.ts +165 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/worker.ts
|
|
3
|
+
import { reportRunFailure } from "@sixb/core/internal/error-reporting";
|
|
4
|
+
import { QueueWorker } from "@sixb/core/internal/workers";
|
|
5
|
+
|
|
6
|
+
// src/events.ts
|
|
7
|
+
var SOURCE = "SixbPipelineWorker";
|
|
8
|
+
async function emitPipelineRunStarted(events, run) {
|
|
9
|
+
await events?.emit({
|
|
10
|
+
events: [
|
|
11
|
+
{
|
|
12
|
+
type: "pipeline.run.started",
|
|
13
|
+
payload: {
|
|
14
|
+
pipelineId: run.pipelineId,
|
|
15
|
+
runId: run.id,
|
|
16
|
+
startedAt: run.startedAt.toISOString()
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
}, { source: SOURCE });
|
|
21
|
+
}
|
|
22
|
+
async function emitPipelineRunStepStarted(events, step, context) {
|
|
23
|
+
await events?.emit({
|
|
24
|
+
events: [
|
|
25
|
+
{
|
|
26
|
+
type: "pipeline.run.step.started",
|
|
27
|
+
payload: {
|
|
28
|
+
pipelineId: step.pipelineId,
|
|
29
|
+
runId: step.pipelineRunId,
|
|
30
|
+
stepRunId: step.id,
|
|
31
|
+
stepId: step.stepId,
|
|
32
|
+
stepIndex: context.stepIndex,
|
|
33
|
+
totalSteps: context.totalSteps,
|
|
34
|
+
datasetId: step.datasetId,
|
|
35
|
+
startedAt: step.startedAt.toISOString()
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
}, { source: SOURCE });
|
|
40
|
+
}
|
|
41
|
+
async function emitPipelineRunStepFinished(events, step, context) {
|
|
42
|
+
await events?.emit({
|
|
43
|
+
events: [
|
|
44
|
+
{
|
|
45
|
+
type: "pipeline.run.step.finished",
|
|
46
|
+
payload: {
|
|
47
|
+
pipelineId: step.pipelineId,
|
|
48
|
+
runId: step.pipelineRunId,
|
|
49
|
+
stepRunId: step.id,
|
|
50
|
+
stepId: step.stepId,
|
|
51
|
+
stepIndex: context.stepIndex,
|
|
52
|
+
totalSteps: context.totalSteps,
|
|
53
|
+
datasetId: step.datasetId,
|
|
54
|
+
status: requireTerminalStatus(step.status, `Pipeline step run '${step.id}'`),
|
|
55
|
+
finishedAt: requireFinishedAt(step.id, step.finishedAt).toISOString(),
|
|
56
|
+
...step.output ? { versionId: step.output.versionId } : {},
|
|
57
|
+
...step.rowsWritten !== undefined ? { rowsWritten: step.rowsWritten } : {},
|
|
58
|
+
...step.error ? { error: step.error.message } : {}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
]
|
|
62
|
+
}, { source: SOURCE });
|
|
63
|
+
}
|
|
64
|
+
async function emitDatasetVersionCommitted(events, job, step) {
|
|
65
|
+
if (!step.versionCreated)
|
|
66
|
+
return;
|
|
67
|
+
await events?.emit({
|
|
68
|
+
events: [
|
|
69
|
+
{
|
|
70
|
+
type: "dataset.version.committed",
|
|
71
|
+
payload: {
|
|
72
|
+
datasetId: step.version.datasetId,
|
|
73
|
+
versionId: step.version.versionId,
|
|
74
|
+
createdAt: step.version.createdAt.toISOString(),
|
|
75
|
+
producer: {
|
|
76
|
+
kind: "pipeline",
|
|
77
|
+
id: job.pipelineId,
|
|
78
|
+
runId: job.id,
|
|
79
|
+
stepId: step.run.stepId
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
]
|
|
84
|
+
}, { source: SOURCE });
|
|
85
|
+
}
|
|
86
|
+
async function emitPipelineRunFinished(events, job) {
|
|
87
|
+
await events?.emit({ events: [{ type: "pipeline.run.finished", payload: buildPipelineRunFinishedPayload(job) }] }, { source: SOURCE });
|
|
88
|
+
}
|
|
89
|
+
function buildPipelineRunFinishedPayload(job) {
|
|
90
|
+
return {
|
|
91
|
+
pipelineId: job.pipelineId,
|
|
92
|
+
runId: job.id,
|
|
93
|
+
status: job.status,
|
|
94
|
+
...job.datasetId && job.versionId ? {
|
|
95
|
+
datasetId: job.datasetId,
|
|
96
|
+
versionId: job.versionId
|
|
97
|
+
} : {}
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function requireFinishedAt(stepRunId, finishedAt) {
|
|
101
|
+
if (finishedAt) {
|
|
102
|
+
return finishedAt;
|
|
103
|
+
}
|
|
104
|
+
throw new Error(`[SixbPipelineWorker] Pipeline step run '${stepRunId}' has no finishedAt.`);
|
|
105
|
+
}
|
|
106
|
+
function requireTerminalStatus(status, context) {
|
|
107
|
+
if (status === "running") {
|
|
108
|
+
throw new Error(`[SixbPipelineWorker] ${context} is still running.`);
|
|
109
|
+
}
|
|
110
|
+
return status;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// src/run-pipeline-job.ts
|
|
114
|
+
import { resolveLogsRuntime } from "@sixb/core/internal/logging";
|
|
115
|
+
|
|
116
|
+
// src/errors.ts
|
|
117
|
+
class PipelineWorkerError extends Error {
|
|
118
|
+
name = "PipelineWorkerError";
|
|
119
|
+
}
|
|
120
|
+
function toPipelineRunFailure(error) {
|
|
121
|
+
if (error instanceof Error) {
|
|
122
|
+
return {
|
|
123
|
+
name: error.name,
|
|
124
|
+
message: error.message
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
message: String(error)
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function throwIfAborted(signal) {
|
|
132
|
+
if (signal.aborted) {
|
|
133
|
+
throw createAbortError();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function statusForFailure(signal, error) {
|
|
137
|
+
return signal.aborted || isAbortError(error) ? "cancelled" : "failed";
|
|
138
|
+
}
|
|
139
|
+
function requireFinishedAt2(runId, finishedAt) {
|
|
140
|
+
if (finishedAt) {
|
|
141
|
+
return finishedAt;
|
|
142
|
+
}
|
|
143
|
+
throw new PipelineWorkerError(`[SixbPipelineWorker] Pipeline run '${runId}' finished without a finishedAt timestamp.`);
|
|
144
|
+
}
|
|
145
|
+
function createStepBookkeepingError(options) {
|
|
146
|
+
return new PipelineWorkerError(`[SixbPipelineWorker] Pipeline '${options.pipelineId}' step '${options.stepId}' committed dataset version '${options.version.versionId}', but failed to finalize step run '${options.runId}'. The dataset commit may already have succeeded and the step run record may need repair.`, { cause: options.cause });
|
|
147
|
+
}
|
|
148
|
+
function createPipelineBookkeepingError(options) {
|
|
149
|
+
return new PipelineWorkerError(`[SixbPipelineWorker] Pipeline '${options.pipelineId}' committed final dataset version '${options.version.versionId}', but failed to finalize pipeline run '${options.runId}'. The dataset commit may already have succeeded and the pipeline run record may need repair.`, { cause: options.cause });
|
|
150
|
+
}
|
|
151
|
+
function requirePipeline(pipeline, job) {
|
|
152
|
+
if (!pipeline) {
|
|
153
|
+
throw new PipelineWorkerError(`[SixbPipelineWorker] Unknown pipeline '${job.pipelineId}'.`);
|
|
154
|
+
}
|
|
155
|
+
return pipeline;
|
|
156
|
+
}
|
|
157
|
+
function requireRegisteredDataset(options) {
|
|
158
|
+
if (options.dataset) {
|
|
159
|
+
return options.dataset;
|
|
160
|
+
}
|
|
161
|
+
const namedRole = options.role === "input" ? `input '${options.name ?? "unknown"}'` : "output";
|
|
162
|
+
throw new PipelineWorkerError(`[SixbPipelineWorker] Pipeline '${options.pipelineId}' step '${options.stepId}' ${namedRole} references unknown dataset '${options.datasetId}'.`);
|
|
163
|
+
}
|
|
164
|
+
function createAbortError() {
|
|
165
|
+
const error = new Error("Worker runtime aborted.");
|
|
166
|
+
error.name = "AbortError";
|
|
167
|
+
return error;
|
|
168
|
+
}
|
|
169
|
+
function isAbortError(error) {
|
|
170
|
+
return error instanceof Error && error.name === "AbortError";
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// src/step-inputs.ts
|
|
174
|
+
async function resolveStepInputs(input) {
|
|
175
|
+
const { runtime, pipeline, step } = input;
|
|
176
|
+
const resolved = [];
|
|
177
|
+
for (const [name, declaredDataset] of Object.entries(step.inputs)) {
|
|
178
|
+
const dataset = requireRegisteredDataset({
|
|
179
|
+
dataset: runtime.getDatasetById(declaredDataset.id),
|
|
180
|
+
pipelineId: pipeline.id,
|
|
181
|
+
stepId: step.id,
|
|
182
|
+
role: "input",
|
|
183
|
+
name,
|
|
184
|
+
datasetId: declaredDataset.id
|
|
185
|
+
});
|
|
186
|
+
const version = await runtime.lakeStorage.getLatestVersion(dataset.id);
|
|
187
|
+
if (!version) {
|
|
188
|
+
throw new PipelineWorkerError(`[SixbPipelineWorker] Pipeline '${pipeline.id}' step '${step.id}' input '${name}' dataset '${dataset.id}' has no committed version.`);
|
|
189
|
+
}
|
|
190
|
+
resolved.push({
|
|
191
|
+
name,
|
|
192
|
+
dataset,
|
|
193
|
+
version,
|
|
194
|
+
ref: {
|
|
195
|
+
datasetId: version.datasetId,
|
|
196
|
+
versionId: version.versionId
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
return resolved;
|
|
201
|
+
}
|
|
202
|
+
function createStepInputs(lakeStorage, resolved) {
|
|
203
|
+
const inputs = {};
|
|
204
|
+
for (const input of resolved) {
|
|
205
|
+
inputs[input.name] = {
|
|
206
|
+
dataset: input.dataset,
|
|
207
|
+
version: input.version,
|
|
208
|
+
readRows(readInput = {}) {
|
|
209
|
+
return lakeStorage.readRows({
|
|
210
|
+
...readInput,
|
|
211
|
+
datasetId: input.dataset.id,
|
|
212
|
+
versionId: input.version.versionId
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
return inputs;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// src/execute-run-step.ts
|
|
221
|
+
async function executeRunStep(input) {
|
|
222
|
+
const { runtime, pipeline, step, job, signal, outputDataset, resolvedInputs } = input;
|
|
223
|
+
if (step.executor.kind !== "run") {
|
|
224
|
+
throw new PipelineWorkerError(`[SixbPipelineWorker] Pipeline '${pipeline.id}' step '${step.id}' uses SQL execution, which is not supported by the run step executor.`);
|
|
225
|
+
}
|
|
226
|
+
const logger = input.logSession.withContext({ stepId: step.id });
|
|
227
|
+
let rowsWritten = 0;
|
|
228
|
+
const write = await runtime.lakeStorage.beginWrite({
|
|
229
|
+
dataset: outputDataset,
|
|
230
|
+
mode: step.mode,
|
|
231
|
+
inputs: resolvedInputs.map((resolved) => resolved.ref),
|
|
232
|
+
producer: {
|
|
233
|
+
kind: "pipeline",
|
|
234
|
+
id: pipeline.id,
|
|
235
|
+
runId: job.id,
|
|
236
|
+
stepId: step.id
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
try {
|
|
240
|
+
const context = {
|
|
241
|
+
projectId: runtime.id,
|
|
242
|
+
pipelineId: pipeline.id,
|
|
243
|
+
stepId: step.id,
|
|
244
|
+
runId: job.id,
|
|
245
|
+
signal,
|
|
246
|
+
logger,
|
|
247
|
+
inputs: createStepInputs(runtime.lakeStorage, resolvedInputs),
|
|
248
|
+
output: {
|
|
249
|
+
async writeRows(rows) {
|
|
250
|
+
await write.writeRows(countRowsWritten(rows, signal, () => {
|
|
251
|
+
rowsWritten += 1;
|
|
252
|
+
}));
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
await step.executor.handler(context);
|
|
257
|
+
throwIfAborted(signal);
|
|
258
|
+
const commit = await write.commit({
|
|
259
|
+
commitMessage: `pipeline ${pipeline.id} step ${step.id} run ${job.id}`
|
|
260
|
+
});
|
|
261
|
+
const { outcome, ...version } = commit;
|
|
262
|
+
return {
|
|
263
|
+
version,
|
|
264
|
+
versionCreated: outcome === "created",
|
|
265
|
+
rowsWritten
|
|
266
|
+
};
|
|
267
|
+
} catch (error) {
|
|
268
|
+
await write.abort().catch(() => {});
|
|
269
|
+
throw error;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
async function* countRowsWritten(rows, signal, onWritten) {
|
|
273
|
+
for await (const row of rows) {
|
|
274
|
+
throwIfAborted(signal);
|
|
275
|
+
yield row;
|
|
276
|
+
onWritten();
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// src/execute-sql-step.ts
|
|
281
|
+
function hasSqlExecutor(lakeStorage) {
|
|
282
|
+
const sql = "sql" in lakeStorage ? lakeStorage.sql : null;
|
|
283
|
+
return typeof sql === "object" && sql !== null && "execute" in sql && typeof sql.execute === "function";
|
|
284
|
+
}
|
|
285
|
+
function assertSqlModeSupported(input) {
|
|
286
|
+
const supported = input.mode === "append" ? input.capabilities.supportsAppend : input.capabilities.supportsSnapshot;
|
|
287
|
+
if (supported)
|
|
288
|
+
return;
|
|
289
|
+
throw new PipelineWorkerError(`[SixbPipelineWorker] Pipeline '${input.pipelineId}' step '${input.stepId}' writes in ` + `'${input.mode}' mode, which the ${input.dialect} SQL executor does not support.`);
|
|
290
|
+
}
|
|
291
|
+
async function executeSqlStep(input) {
|
|
292
|
+
const { runtime, pipeline, step, job, signal, outputDataset, resolvedInputs } = input;
|
|
293
|
+
if (step.executor.kind !== "sql") {
|
|
294
|
+
throw new PipelineWorkerError(`[SixbPipelineWorker] Pipeline '${pipeline.id}' step '${step.id}' uses run execution, which is not supported by the SQL step executor.`);
|
|
295
|
+
}
|
|
296
|
+
if (!hasSqlExecutor(runtime.lakeStorage)) {
|
|
297
|
+
throw new PipelineWorkerError(`[SixbPipelineWorker] Pipeline '${pipeline.id}' step '${step.id}' requires SQL transform support, but lake storage does not provide lakeStorage.sql.execute(...).`);
|
|
298
|
+
}
|
|
299
|
+
assertSqlModeSupported({
|
|
300
|
+
capabilities: runtime.lakeStorage.sql.capabilities,
|
|
301
|
+
dialect: runtime.lakeStorage.sql.dialect,
|
|
302
|
+
mode: step.mode,
|
|
303
|
+
pipelineId: pipeline.id,
|
|
304
|
+
stepId: step.id
|
|
305
|
+
});
|
|
306
|
+
throwIfAborted(signal);
|
|
307
|
+
const commit = await runtime.lakeStorage.sql.execute({
|
|
308
|
+
sources: Object.fromEntries(resolvedInputs.map((input2) => [
|
|
309
|
+
input2.name,
|
|
310
|
+
{
|
|
311
|
+
dataset: input2.dataset,
|
|
312
|
+
versionId: input2.version.versionId
|
|
313
|
+
}
|
|
314
|
+
])),
|
|
315
|
+
sql: step.executor.sql,
|
|
316
|
+
target: outputDataset,
|
|
317
|
+
mode: step.mode,
|
|
318
|
+
producer: {
|
|
319
|
+
kind: "pipeline",
|
|
320
|
+
id: pipeline.id,
|
|
321
|
+
runId: job.id,
|
|
322
|
+
stepId: step.id
|
|
323
|
+
},
|
|
324
|
+
commitMessage: `pipeline ${pipeline.id} step ${step.id} run ${job.id}`
|
|
325
|
+
});
|
|
326
|
+
throwIfAborted(signal);
|
|
327
|
+
const { outcome, ...version } = commit;
|
|
328
|
+
return {
|
|
329
|
+
version,
|
|
330
|
+
versionCreated: outcome === "created",
|
|
331
|
+
rowsWritten: version.rowCount
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// src/run-step.ts
|
|
336
|
+
async function runStep(input) {
|
|
337
|
+
const { runtime, pipeline, step, stepIndex, job, signal } = input;
|
|
338
|
+
const lifecycleContext = {
|
|
339
|
+
stepIndex,
|
|
340
|
+
totalSteps: pipeline.graph.nodes.length
|
|
341
|
+
};
|
|
342
|
+
throwIfAborted(signal);
|
|
343
|
+
const resolvedInputs = await resolveStepInputs({ runtime, pipeline, step });
|
|
344
|
+
const inputRefs = resolvedInputs.map((resolved) => resolved.ref);
|
|
345
|
+
const outputDataset = requireRegisteredDataset({
|
|
346
|
+
dataset: runtime.getDatasetById(step.output.id),
|
|
347
|
+
pipelineId: pipeline.id,
|
|
348
|
+
stepId: step.id,
|
|
349
|
+
role: "output",
|
|
350
|
+
datasetId: step.output.id
|
|
351
|
+
});
|
|
352
|
+
await runtime.lakeStorage.createDataset(outputDataset);
|
|
353
|
+
const stepRunId = `${job.id}:step:${stepIndex + 1}:${step.id}`;
|
|
354
|
+
const startedStepRun = await runtime.pipelineRunsStorage.startStep({
|
|
355
|
+
projectId: runtime.id,
|
|
356
|
+
id: stepRunId,
|
|
357
|
+
pipelineRunId: job.id,
|
|
358
|
+
pipelineId: pipeline.id,
|
|
359
|
+
stepId: step.id,
|
|
360
|
+
datasetId: outputDataset.id,
|
|
361
|
+
mode: step.mode,
|
|
362
|
+
inputs: inputRefs
|
|
363
|
+
});
|
|
364
|
+
await input.onStepStarted?.(startedStepRun, lifecycleContext);
|
|
365
|
+
let committedVersion;
|
|
366
|
+
let rowsWritten;
|
|
367
|
+
try {
|
|
368
|
+
const execution = await executeStep({
|
|
369
|
+
runtime,
|
|
370
|
+
pipeline,
|
|
371
|
+
step,
|
|
372
|
+
job,
|
|
373
|
+
signal,
|
|
374
|
+
logSession: input.logSession,
|
|
375
|
+
outputDataset,
|
|
376
|
+
resolvedInputs
|
|
377
|
+
});
|
|
378
|
+
committedVersion = execution.version;
|
|
379
|
+
rowsWritten = execution.rowsWritten;
|
|
380
|
+
let stepRun;
|
|
381
|
+
try {
|
|
382
|
+
stepRun = await runtime.pipelineRunsStorage.finishStep({
|
|
383
|
+
projectId: runtime.id,
|
|
384
|
+
id: stepRunId,
|
|
385
|
+
status: "succeeded",
|
|
386
|
+
output: {
|
|
387
|
+
datasetId: committedVersion.datasetId,
|
|
388
|
+
versionId: committedVersion.versionId
|
|
389
|
+
},
|
|
390
|
+
rowsWritten
|
|
391
|
+
});
|
|
392
|
+
} catch (error) {
|
|
393
|
+
throw createStepBookkeepingError({
|
|
394
|
+
pipelineId: pipeline.id,
|
|
395
|
+
stepId: step.id,
|
|
396
|
+
runId: stepRunId,
|
|
397
|
+
version: committedVersion,
|
|
398
|
+
cause: error
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
await input.onStepFinished?.(stepRun, lifecycleContext);
|
|
402
|
+
return {
|
|
403
|
+
run: stepRun,
|
|
404
|
+
version: committedVersion,
|
|
405
|
+
versionCreated: execution.versionCreated
|
|
406
|
+
};
|
|
407
|
+
} catch (error) {
|
|
408
|
+
if (!committedVersion) {
|
|
409
|
+
const failedStepRun = await runtime.pipelineRunsStorage.finishStep({
|
|
410
|
+
projectId: runtime.id,
|
|
411
|
+
id: stepRunId,
|
|
412
|
+
status: statusForFailure(signal, error),
|
|
413
|
+
rowsWritten,
|
|
414
|
+
error: toPipelineRunFailure(error)
|
|
415
|
+
}).catch(() => null);
|
|
416
|
+
if (failedStepRun) {
|
|
417
|
+
await input.onStepFinished?.(failedStepRun, lifecycleContext);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
throw error;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
async function executeStep(input) {
|
|
424
|
+
switch (input.step.executor.kind) {
|
|
425
|
+
case "run":
|
|
426
|
+
return executeRunStep(input);
|
|
427
|
+
case "sql":
|
|
428
|
+
return executeSqlStep(input);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// src/run-pipeline-job.ts
|
|
433
|
+
class PipelineRunAlreadyStartedError extends Error {
|
|
434
|
+
run;
|
|
435
|
+
name = "PipelineRunAlreadyStartedError";
|
|
436
|
+
constructor(run) {
|
|
437
|
+
super(`[SixbPipelineWorker] Pipeline run '${run.id}' has already started.`);
|
|
438
|
+
this.run = run;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
async function runPipelineJob(input) {
|
|
442
|
+
const { runtime, job } = input;
|
|
443
|
+
const signal = input.signal ?? new AbortController().signal;
|
|
444
|
+
const pipeline = requirePipeline(runtime.getPipelineById(job.pipelineId), job);
|
|
445
|
+
const logSession = resolveLogsRuntime(runtime.id, runtime.logs).startExecution({
|
|
446
|
+
kind: "pipeline",
|
|
447
|
+
id: job.id
|
|
448
|
+
});
|
|
449
|
+
const steps = [];
|
|
450
|
+
let startedRun;
|
|
451
|
+
let finalVersion;
|
|
452
|
+
let finished = false;
|
|
453
|
+
throwIfAborted(signal);
|
|
454
|
+
try {
|
|
455
|
+
try {
|
|
456
|
+
startedRun = await runtime.pipelineRunsStorage.start({
|
|
457
|
+
projectId: runtime.id,
|
|
458
|
+
id: job.id,
|
|
459
|
+
pipelineId: pipeline.id
|
|
460
|
+
});
|
|
461
|
+
} catch (error) {
|
|
462
|
+
const existing = await runtime.pipelineRunsStorage.getById({
|
|
463
|
+
projectId: runtime.id,
|
|
464
|
+
id: job.id
|
|
465
|
+
});
|
|
466
|
+
if (existing?.pipelineId === pipeline.id) {
|
|
467
|
+
throw new PipelineRunAlreadyStartedError(existing);
|
|
468
|
+
}
|
|
469
|
+
throw error;
|
|
470
|
+
}
|
|
471
|
+
await input.onRunStarted?.(startedRun);
|
|
472
|
+
for (const [stepIndex, node] of pipeline.graph.nodes.entries()) {
|
|
473
|
+
const stepResult = await runStep({
|
|
474
|
+
runtime,
|
|
475
|
+
pipeline,
|
|
476
|
+
step: node.step,
|
|
477
|
+
stepIndex,
|
|
478
|
+
job,
|
|
479
|
+
signal,
|
|
480
|
+
logSession,
|
|
481
|
+
onStepStarted: input.onStepStarted,
|
|
482
|
+
onStepFinished: input.onStepFinished
|
|
483
|
+
});
|
|
484
|
+
steps.push(stepResult);
|
|
485
|
+
finalVersion = stepResult.version;
|
|
486
|
+
await input.onStepCommitted?.(stepResult);
|
|
487
|
+
}
|
|
488
|
+
let run;
|
|
489
|
+
try {
|
|
490
|
+
run = await runtime.pipelineRunsStorage.finish({
|
|
491
|
+
projectId: runtime.id,
|
|
492
|
+
id: job.id,
|
|
493
|
+
status: "succeeded",
|
|
494
|
+
output: finalVersion ? {
|
|
495
|
+
datasetId: finalVersion.datasetId,
|
|
496
|
+
versionId: finalVersion.versionId
|
|
497
|
+
} : undefined
|
|
498
|
+
});
|
|
499
|
+
} catch (error) {
|
|
500
|
+
if (finalVersion) {
|
|
501
|
+
throw createPipelineBookkeepingError({
|
|
502
|
+
pipelineId: pipeline.id,
|
|
503
|
+
runId: job.id,
|
|
504
|
+
version: finalVersion,
|
|
505
|
+
cause: error
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
throw error;
|
|
509
|
+
}
|
|
510
|
+
finished = true;
|
|
511
|
+
return {
|
|
512
|
+
run: {
|
|
513
|
+
...run,
|
|
514
|
+
finishedAt: requireFinishedAt2(job.id, run.finishedAt)
|
|
515
|
+
},
|
|
516
|
+
steps,
|
|
517
|
+
version: finalVersion
|
|
518
|
+
};
|
|
519
|
+
} catch (error) {
|
|
520
|
+
if (startedRun && !finished) {
|
|
521
|
+
const status = statusForFailure(signal, error);
|
|
522
|
+
try {
|
|
523
|
+
const run = await runtime.pipelineRunsStorage.finish({
|
|
524
|
+
projectId: runtime.id,
|
|
525
|
+
id: job.id,
|
|
526
|
+
status,
|
|
527
|
+
error: toPipelineRunFailure(error)
|
|
528
|
+
});
|
|
529
|
+
if (status === "failed" && run.status === "failed")
|
|
530
|
+
input.onRunFailed?.(error, run);
|
|
531
|
+
} catch {}
|
|
532
|
+
}
|
|
533
|
+
throw error;
|
|
534
|
+
} finally {
|
|
535
|
+
await logSession.flush();
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// src/worker.ts
|
|
540
|
+
class PipelineWorker extends QueueWorker {
|
|
541
|
+
context;
|
|
542
|
+
sixb;
|
|
543
|
+
constructor(sixb) {
|
|
544
|
+
if (sixb.listPipelines().length === 0) {
|
|
545
|
+
throw new Error("[SixbPipelineWorker] No pipeline definitions are registered.");
|
|
546
|
+
}
|
|
547
|
+
const pipelineRunsStorage = sixb.storage.pipelineRuns;
|
|
548
|
+
if (!pipelineRunsStorage) {
|
|
549
|
+
throw new Error("[SixbPipelineWorker] Pipeline workers require storage.pipelineRuns support.");
|
|
550
|
+
}
|
|
551
|
+
super({
|
|
552
|
+
projectId: sixb.id,
|
|
553
|
+
queue: sixb.queues.pipelines,
|
|
554
|
+
workerId: `pipeline-worker-${sixb.id}`
|
|
555
|
+
});
|
|
556
|
+
this.context = buildPipelineContext(sixb, pipelineRunsStorage);
|
|
557
|
+
this.sixb = sixb;
|
|
558
|
+
}
|
|
559
|
+
async execute(claimed, signal) {
|
|
560
|
+
const { job } = claimed;
|
|
561
|
+
const pipelineJob = {
|
|
562
|
+
id: job.payload.runId ?? `${job.id}:attempt:${job.attempt}`,
|
|
563
|
+
pipelineId: job.payload.pipelineId
|
|
564
|
+
};
|
|
565
|
+
let result;
|
|
566
|
+
try {
|
|
567
|
+
result = await runPipelineJob({
|
|
568
|
+
runtime: this.context,
|
|
569
|
+
job: pipelineJob,
|
|
570
|
+
signal,
|
|
571
|
+
onRunStarted: (run) => emitPipelineRunStarted(this.sixb.events, run),
|
|
572
|
+
onRunFailed: (error, run) => {
|
|
573
|
+
reportRunFailure(this.sixb, error, {
|
|
574
|
+
projectId: this.sixb.id,
|
|
575
|
+
occurredAt: run.finishedAt,
|
|
576
|
+
attempt: job.attempt,
|
|
577
|
+
run: {
|
|
578
|
+
kind: "pipeline",
|
|
579
|
+
runId: run.id,
|
|
580
|
+
pipelineId: run.pipelineId
|
|
581
|
+
}
|
|
582
|
+
});
|
|
583
|
+
},
|
|
584
|
+
onStepStarted: (step, context) => emitPipelineRunStepStarted(this.sixb.events, step, context),
|
|
585
|
+
onStepFinished: (step, context) => emitPipelineRunStepFinished(this.sixb.events, step, context),
|
|
586
|
+
onStepCommitted: (step) => emitDatasetVersionCommitted(this.sixb.events, pipelineJob, step)
|
|
587
|
+
});
|
|
588
|
+
} catch (error) {
|
|
589
|
+
if (error instanceof PipelineRunAlreadyStartedError)
|
|
590
|
+
return;
|
|
591
|
+
throw error;
|
|
592
|
+
}
|
|
593
|
+
await emitPipelineRunFinished(this.sixb.events, {
|
|
594
|
+
id: pipelineJob.id,
|
|
595
|
+
pipelineId: pipelineJob.pipelineId,
|
|
596
|
+
status: "succeeded",
|
|
597
|
+
datasetId: result.version?.datasetId,
|
|
598
|
+
versionId: result.version?.versionId
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
async onExecutionError(claimed, _error) {
|
|
602
|
+
const { job } = claimed;
|
|
603
|
+
await emitPipelineRunFinished(this.sixb.events, {
|
|
604
|
+
id: job.payload.runId ?? `${job.id}:attempt:${job.attempt}`,
|
|
605
|
+
pipelineId: job.payload.pipelineId,
|
|
606
|
+
status: "failed"
|
|
607
|
+
});
|
|
608
|
+
return { kind: "fail" };
|
|
609
|
+
}
|
|
610
|
+
async onAbortError(claimed, _error) {
|
|
611
|
+
const { job } = claimed;
|
|
612
|
+
const pipelineJob = {
|
|
613
|
+
id: job.payload.runId ?? `${job.id}:attempt:${job.attempt}`,
|
|
614
|
+
pipelineId: job.payload.pipelineId
|
|
615
|
+
};
|
|
616
|
+
if (!await hasCommittedStep(this.context, pipelineJob.id)) {
|
|
617
|
+
return { kind: "retry" };
|
|
618
|
+
}
|
|
619
|
+
await emitPipelineRunFinished(this.sixb.events, {
|
|
620
|
+
id: pipelineJob.id,
|
|
621
|
+
pipelineId: pipelineJob.pipelineId,
|
|
622
|
+
status: "cancelled"
|
|
623
|
+
});
|
|
624
|
+
return { kind: "fail" };
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
function buildPipelineContext(sixb, pipelineRunsStorage) {
|
|
628
|
+
return {
|
|
629
|
+
id: sixb.id,
|
|
630
|
+
pipelineRunsStorage,
|
|
631
|
+
lakeStorage: sixb.lakeStorage,
|
|
632
|
+
logs: sixb.logs,
|
|
633
|
+
getPipelineById(pipelineId) {
|
|
634
|
+
return sixb.getPipelineById(pipelineId);
|
|
635
|
+
},
|
|
636
|
+
getDatasetById(datasetId) {
|
|
637
|
+
return sixb.getDatasetById(datasetId);
|
|
638
|
+
}
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
async function hasCommittedStep(context, pipelineRunId) {
|
|
642
|
+
const result = await context.pipelineRunsStorage.listSteps({
|
|
643
|
+
projectId: context.id,
|
|
644
|
+
pipelineRunId,
|
|
645
|
+
statuses: ["succeeded"],
|
|
646
|
+
limit: 1
|
|
647
|
+
});
|
|
648
|
+
return result.steps.some((step) => Boolean(step.output));
|
|
649
|
+
}
|
|
650
|
+
export {
|
|
651
|
+
PipelineWorker
|
|
652
|
+
};
|
|
653
|
+
|
|
654
|
+
//# debugId=068611D3BE49C18D64756E2164756E21
|