@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/src/worker.ts ADDED
@@ -0,0 +1,165 @@
1
+ import { reportRunFailure } from "@sixb/core/internal/error-reporting"
2
+ import type { QueueWorkerFailureDecision } from "@sixb/core/internal/workers"
3
+ import { QueueWorker } from "@sixb/core/internal/workers"
4
+ import type { ClaimedQueueJob, PipelineRunRequestedQueueJob } from "@sixb/core/queues"
5
+ import type { PipelineRunStorage } from "@sixb/core/storage"
6
+ import {
7
+ emitDatasetVersionCommitted,
8
+ emitPipelineRunFinished,
9
+ emitPipelineRunStarted,
10
+ emitPipelineRunStepFinished,
11
+ emitPipelineRunStepStarted,
12
+ } from "./events"
13
+ import { PipelineRunAlreadyStartedError, runPipelineJob } from "./run-pipeline-job"
14
+ import type {
15
+ PipelineJob,
16
+ PipelineRunResult,
17
+ PipelineWorkerContext,
18
+ PipelineWorkerSixb,
19
+ } from "./types"
20
+
21
+ export class PipelineWorker extends QueueWorker<PipelineRunRequestedQueueJob> {
22
+ private readonly context: PipelineWorkerContext
23
+ private readonly sixb: PipelineWorkerSixb
24
+
25
+ constructor(sixb: PipelineWorkerSixb) {
26
+ if (sixb.listPipelines().length === 0) {
27
+ throw new Error("[SixbPipelineWorker] No pipeline definitions are registered.")
28
+ }
29
+
30
+ const pipelineRunsStorage = sixb.storage.pipelineRuns
31
+ if (!pipelineRunsStorage) {
32
+ throw new Error("[SixbPipelineWorker] Pipeline workers require storage.pipelineRuns support.")
33
+ }
34
+
35
+ super({
36
+ projectId: sixb.id,
37
+ queue: sixb.queues.pipelines,
38
+ workerId: `pipeline-worker-${sixb.id}`,
39
+ })
40
+
41
+ this.context = buildPipelineContext(sixb, pipelineRunsStorage)
42
+ this.sixb = sixb
43
+ }
44
+
45
+ protected async execute(
46
+ claimed: ClaimedQueueJob<PipelineRunRequestedQueueJob>,
47
+ signal: AbortSignal
48
+ ): Promise<void> {
49
+ const { job } = claimed
50
+ const pipelineJob: PipelineJob = {
51
+ id: job.payload.runId ?? `${job.id}:attempt:${job.attempt}`,
52
+ pipelineId: job.payload.pipelineId,
53
+ }
54
+
55
+ let result: PipelineRunResult
56
+ try {
57
+ result = await runPipelineJob({
58
+ runtime: this.context,
59
+ job: pipelineJob,
60
+ signal,
61
+ onRunStarted: (run) => emitPipelineRunStarted(this.sixb.events, run),
62
+ onRunFailed: (error, run) => {
63
+ reportRunFailure(this.sixb, error, {
64
+ projectId: this.sixb.id,
65
+ occurredAt: run.finishedAt,
66
+ attempt: job.attempt,
67
+ run: {
68
+ kind: "pipeline",
69
+ runId: run.id,
70
+ pipelineId: run.pipelineId,
71
+ },
72
+ })
73
+ },
74
+ onStepStarted: (step, context) =>
75
+ emitPipelineRunStepStarted(this.sixb.events, step, context),
76
+ onStepFinished: (step, context) =>
77
+ emitPipelineRunStepFinished(this.sixb.events, step, context),
78
+ onStepCommitted: (step) => emitDatasetVersionCommitted(this.sixb.events, pipelineJob, step),
79
+ })
80
+ } catch (error) {
81
+ if (error instanceof PipelineRunAlreadyStartedError) return
82
+ throw error
83
+ }
84
+
85
+ await emitPipelineRunFinished(this.sixb.events, {
86
+ id: pipelineJob.id,
87
+ pipelineId: pipelineJob.pipelineId,
88
+ status: "succeeded",
89
+ datasetId: result.version?.datasetId,
90
+ versionId: result.version?.versionId,
91
+ })
92
+ }
93
+
94
+ protected override async onExecutionError(
95
+ claimed: ClaimedQueueJob<PipelineRunRequestedQueueJob>,
96
+ _error: unknown
97
+ ): Promise<QueueWorkerFailureDecision> {
98
+ const { job } = claimed
99
+ await emitPipelineRunFinished(this.sixb.events, {
100
+ id: job.payload.runId ?? `${job.id}:attempt:${job.attempt}`,
101
+ pipelineId: job.payload.pipelineId,
102
+ status: "failed",
103
+ })
104
+
105
+ // Pipeline runs are not all-or-nothing in V1. A previous step may have committed an append
106
+ // output, so failed pipeline jobs must not be retried automatically.
107
+ return { kind: "fail" }
108
+ }
109
+
110
+ protected override async onAbortError(
111
+ claimed: ClaimedQueueJob<PipelineRunRequestedQueueJob>,
112
+ _error: unknown
113
+ ): Promise<QueueWorkerFailureDecision> {
114
+ const { job } = claimed
115
+ const pipelineJob: PipelineJob = {
116
+ id: job.payload.runId ?? `${job.id}:attempt:${job.attempt}`,
117
+ pipelineId: job.payload.pipelineId,
118
+ }
119
+
120
+ if (!(await hasCommittedStep(this.context, pipelineJob.id))) {
121
+ return { kind: "retry" }
122
+ }
123
+
124
+ // After a step commit, rerunning the whole pipeline could duplicate append outputs.
125
+ await emitPipelineRunFinished(this.sixb.events, {
126
+ id: pipelineJob.id,
127
+ pipelineId: pipelineJob.pipelineId,
128
+ status: "cancelled",
129
+ })
130
+
131
+ return { kind: "fail" }
132
+ }
133
+ }
134
+
135
+ function buildPipelineContext(
136
+ sixb: PipelineWorkerSixb,
137
+ pipelineRunsStorage: PipelineRunStorage
138
+ ): PipelineWorkerContext {
139
+ return {
140
+ id: sixb.id,
141
+ pipelineRunsStorage,
142
+ lakeStorage: sixb.lakeStorage,
143
+ logs: sixb.logs,
144
+ getPipelineById(pipelineId) {
145
+ return sixb.getPipelineById(pipelineId)
146
+ },
147
+ getDatasetById(datasetId) {
148
+ return sixb.getDatasetById(datasetId)
149
+ },
150
+ }
151
+ }
152
+
153
+ async function hasCommittedStep(
154
+ context: PipelineWorkerContext,
155
+ pipelineRunId: string
156
+ ): Promise<boolean> {
157
+ const result = await context.pipelineRunsStorage.listSteps({
158
+ projectId: context.id,
159
+ pipelineRunId,
160
+ statuses: ["succeeded"],
161
+ limit: 1,
162
+ })
163
+
164
+ return result.steps.some((step) => Boolean(step.output))
165
+ }