@sixb/pipeline-worker 0.1.1 → 0.1.3

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/run-step.ts CHANGED
@@ -1,12 +1,13 @@
1
1
  import type { DatasetDefinition, PipelineDefinition, PipelineStepDefinition } from "@sixb/core"
2
+ import { captureSixbFailure } from "@sixb/core/internal/errors"
2
3
  import type { DatasetVersion } from "@sixb/core/lake-storage"
3
- import type { PipelineStepRunRecord } from "@sixb/core/storage"
4
+ import { PIPELINE_RUN_FAILURE_CODES, type PipelineStepRunRecord } from "@sixb/core/storage"
4
5
  import {
6
+ createPipelineStepFailure,
5
7
  createStepBookkeepingError,
6
8
  requireRegisteredDataset,
7
9
  statusForFailure,
8
10
  throwIfAborted,
9
- toPipelineRunFailure,
10
11
  } from "./errors"
11
12
  import { executeRunStep } from "./execute-run-step"
12
13
  import { executeSqlStep } from "./execute-sql-step"
@@ -44,17 +45,37 @@ export async function runStep(input: {
44
45
  }
45
46
 
46
47
  throwIfAborted(signal)
47
- const resolvedInputs = await resolveStepInputs({ runtime, pipeline, step })
48
- const inputRefs = resolvedInputs.map((resolved) => resolved.ref)
49
- const outputDataset = requireRegisteredDataset({
50
- dataset: runtime.getDatasetById(step.output.id),
51
- pipelineId: pipeline.id,
52
- stepId: step.id,
53
- role: "output",
54
- datasetId: step.output.id,
55
- })
48
+ let resolvedInputs: readonly ResolvedStepInput[]
49
+ let outputDataset: DatasetDefinition
50
+ try {
51
+ resolvedInputs = await resolveStepInputs({
52
+ runtime,
53
+ pipeline,
54
+ step,
55
+ pipelineRunId: job.id,
56
+ })
57
+ outputDataset = requireRegisteredDataset({
58
+ dataset: runtime.datasets.getById(step.output.id),
59
+ pipelineId: pipeline.id,
60
+ pipelineRunId: job.id,
61
+ stepId: step.id,
62
+ role: "output",
63
+ datasetId: step.output.id,
64
+ })
56
65
 
57
- await runtime.lakeStorage.createDataset(outputDataset)
66
+ await runtime.lakeStorage.createDataset(outputDataset)
67
+ } catch (error) {
68
+ if (statusForFailure(signal, error) === "failed") {
69
+ throw createPipelineStepFailure({
70
+ pipelineId: pipeline.id,
71
+ pipelineRunId: job.id,
72
+ stepId: step.id,
73
+ cause: error,
74
+ })
75
+ }
76
+ throw error
77
+ }
78
+ const inputRefs = resolvedInputs.map((resolved) => resolved.ref)
58
79
 
59
80
  const stepRunId = `${job.id}:step:${stepIndex + 1}:${step.id}`
60
81
  const startedStepRun = await runtime.pipelineRunsStorage.startStep({
@@ -101,8 +122,9 @@ export async function runStep(input: {
101
122
  } catch (error) {
102
123
  throw createStepBookkeepingError({
103
124
  pipelineId: pipeline.id,
125
+ pipelineRunId: job.id,
104
126
  stepId: step.id,
105
- runId: stepRunId,
127
+ stepRunId,
106
128
  version: committedVersion,
107
129
  cause: error,
108
130
  })
@@ -116,19 +138,41 @@ export async function runStep(input: {
116
138
  }
117
139
  } catch (error) {
118
140
  if (!committedVersion) {
141
+ const status = statusForFailure(signal, error)
142
+ const failureError =
143
+ status === "failed"
144
+ ? createPipelineStepFailure({
145
+ pipelineId: pipeline.id,
146
+ pipelineRunId: job.id,
147
+ stepId: step.id,
148
+ stepRunId,
149
+ cause: error,
150
+ })
151
+ : error
119
152
  const failedStepRun = await runtime.pipelineRunsStorage
120
153
  .finishStep({
121
154
  projectId: runtime.id,
122
155
  id: stepRunId,
123
- status: statusForFailure(signal, error),
156
+ status,
124
157
  rowsWritten,
125
- error: toPipelineRunFailure(error),
158
+ error: captureSixbFailure(failureError, {
159
+ allowedCodes: PIPELINE_RUN_FAILURE_CODES,
160
+ defaultCode: status === "cancelled" ? "runtime.cancelled" : "internal.unexpected",
161
+ details: {
162
+ pipelineId: pipeline.id,
163
+ pipelineRunId: job.id,
164
+ stepId: step.id,
165
+ stepRunId,
166
+ },
167
+ }),
126
168
  })
127
169
  .catch(() => null)
128
170
 
129
171
  if (failedStepRun) {
130
172
  await input.onStepFinished?.(failedStepRun, lifecycleContext)
131
173
  }
174
+
175
+ throw failureError
132
176
  }
133
177
 
134
178
  throw error
@@ -5,12 +5,13 @@ import type {
5
5
  PipelineStepInput,
6
6
  PipelineStepRunContext,
7
7
  } from "@sixb/core"
8
+ import { createSixbError } from "@sixb/core/internal/errors"
8
9
  import type {
9
10
  DatasetVersion,
10
11
  DatasetVersionRef,
11
12
  ReadDatasetRowsInput,
12
13
  } from "@sixb/core/lake-storage"
13
- import { PipelineWorkerError, requireRegisteredDataset } from "./errors"
14
+ import { requireRegisteredDataset } from "./errors"
14
15
  import type { PipelineWorkerContext } from "./types"
15
16
 
16
17
  export interface ResolvedStepInput {
@@ -24,14 +25,16 @@ export async function resolveStepInputs(input: {
24
25
  readonly runtime: PipelineWorkerContext
25
26
  readonly pipeline: PipelineDefinition
26
27
  readonly step: PipelineStepDefinition
28
+ readonly pipelineRunId: string
27
29
  }): Promise<readonly ResolvedStepInput[]> {
28
- const { runtime, pipeline, step } = input
30
+ const { runtime, pipeline, step, pipelineRunId } = input
29
31
  const resolved: ResolvedStepInput[] = []
30
32
 
31
33
  for (const [name, declaredDataset] of Object.entries(step.inputs)) {
32
34
  const dataset = requireRegisteredDataset({
33
- dataset: runtime.getDatasetById(declaredDataset.id),
35
+ dataset: runtime.datasets.getById(declaredDataset.id),
34
36
  pipelineId: pipeline.id,
37
+ pipelineRunId,
35
38
  stepId: step.id,
36
39
  role: "input",
37
40
  name,
@@ -40,8 +43,17 @@ export async function resolveStepInputs(input: {
40
43
 
41
44
  const version = await runtime.lakeStorage.getLatestVersion(dataset.id)
42
45
  if (!version) {
43
- throw new PipelineWorkerError(
44
- `[SixbPipelineWorker] Pipeline '${pipeline.id}' step '${step.id}' input '${name}' dataset '${dataset.id}' has no committed version.`
46
+ throw createSixbError(
47
+ "internal.unexpected",
48
+ `[SixbPipelineWorker] Pipeline '${pipeline.id}' step '${step.id}' input '${name}' dataset '${dataset.id}' has no committed version.`,
49
+ {
50
+ details: {
51
+ pipelineId: pipeline.id,
52
+ pipelineRunId,
53
+ stepId: step.id,
54
+ datasetId: dataset.id,
55
+ },
56
+ }
45
57
  )
46
58
  }
47
59
 
package/src/types.ts CHANGED
@@ -1,40 +1,40 @@
1
1
  import type {
2
- DatasetDefinition,
3
2
  DomainEventLog,
4
3
  LakeStorage,
5
- PipelineDefinition,
6
4
  Queues,
5
+ SixbDefinitions,
6
+ SixbFailure,
7
7
  Storage,
8
8
  } from "@sixb/core"
9
- import type { LogsRuntime } from "@sixb/core/internal/logging"
9
+ import type { LoggingService } from "@sixb/core/internal/logging"
10
+ import type { PrimitiveExecutionHost } from "@sixb/core/internal/primitive-execution"
10
11
  import type { DatasetVersion } from "@sixb/core/lake-storage"
11
12
  import type {
13
+ PipelineRunFailureCode,
12
14
  PipelineRunRecord,
13
15
  PipelineRunStorage,
14
16
  PipelineStepRunRecord,
15
17
  } from "@sixb/core/storage"
16
18
 
17
- export type PipelineLogSession = ReturnType<LogsRuntime["startExecution"]>
19
+ export type PipelineLogSession = ReturnType<LoggingService["startExecution"]>
18
20
 
19
21
  export interface PipelineWorkerContext {
20
22
  readonly id: string
21
23
  readonly pipelineRunsStorage: PipelineRunStorage
22
24
  readonly lakeStorage: LakeStorage
23
- readonly logs?: LogsRuntime
24
- getDatasetById(datasetId: string): DatasetDefinition | null
25
- getPipelineById(pipelineId: string): PipelineDefinition | null
25
+ readonly logging?: LoggingService
26
+ readonly datasets: Pick<SixbDefinitions["datasets"], "getById">
27
+ readonly pipelines: Pick<SixbDefinitions["pipelines"], "getById">
26
28
  }
27
29
 
28
- export interface PipelineWorkerSixb {
30
+ export interface PipelineWorkerHost extends PrimitiveExecutionHost {
29
31
  readonly id: string
30
32
  readonly events?: DomainEventLog
31
- readonly logs?: LogsRuntime
33
+ readonly logging?: LoggingService
32
34
  readonly lakeStorage: LakeStorage
33
35
  readonly queues: Queues
34
36
  readonly storage: Storage
35
- listPipelines(): readonly PipelineDefinition[]
36
- getPipelineById(pipelineId: string): PipelineDefinition | null
37
- getDatasetById(datasetId: string): DatasetDefinition | null
37
+ readonly definitions: Pick<SixbDefinitions, "pipelines" | "datasets">
38
38
  }
39
39
 
40
40
  export interface PipelineJob {
@@ -44,9 +44,11 @@ export interface PipelineJob {
44
44
 
45
45
  export interface RunPipelineJobInput {
46
46
  readonly runtime: PipelineWorkerContext
47
- readonly job: PipelineJob
47
+ /** Durable run loaded before the execution scope is restored. */
48
+ readonly run: PipelineRunRecord
48
49
  readonly signal?: AbortSignal
49
50
  readonly onRunStarted?: PipelineRunStartedHandler
51
+ readonly onRunFinished?: PipelineRunFinishedHandler
50
52
  readonly onRunFailed?: PipelineRunFailedHandler
51
53
  readonly onStepStarted?: PipelineStepStartedHandler
52
54
  readonly onStepFinished?: PipelineStepFinishedHandler
@@ -57,7 +59,13 @@ export type PipelineStepCommittedHandler = (result: PipelineStepRunResult) => Pr
57
59
 
58
60
  export type PipelineRunStartedHandler = (run: PipelineRunRecord) => Promise<void> | void
59
61
 
60
- export type PipelineRunFailedHandler = (error: unknown, run: PipelineRunRecord) => void
62
+ export type PipelineRunFinishedHandler = (run: PipelineRunRecord) => Promise<void> | void
63
+
64
+ export type PipelineRunFailedHandler = (
65
+ error: unknown,
66
+ run: PipelineRunRecord,
67
+ failure: SixbFailure<PipelineRunFailureCode>
68
+ ) => void
61
69
 
62
70
  export interface PipelineStepLifecycleContext {
63
71
  readonly stepIndex: number
package/src/worker.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import { reportRunFailure } from "@sixb/core/internal/error-reporting"
2
+ import { bindDurablePrimitiveExecution } from "@sixb/core/internal/primitive-execution"
2
3
  import type { QueueWorkerFailureDecision } from "@sixb/core/internal/workers"
3
4
  import { QueueWorker } from "@sixb/core/internal/workers"
4
5
  import type { ClaimedQueueJob, PipelineRunRequestedQueueJob } from "@sixb/core/queues"
5
- import type { PipelineRunStorage } from "@sixb/core/storage"
6
+ import { PIPELINE_RUN_FAILURE_CODES, type PipelineRunStorage } from "@sixb/core/storage"
6
7
  import {
7
8
  emitDatasetVersionCommitted,
8
9
  emitPipelineRunFinished,
@@ -11,35 +12,40 @@ import {
11
12
  emitPipelineRunStepStarted,
12
13
  } from "./events"
13
14
  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> {
15
+ import type { PipelineJob, PipelineWorkerContext, PipelineWorkerHost } from "./types"
16
+
17
+ export interface PipelineWorkerOptions {
18
+ /** Maximum pipeline run jobs this worker claims and executes at once. Defaults to 1. */
19
+ readonly concurrency?: number
20
+ }
21
+
22
+ export class PipelineWorker extends QueueWorker<
23
+ PipelineRunRequestedQueueJob,
24
+ typeof PIPELINE_RUN_FAILURE_CODES
25
+ > {
22
26
  private readonly context: PipelineWorkerContext
23
- private readonly sixb: PipelineWorkerSixb
27
+ private readonly host: PipelineWorkerHost
24
28
 
25
- constructor(sixb: PipelineWorkerSixb) {
26
- if (sixb.listPipelines().length === 0) {
29
+ constructor(host: PipelineWorkerHost, options: PipelineWorkerOptions = {}) {
30
+ if (host.definitions.pipelines.list().length === 0) {
27
31
  throw new Error("[SixbPipelineWorker] No pipeline definitions are registered.")
28
32
  }
29
33
 
30
- const pipelineRunsStorage = sixb.storage.pipelineRuns
34
+ const pipelineRunsStorage = host.storage.pipelineRuns
31
35
  if (!pipelineRunsStorage) {
32
36
  throw new Error("[SixbPipelineWorker] Pipeline workers require storage.pipelineRuns support.")
33
37
  }
34
38
 
35
39
  super({
36
- projectId: sixb.id,
37
- queue: sixb.queues.pipelines,
38
- workerId: `pipeline-worker-${sixb.id}`,
40
+ projectId: host.id,
41
+ queue: host.queues.pipelines,
42
+ failureCodes: PIPELINE_RUN_FAILURE_CODES,
43
+ workerId: `pipeline-worker-${host.id}`,
44
+ claimLimit: options.concurrency,
39
45
  })
40
46
 
41
- this.context = buildPipelineContext(sixb, pipelineRunsStorage)
42
- this.sixb = sixb
47
+ this.context = buildPipelineContext(host, pipelineRunsStorage)
48
+ this.host = host
43
49
  }
44
50
 
45
51
  protected async execute(
@@ -47,106 +53,132 @@ export class PipelineWorker extends QueueWorker<PipelineRunRequestedQueueJob> {
47
53
  signal: AbortSignal
48
54
  ): Promise<void> {
49
55
  const { job } = claimed
56
+ const pipelineRuns = this.host.storage.pipelineRuns
57
+ if (!pipelineRuns) {
58
+ throw new Error("[SixbPipelineWorker] Pipeline workers require storage.pipelineRuns support.")
59
+ }
60
+ const run = await pipelineRuns.getById({ projectId: this.host.id, id: job.payload.runId })
61
+ if (!run) {
62
+ throw new Error(`[SixbPipelineWorker] Pipeline run '${job.payload.runId}' was not found.`)
63
+ }
64
+
65
+ const durableExecution = await this.host.storage.executions.getById({
66
+ projectId: this.host.id,
67
+ id: run.executionId,
68
+ })
69
+ if (!durableExecution) {
70
+ throw new Error(
71
+ `[SixbPipelineWorker] Pipeline run '${run.id}' references missing execution '${run.executionId}'.`
72
+ )
73
+ }
74
+
50
75
  const pipelineJob: PipelineJob = {
51
- id: job.payload.runId ?? `${job.id}:attempt:${job.attempt}`,
52
- pipelineId: job.payload.pipelineId,
76
+ id: run.id,
77
+ pipelineId: run.pipelineId,
53
78
  }
79
+ // Pipeline steps currently use host-owned lake and run storage directly. Binding still happens
80
+ // here so the durable execution and its trusted primitive authority are validated before work.
81
+ bindDurablePrimitiveExecution(this.host, {
82
+ execution: durableExecution,
83
+ primitive: { kind: "pipeline", id: run.pipelineId, runId: run.id },
84
+ })
54
85
 
55
- let result: PipelineRunResult
56
86
  try {
57
- result = await runPipelineJob({
87
+ await runPipelineJob({
58
88
  runtime: this.context,
59
- job: pipelineJob,
89
+ run,
60
90
  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,
91
+ onRunStarted: (run) =>
92
+ emitPipelineRunStarted(this.host.events, run, durableExecution.correlationId),
93
+ onRunFinished: (run) =>
94
+ emitPipelineRunFinished(this.host.events, run, durableExecution.correlationId),
95
+ onRunFailed: (error, run, failure) => {
96
+ reportRunFailure(this.host, error, {
97
+ projectId: this.host.id,
66
98
  attempt: job.attempt,
99
+ runKind: "pipeline",
67
100
  run: {
68
- kind: "pipeline",
69
101
  runId: run.id,
70
102
  pipelineId: run.pipelineId,
71
103
  },
104
+ failure,
72
105
  })
73
106
  },
74
107
  onStepStarted: (step, context) =>
75
- emitPipelineRunStepStarted(this.sixb.events, step, context),
108
+ emitPipelineRunStepStarted(
109
+ this.host.events,
110
+ step,
111
+ context,
112
+ durableExecution.correlationId
113
+ ),
76
114
  onStepFinished: (step, context) =>
77
- emitPipelineRunStepFinished(this.sixb.events, step, context),
78
- onStepCommitted: (step) => emitDatasetVersionCommitted(this.sixb.events, pipelineJob, step),
115
+ emitPipelineRunStepFinished(
116
+ this.host.events,
117
+ step,
118
+ context,
119
+ durableExecution.correlationId
120
+ ),
121
+ onStepCommitted: (step) =>
122
+ emitDatasetVersionCommitted(
123
+ this.host.events,
124
+ pipelineJob,
125
+ step,
126
+ durableExecution.correlationId
127
+ ),
79
128
  })
80
129
  } catch (error) {
81
130
  if (error instanceof PipelineRunAlreadyStartedError) return
82
131
  throw error
83
132
  }
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
133
  }
93
134
 
94
135
  protected override async onExecutionError(
95
136
  claimed: ClaimedQueueJob<PipelineRunRequestedQueueJob>,
96
137
  _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
-
138
+ ): Promise<QueueWorkerFailureDecision<(typeof PIPELINE_RUN_FAILURE_CODES)[number]>> {
105
139
  // Pipeline runs are not all-or-nothing in V1. A previous step may have committed an append
106
140
  // output, so failed pipeline jobs must not be retried automatically.
107
- return { kind: "fail" }
141
+ const run = await this.host.storage.pipelineRuns?.getById({
142
+ projectId: this.host.id,
143
+ id: claimed.job.payload.runId,
144
+ })
145
+ return run?.status === "failed" && run.error
146
+ ? { kind: "fail", failure: run.error }
147
+ : { kind: "fail" }
108
148
  }
109
149
 
110
150
  protected override async onAbortError(
111
151
  claimed: ClaimedQueueJob<PipelineRunRequestedQueueJob>,
112
152
  _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
- }
153
+ ): Promise<QueueWorkerFailureDecision<(typeof PIPELINE_RUN_FAILURE_CODES)[number]>> {
154
+ const run = await this.host.storage.pipelineRuns?.getById({
155
+ projectId: this.host.id,
156
+ id: claimed.job.payload.runId,
157
+ })
158
+ if (!run) return { kind: "fail" }
119
159
 
120
- if (!(await hasCommittedStep(this.context, pipelineJob.id))) {
160
+ if (!(await hasCommittedStep(this.context, run.id))) {
121
161
  return { kind: "retry" }
122
162
  }
123
163
 
124
164
  // 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" }
165
+ return run.status === "cancelled" && run.error
166
+ ? { kind: "fail", failure: run.error }
167
+ : { kind: "fail" }
132
168
  }
133
169
  }
134
170
 
135
171
  function buildPipelineContext(
136
- sixb: PipelineWorkerSixb,
172
+ host: PipelineWorkerHost,
137
173
  pipelineRunsStorage: PipelineRunStorage
138
174
  ): PipelineWorkerContext {
139
175
  return {
140
- id: sixb.id,
176
+ id: host.id,
141
177
  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
- },
178
+ lakeStorage: host.lakeStorage,
179
+ logging: host.logging,
180
+ pipelines: host.definitions.pipelines,
181
+ datasets: host.definitions.datasets,
150
182
  }
151
183
  }
152
184