@sixb/pipeline-worker 0.1.0 → 0.1.2

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,34 @@ 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 class PipelineWorker extends QueueWorker<
18
+ PipelineRunRequestedQueueJob,
19
+ typeof PIPELINE_RUN_FAILURE_CODES
20
+ > {
22
21
  private readonly context: PipelineWorkerContext
23
- private readonly sixb: PipelineWorkerSixb
22
+ private readonly host: PipelineWorkerHost
24
23
 
25
- constructor(sixb: PipelineWorkerSixb) {
26
- if (sixb.listPipelines().length === 0) {
24
+ constructor(host: PipelineWorkerHost) {
25
+ if (host.definitions.pipelines.list().length === 0) {
27
26
  throw new Error("[SixbPipelineWorker] No pipeline definitions are registered.")
28
27
  }
29
28
 
30
- const pipelineRunsStorage = sixb.storage.pipelineRuns
29
+ const pipelineRunsStorage = host.storage.pipelineRuns
31
30
  if (!pipelineRunsStorage) {
32
31
  throw new Error("[SixbPipelineWorker] Pipeline workers require storage.pipelineRuns support.")
33
32
  }
34
33
 
35
34
  super({
36
- projectId: sixb.id,
37
- queue: sixb.queues.pipelines,
38
- workerId: `pipeline-worker-${sixb.id}`,
35
+ projectId: host.id,
36
+ queue: host.queues.pipelines,
37
+ failureCodes: PIPELINE_RUN_FAILURE_CODES,
38
+ workerId: `pipeline-worker-${host.id}`,
39
39
  })
40
40
 
41
- this.context = buildPipelineContext(sixb, pipelineRunsStorage)
42
- this.sixb = sixb
41
+ this.context = buildPipelineContext(host, pipelineRunsStorage)
42
+ this.host = host
43
43
  }
44
44
 
45
45
  protected async execute(
@@ -47,106 +47,132 @@ export class PipelineWorker extends QueueWorker<PipelineRunRequestedQueueJob> {
47
47
  signal: AbortSignal
48
48
  ): Promise<void> {
49
49
  const { job } = claimed
50
+ const pipelineRuns = this.host.storage.pipelineRuns
51
+ if (!pipelineRuns) {
52
+ throw new Error("[SixbPipelineWorker] Pipeline workers require storage.pipelineRuns support.")
53
+ }
54
+ const run = await pipelineRuns.getById({ projectId: this.host.id, id: job.payload.runId })
55
+ if (!run) {
56
+ throw new Error(`[SixbPipelineWorker] Pipeline run '${job.payload.runId}' was not found.`)
57
+ }
58
+
59
+ const durableExecution = await this.host.storage.executions.getById({
60
+ projectId: this.host.id,
61
+ id: run.executionId,
62
+ })
63
+ if (!durableExecution) {
64
+ throw new Error(
65
+ `[SixbPipelineWorker] Pipeline run '${run.id}' references missing execution '${run.executionId}'.`
66
+ )
67
+ }
68
+
50
69
  const pipelineJob: PipelineJob = {
51
- id: job.payload.runId ?? `${job.id}:attempt:${job.attempt}`,
52
- pipelineId: job.payload.pipelineId,
70
+ id: run.id,
71
+ pipelineId: run.pipelineId,
53
72
  }
73
+ // Pipeline steps currently use host-owned lake and run storage directly. Binding still happens
74
+ // here so the durable execution and its trusted primitive authority are validated before work.
75
+ bindDurablePrimitiveExecution(this.host, {
76
+ execution: durableExecution,
77
+ primitive: { kind: "pipeline", id: run.pipelineId, runId: run.id },
78
+ })
54
79
 
55
- let result: PipelineRunResult
56
80
  try {
57
- result = await runPipelineJob({
81
+ await runPipelineJob({
58
82
  runtime: this.context,
59
- job: pipelineJob,
83
+ run,
60
84
  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,
85
+ onRunStarted: (run) =>
86
+ emitPipelineRunStarted(this.host.events, run, durableExecution.correlationId),
87
+ onRunFinished: (run) =>
88
+ emitPipelineRunFinished(this.host.events, run, durableExecution.correlationId),
89
+ onRunFailed: (error, run, failure) => {
90
+ reportRunFailure(this.host, error, {
91
+ projectId: this.host.id,
66
92
  attempt: job.attempt,
93
+ runKind: "pipeline",
67
94
  run: {
68
- kind: "pipeline",
69
95
  runId: run.id,
70
96
  pipelineId: run.pipelineId,
71
97
  },
98
+ failure,
72
99
  })
73
100
  },
74
101
  onStepStarted: (step, context) =>
75
- emitPipelineRunStepStarted(this.sixb.events, step, context),
102
+ emitPipelineRunStepStarted(
103
+ this.host.events,
104
+ step,
105
+ context,
106
+ durableExecution.correlationId
107
+ ),
76
108
  onStepFinished: (step, context) =>
77
- emitPipelineRunStepFinished(this.sixb.events, step, context),
78
- onStepCommitted: (step) => emitDatasetVersionCommitted(this.sixb.events, pipelineJob, step),
109
+ emitPipelineRunStepFinished(
110
+ this.host.events,
111
+ step,
112
+ context,
113
+ durableExecution.correlationId
114
+ ),
115
+ onStepCommitted: (step) =>
116
+ emitDatasetVersionCommitted(
117
+ this.host.events,
118
+ pipelineJob,
119
+ step,
120
+ durableExecution.correlationId
121
+ ),
79
122
  })
80
123
  } catch (error) {
81
124
  if (error instanceof PipelineRunAlreadyStartedError) return
82
125
  throw error
83
126
  }
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
127
  }
93
128
 
94
129
  protected override async onExecutionError(
95
130
  claimed: ClaimedQueueJob<PipelineRunRequestedQueueJob>,
96
131
  _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
-
132
+ ): Promise<QueueWorkerFailureDecision<(typeof PIPELINE_RUN_FAILURE_CODES)[number]>> {
105
133
  // Pipeline runs are not all-or-nothing in V1. A previous step may have committed an append
106
134
  // output, so failed pipeline jobs must not be retried automatically.
107
- return { kind: "fail" }
135
+ const run = await this.host.storage.pipelineRuns?.getById({
136
+ projectId: this.host.id,
137
+ id: claimed.job.payload.runId,
138
+ })
139
+ return run?.status === "failed" && run.error
140
+ ? { kind: "fail", failure: run.error }
141
+ : { kind: "fail" }
108
142
  }
109
143
 
110
144
  protected override async onAbortError(
111
145
  claimed: ClaimedQueueJob<PipelineRunRequestedQueueJob>,
112
146
  _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
- }
147
+ ): Promise<QueueWorkerFailureDecision<(typeof PIPELINE_RUN_FAILURE_CODES)[number]>> {
148
+ const run = await this.host.storage.pipelineRuns?.getById({
149
+ projectId: this.host.id,
150
+ id: claimed.job.payload.runId,
151
+ })
152
+ if (!run) return { kind: "fail" }
119
153
 
120
- if (!(await hasCommittedStep(this.context, pipelineJob.id))) {
154
+ if (!(await hasCommittedStep(this.context, run.id))) {
121
155
  return { kind: "retry" }
122
156
  }
123
157
 
124
158
  // 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" }
159
+ return run.status === "cancelled" && run.error
160
+ ? { kind: "fail", failure: run.error }
161
+ : { kind: "fail" }
132
162
  }
133
163
  }
134
164
 
135
165
  function buildPipelineContext(
136
- sixb: PipelineWorkerSixb,
166
+ host: PipelineWorkerHost,
137
167
  pipelineRunsStorage: PipelineRunStorage
138
168
  ): PipelineWorkerContext {
139
169
  return {
140
- id: sixb.id,
170
+ id: host.id,
141
171
  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
- },
172
+ lakeStorage: host.lakeStorage,
173
+ logging: host.logging,
174
+ pipelines: host.definitions.pipelines,
175
+ datasets: host.definitions.datasets,
150
176
  }
151
177
  }
152
178