@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
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DatasetDefinition,
|
|
3
|
+
DatasetRow,
|
|
4
|
+
PipelineDefinition,
|
|
5
|
+
PipelineStepDefinition,
|
|
6
|
+
PipelineStepRunContext,
|
|
7
|
+
} from "@sixb/core"
|
|
8
|
+
import type { DatasetVersion } from "@sixb/core/lake-storage"
|
|
9
|
+
import { PipelineWorkerError, throwIfAborted } from "./errors"
|
|
10
|
+
import { createStepInputs, type ResolvedStepInput } from "./step-inputs"
|
|
11
|
+
import type { PipelineJob, PipelineLogSession, PipelineWorkerContext } from "./types"
|
|
12
|
+
|
|
13
|
+
export async function executeRunStep(input: {
|
|
14
|
+
readonly runtime: PipelineWorkerContext
|
|
15
|
+
readonly pipeline: PipelineDefinition
|
|
16
|
+
readonly step: PipelineStepDefinition
|
|
17
|
+
readonly job: PipelineJob
|
|
18
|
+
readonly signal: AbortSignal
|
|
19
|
+
readonly logSession: PipelineLogSession
|
|
20
|
+
readonly outputDataset: DatasetDefinition
|
|
21
|
+
readonly resolvedInputs: readonly ResolvedStepInput[]
|
|
22
|
+
}): Promise<{
|
|
23
|
+
readonly version: DatasetVersion
|
|
24
|
+
readonly versionCreated: boolean
|
|
25
|
+
readonly rowsWritten: number
|
|
26
|
+
}> {
|
|
27
|
+
const { runtime, pipeline, step, job, signal, outputDataset, resolvedInputs } = input
|
|
28
|
+
|
|
29
|
+
if (step.executor.kind !== "run") {
|
|
30
|
+
throw new PipelineWorkerError(
|
|
31
|
+
`[SixbPipelineWorker] Pipeline '${pipeline.id}' step '${step.id}' uses SQL execution, which is not supported by the run step executor.`
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const logger = input.logSession.withContext({ stepId: step.id })
|
|
36
|
+
let rowsWritten = 0
|
|
37
|
+
const write = await runtime.lakeStorage.beginWrite({
|
|
38
|
+
dataset: outputDataset,
|
|
39
|
+
mode: step.mode,
|
|
40
|
+
inputs: resolvedInputs.map((resolved) => resolved.ref),
|
|
41
|
+
producer: {
|
|
42
|
+
kind: "pipeline",
|
|
43
|
+
id: pipeline.id,
|
|
44
|
+
runId: job.id,
|
|
45
|
+
stepId: step.id,
|
|
46
|
+
},
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const context: PipelineStepRunContext = {
|
|
51
|
+
projectId: runtime.id,
|
|
52
|
+
pipelineId: pipeline.id,
|
|
53
|
+
stepId: step.id,
|
|
54
|
+
runId: job.id,
|
|
55
|
+
signal,
|
|
56
|
+
logger,
|
|
57
|
+
inputs: createStepInputs(runtime.lakeStorage, resolvedInputs),
|
|
58
|
+
output: {
|
|
59
|
+
async writeRows(rows) {
|
|
60
|
+
await write.writeRows(
|
|
61
|
+
countRowsWritten(rows, signal, () => {
|
|
62
|
+
rowsWritten += 1
|
|
63
|
+
})
|
|
64
|
+
)
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
await step.executor.handler(context)
|
|
70
|
+
throwIfAborted(signal)
|
|
71
|
+
|
|
72
|
+
const commit = await write.commit({
|
|
73
|
+
commitMessage: `pipeline ${pipeline.id} step ${step.id} run ${job.id}`,
|
|
74
|
+
})
|
|
75
|
+
const { outcome, ...version } = commit
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
version,
|
|
79
|
+
versionCreated: outcome === "created",
|
|
80
|
+
rowsWritten,
|
|
81
|
+
}
|
|
82
|
+
} catch (error) {
|
|
83
|
+
await write.abort().catch(() => {})
|
|
84
|
+
throw error
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function* countRowsWritten(
|
|
89
|
+
rows: Iterable<DatasetRow> | AsyncIterable<DatasetRow>,
|
|
90
|
+
signal: AbortSignal,
|
|
91
|
+
onWritten: () => void
|
|
92
|
+
): AsyncIterable<DatasetRow> {
|
|
93
|
+
for await (const row of rows) {
|
|
94
|
+
throwIfAborted(signal)
|
|
95
|
+
yield row
|
|
96
|
+
onWritten()
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DatasetDefinition,
|
|
3
|
+
LakeStorage,
|
|
4
|
+
LakeStorageWithSql,
|
|
5
|
+
PipelineDefinition,
|
|
6
|
+
PipelineStepDefinition,
|
|
7
|
+
} from "@sixb/core"
|
|
8
|
+
import type {
|
|
9
|
+
DatasetVersion,
|
|
10
|
+
DatasetWriteMode,
|
|
11
|
+
LakeSqlTransformCapabilities,
|
|
12
|
+
} from "@sixb/core/lake-storage"
|
|
13
|
+
import { PipelineWorkerError, throwIfAborted } from "./errors"
|
|
14
|
+
import type { ResolvedStepInput } from "./step-inputs"
|
|
15
|
+
import type { PipelineJob, PipelineWorkerContext } from "./types"
|
|
16
|
+
|
|
17
|
+
function hasSqlExecutor(lakeStorage: LakeStorage): lakeStorage is LakeStorageWithSql {
|
|
18
|
+
const sql = "sql" in lakeStorage ? lakeStorage.sql : null
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
typeof sql === "object" && sql !== null && "execute" in sql && typeof sql.execute === "function"
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function assertSqlModeSupported(input: {
|
|
26
|
+
readonly capabilities: LakeSqlTransformCapabilities
|
|
27
|
+
readonly dialect: string
|
|
28
|
+
readonly mode: DatasetWriteMode
|
|
29
|
+
readonly pipelineId: string
|
|
30
|
+
readonly stepId: string
|
|
31
|
+
}): void {
|
|
32
|
+
const supported =
|
|
33
|
+
input.mode === "append"
|
|
34
|
+
? input.capabilities.supportsAppend
|
|
35
|
+
: input.capabilities.supportsSnapshot
|
|
36
|
+
if (supported) return
|
|
37
|
+
|
|
38
|
+
throw new PipelineWorkerError(
|
|
39
|
+
`[SixbPipelineWorker] Pipeline '${input.pipelineId}' step '${input.stepId}' writes in ` +
|
|
40
|
+
`'${input.mode}' mode, which the ${input.dialect} SQL executor does not support.`
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function executeSqlStep(input: {
|
|
45
|
+
readonly runtime: PipelineWorkerContext
|
|
46
|
+
readonly pipeline: PipelineDefinition
|
|
47
|
+
readonly step: PipelineStepDefinition
|
|
48
|
+
readonly job: PipelineJob
|
|
49
|
+
readonly signal: AbortSignal
|
|
50
|
+
readonly outputDataset: DatasetDefinition
|
|
51
|
+
readonly resolvedInputs: readonly ResolvedStepInput[]
|
|
52
|
+
}): Promise<{
|
|
53
|
+
readonly version: DatasetVersion
|
|
54
|
+
readonly versionCreated: boolean
|
|
55
|
+
readonly rowsWritten?: number
|
|
56
|
+
}> {
|
|
57
|
+
const { runtime, pipeline, step, job, signal, outputDataset, resolvedInputs } = input
|
|
58
|
+
|
|
59
|
+
if (step.executor.kind !== "sql") {
|
|
60
|
+
throw new PipelineWorkerError(
|
|
61
|
+
`[SixbPipelineWorker] Pipeline '${pipeline.id}' step '${step.id}' uses run execution, which is not supported by the SQL step executor.`
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!hasSqlExecutor(runtime.lakeStorage)) {
|
|
66
|
+
throw new PipelineWorkerError(
|
|
67
|
+
`[SixbPipelineWorker] Pipeline '${pipeline.id}' step '${step.id}' requires SQL transform support, but lake storage does not provide lakeStorage.sql.execute(...).`
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// `LakeSqlExecutor.capabilities` is a required field on the contract that nothing read,
|
|
72
|
+
// so a provider declaring `supportsAppend: false` was asked to be honest and then
|
|
73
|
+
// ignored — the unsupported mode failed somewhere inside the provider instead, in
|
|
74
|
+
// whatever words that provider happened to use.
|
|
75
|
+
assertSqlModeSupported({
|
|
76
|
+
capabilities: runtime.lakeStorage.sql.capabilities,
|
|
77
|
+
dialect: runtime.lakeStorage.sql.dialect,
|
|
78
|
+
mode: step.mode,
|
|
79
|
+
pipelineId: pipeline.id,
|
|
80
|
+
stepId: step.id,
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
throwIfAborted(signal)
|
|
84
|
+
|
|
85
|
+
const commit = await runtime.lakeStorage.sql.execute({
|
|
86
|
+
sources: Object.fromEntries(
|
|
87
|
+
resolvedInputs.map((input) => [
|
|
88
|
+
input.name,
|
|
89
|
+
{
|
|
90
|
+
dataset: input.dataset,
|
|
91
|
+
versionId: input.version.versionId,
|
|
92
|
+
},
|
|
93
|
+
])
|
|
94
|
+
),
|
|
95
|
+
sql: step.executor.sql,
|
|
96
|
+
target: outputDataset,
|
|
97
|
+
mode: step.mode,
|
|
98
|
+
producer: {
|
|
99
|
+
kind: "pipeline",
|
|
100
|
+
id: pipeline.id,
|
|
101
|
+
runId: job.id,
|
|
102
|
+
stepId: step.id,
|
|
103
|
+
},
|
|
104
|
+
commitMessage: `pipeline ${pipeline.id} step ${step.id} run ${job.id}`,
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
throwIfAborted(signal)
|
|
108
|
+
const { outcome, ...version } = commit
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
version,
|
|
112
|
+
versionCreated: outcome === "created",
|
|
113
|
+
rowsWritten: version.rowCount,
|
|
114
|
+
}
|
|
115
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { resolveLogsRuntime } from "@sixb/core/internal/logging"
|
|
2
|
+
import type { DatasetVersion } from "@sixb/core/lake-storage"
|
|
3
|
+
import type { PipelineRunRecord } from "@sixb/core/storage"
|
|
4
|
+
import {
|
|
5
|
+
createPipelineBookkeepingError,
|
|
6
|
+
requireFinishedAt,
|
|
7
|
+
requirePipeline,
|
|
8
|
+
statusForFailure,
|
|
9
|
+
throwIfAborted,
|
|
10
|
+
toPipelineRunFailure,
|
|
11
|
+
} from "./errors"
|
|
12
|
+
import { runStep } from "./run-step"
|
|
13
|
+
import type { PipelineRunResult, PipelineStepRunResult, RunPipelineJobInput } from "./types"
|
|
14
|
+
|
|
15
|
+
export class PipelineRunAlreadyStartedError extends Error {
|
|
16
|
+
override readonly name = "PipelineRunAlreadyStartedError"
|
|
17
|
+
|
|
18
|
+
constructor(readonly run: PipelineRunRecord) {
|
|
19
|
+
super(`[SixbPipelineWorker] Pipeline run '${run.id}' has already started.`)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function runPipelineJob(input: RunPipelineJobInput): Promise<PipelineRunResult> {
|
|
24
|
+
const { runtime, job } = input
|
|
25
|
+
const signal = input.signal ?? new AbortController().signal
|
|
26
|
+
const pipeline = requirePipeline(runtime.getPipelineById(job.pipelineId), job)
|
|
27
|
+
const logSession = resolveLogsRuntime(runtime.id, runtime.logs).startExecution({
|
|
28
|
+
kind: "pipeline",
|
|
29
|
+
id: job.id,
|
|
30
|
+
})
|
|
31
|
+
const steps: PipelineStepRunResult[] = []
|
|
32
|
+
let startedRun: PipelineRunRecord | undefined
|
|
33
|
+
let finalVersion: DatasetVersion | undefined
|
|
34
|
+
let finished = false
|
|
35
|
+
|
|
36
|
+
throwIfAborted(signal)
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
try {
|
|
40
|
+
startedRun = await runtime.pipelineRunsStorage.start({
|
|
41
|
+
projectId: runtime.id,
|
|
42
|
+
id: job.id,
|
|
43
|
+
pipelineId: pipeline.id,
|
|
44
|
+
})
|
|
45
|
+
} catch (error) {
|
|
46
|
+
const existing = await runtime.pipelineRunsStorage.getById({
|
|
47
|
+
projectId: runtime.id,
|
|
48
|
+
id: job.id,
|
|
49
|
+
})
|
|
50
|
+
if (existing?.pipelineId === pipeline.id) {
|
|
51
|
+
throw new PipelineRunAlreadyStartedError(existing)
|
|
52
|
+
}
|
|
53
|
+
throw error
|
|
54
|
+
}
|
|
55
|
+
await input.onRunStarted?.(startedRun)
|
|
56
|
+
|
|
57
|
+
for (const [stepIndex, node] of pipeline.graph.nodes.entries()) {
|
|
58
|
+
const stepResult = await runStep({
|
|
59
|
+
runtime,
|
|
60
|
+
pipeline,
|
|
61
|
+
step: node.step,
|
|
62
|
+
stepIndex,
|
|
63
|
+
job,
|
|
64
|
+
signal,
|
|
65
|
+
logSession,
|
|
66
|
+
onStepStarted: input.onStepStarted,
|
|
67
|
+
onStepFinished: input.onStepFinished,
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
steps.push(stepResult)
|
|
71
|
+
finalVersion = stepResult.version
|
|
72
|
+
// This is the durability boundary for per-step output events.
|
|
73
|
+
await input.onStepCommitted?.(stepResult)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let run: PipelineRunRecord
|
|
77
|
+
try {
|
|
78
|
+
run = await runtime.pipelineRunsStorage.finish({
|
|
79
|
+
projectId: runtime.id,
|
|
80
|
+
id: job.id,
|
|
81
|
+
status: "succeeded",
|
|
82
|
+
output: finalVersion
|
|
83
|
+
? {
|
|
84
|
+
datasetId: finalVersion.datasetId,
|
|
85
|
+
versionId: finalVersion.versionId,
|
|
86
|
+
}
|
|
87
|
+
: undefined,
|
|
88
|
+
})
|
|
89
|
+
} catch (error) {
|
|
90
|
+
if (finalVersion) {
|
|
91
|
+
throw createPipelineBookkeepingError({
|
|
92
|
+
pipelineId: pipeline.id,
|
|
93
|
+
runId: job.id,
|
|
94
|
+
version: finalVersion,
|
|
95
|
+
cause: error,
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
throw error
|
|
99
|
+
}
|
|
100
|
+
finished = true
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
run: {
|
|
104
|
+
...run,
|
|
105
|
+
finishedAt: requireFinishedAt(job.id, run.finishedAt),
|
|
106
|
+
},
|
|
107
|
+
steps,
|
|
108
|
+
version: finalVersion,
|
|
109
|
+
}
|
|
110
|
+
} catch (error) {
|
|
111
|
+
if (startedRun && !finished) {
|
|
112
|
+
const status = statusForFailure(signal, error)
|
|
113
|
+
try {
|
|
114
|
+
const run = await runtime.pipelineRunsStorage.finish({
|
|
115
|
+
projectId: runtime.id,
|
|
116
|
+
id: job.id,
|
|
117
|
+
status,
|
|
118
|
+
error: toPipelineRunFailure(error),
|
|
119
|
+
})
|
|
120
|
+
if (status === "failed" && run.status === "failed") input.onRunFailed?.(error, run)
|
|
121
|
+
} catch {
|
|
122
|
+
// The run did not transition to the requested terminal status.
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
throw error
|
|
127
|
+
} finally {
|
|
128
|
+
await logSession.flush()
|
|
129
|
+
}
|
|
130
|
+
}
|
package/src/run-step.ts
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import type { DatasetDefinition, PipelineDefinition, PipelineStepDefinition } from "@sixb/core"
|
|
2
|
+
import type { DatasetVersion } from "@sixb/core/lake-storage"
|
|
3
|
+
import type { PipelineStepRunRecord } from "@sixb/core/storage"
|
|
4
|
+
import {
|
|
5
|
+
createStepBookkeepingError,
|
|
6
|
+
requireRegisteredDataset,
|
|
7
|
+
statusForFailure,
|
|
8
|
+
throwIfAborted,
|
|
9
|
+
toPipelineRunFailure,
|
|
10
|
+
} from "./errors"
|
|
11
|
+
import { executeRunStep } from "./execute-run-step"
|
|
12
|
+
import { executeSqlStep } from "./execute-sql-step"
|
|
13
|
+
import { type ResolvedStepInput, resolveStepInputs } from "./step-inputs"
|
|
14
|
+
import type {
|
|
15
|
+
PipelineJob,
|
|
16
|
+
PipelineLogSession,
|
|
17
|
+
PipelineStepFinishedHandler,
|
|
18
|
+
PipelineStepRunResult,
|
|
19
|
+
PipelineStepStartedHandler,
|
|
20
|
+
PipelineWorkerContext,
|
|
21
|
+
} from "./types"
|
|
22
|
+
|
|
23
|
+
type StepExecutionResult = {
|
|
24
|
+
readonly version: DatasetVersion
|
|
25
|
+
readonly versionCreated: boolean
|
|
26
|
+
readonly rowsWritten?: number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function runStep(input: {
|
|
30
|
+
readonly runtime: PipelineWorkerContext
|
|
31
|
+
readonly pipeline: PipelineDefinition
|
|
32
|
+
readonly step: PipelineStepDefinition
|
|
33
|
+
readonly stepIndex: number
|
|
34
|
+
readonly job: PipelineJob
|
|
35
|
+
readonly signal: AbortSignal
|
|
36
|
+
readonly logSession: PipelineLogSession
|
|
37
|
+
readonly onStepStarted?: PipelineStepStartedHandler
|
|
38
|
+
readonly onStepFinished?: PipelineStepFinishedHandler
|
|
39
|
+
}): Promise<PipelineStepRunResult> {
|
|
40
|
+
const { runtime, pipeline, step, stepIndex, job, signal } = input
|
|
41
|
+
const lifecycleContext = {
|
|
42
|
+
stepIndex,
|
|
43
|
+
totalSteps: pipeline.graph.nodes.length,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
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
|
+
})
|
|
56
|
+
|
|
57
|
+
await runtime.lakeStorage.createDataset(outputDataset)
|
|
58
|
+
|
|
59
|
+
const stepRunId = `${job.id}:step:${stepIndex + 1}:${step.id}`
|
|
60
|
+
const startedStepRun = await runtime.pipelineRunsStorage.startStep({
|
|
61
|
+
projectId: runtime.id,
|
|
62
|
+
id: stepRunId,
|
|
63
|
+
pipelineRunId: job.id,
|
|
64
|
+
pipelineId: pipeline.id,
|
|
65
|
+
stepId: step.id,
|
|
66
|
+
datasetId: outputDataset.id,
|
|
67
|
+
mode: step.mode,
|
|
68
|
+
inputs: inputRefs,
|
|
69
|
+
})
|
|
70
|
+
await input.onStepStarted?.(startedStepRun, lifecycleContext)
|
|
71
|
+
|
|
72
|
+
let committedVersion: DatasetVersion | undefined
|
|
73
|
+
let rowsWritten: number | undefined
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const execution = await executeStep({
|
|
77
|
+
runtime,
|
|
78
|
+
pipeline,
|
|
79
|
+
step,
|
|
80
|
+
job,
|
|
81
|
+
signal,
|
|
82
|
+
logSession: input.logSession,
|
|
83
|
+
outputDataset,
|
|
84
|
+
resolvedInputs,
|
|
85
|
+
})
|
|
86
|
+
committedVersion = execution.version
|
|
87
|
+
rowsWritten = execution.rowsWritten
|
|
88
|
+
|
|
89
|
+
let stepRun: PipelineStepRunRecord
|
|
90
|
+
try {
|
|
91
|
+
stepRun = await runtime.pipelineRunsStorage.finishStep({
|
|
92
|
+
projectId: runtime.id,
|
|
93
|
+
id: stepRunId,
|
|
94
|
+
status: "succeeded",
|
|
95
|
+
output: {
|
|
96
|
+
datasetId: committedVersion.datasetId,
|
|
97
|
+
versionId: committedVersion.versionId,
|
|
98
|
+
},
|
|
99
|
+
rowsWritten,
|
|
100
|
+
})
|
|
101
|
+
} catch (error) {
|
|
102
|
+
throw createStepBookkeepingError({
|
|
103
|
+
pipelineId: pipeline.id,
|
|
104
|
+
stepId: step.id,
|
|
105
|
+
runId: stepRunId,
|
|
106
|
+
version: committedVersion,
|
|
107
|
+
cause: error,
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
await input.onStepFinished?.(stepRun, lifecycleContext)
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
run: stepRun,
|
|
114
|
+
version: committedVersion,
|
|
115
|
+
versionCreated: execution.versionCreated,
|
|
116
|
+
}
|
|
117
|
+
} catch (error) {
|
|
118
|
+
if (!committedVersion) {
|
|
119
|
+
const failedStepRun = await runtime.pipelineRunsStorage
|
|
120
|
+
.finishStep({
|
|
121
|
+
projectId: runtime.id,
|
|
122
|
+
id: stepRunId,
|
|
123
|
+
status: statusForFailure(signal, error),
|
|
124
|
+
rowsWritten,
|
|
125
|
+
error: toPipelineRunFailure(error),
|
|
126
|
+
})
|
|
127
|
+
.catch(() => null)
|
|
128
|
+
|
|
129
|
+
if (failedStepRun) {
|
|
130
|
+
await input.onStepFinished?.(failedStepRun, lifecycleContext)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
throw error
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function executeStep(input: {
|
|
139
|
+
readonly runtime: PipelineWorkerContext
|
|
140
|
+
readonly pipeline: PipelineDefinition
|
|
141
|
+
readonly step: PipelineStepDefinition
|
|
142
|
+
readonly job: PipelineJob
|
|
143
|
+
readonly signal: AbortSignal
|
|
144
|
+
readonly logSession: PipelineLogSession
|
|
145
|
+
readonly outputDataset: DatasetDefinition
|
|
146
|
+
readonly resolvedInputs: readonly ResolvedStepInput[]
|
|
147
|
+
}): Promise<StepExecutionResult> {
|
|
148
|
+
switch (input.step.executor.kind) {
|
|
149
|
+
case "run":
|
|
150
|
+
return executeRunStep(input)
|
|
151
|
+
case "sql":
|
|
152
|
+
return executeSqlStep(input)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DatasetDefinition,
|
|
3
|
+
PipelineDefinition,
|
|
4
|
+
PipelineStepDefinition,
|
|
5
|
+
PipelineStepInput,
|
|
6
|
+
PipelineStepRunContext,
|
|
7
|
+
} from "@sixb/core"
|
|
8
|
+
import type {
|
|
9
|
+
DatasetVersion,
|
|
10
|
+
DatasetVersionRef,
|
|
11
|
+
ReadDatasetRowsInput,
|
|
12
|
+
} from "@sixb/core/lake-storage"
|
|
13
|
+
import { PipelineWorkerError, requireRegisteredDataset } from "./errors"
|
|
14
|
+
import type { PipelineWorkerContext } from "./types"
|
|
15
|
+
|
|
16
|
+
export interface ResolvedStepInput {
|
|
17
|
+
readonly name: string
|
|
18
|
+
readonly dataset: DatasetDefinition
|
|
19
|
+
readonly version: DatasetVersion
|
|
20
|
+
readonly ref: DatasetVersionRef
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function resolveStepInputs(input: {
|
|
24
|
+
readonly runtime: PipelineWorkerContext
|
|
25
|
+
readonly pipeline: PipelineDefinition
|
|
26
|
+
readonly step: PipelineStepDefinition
|
|
27
|
+
}): Promise<readonly ResolvedStepInput[]> {
|
|
28
|
+
const { runtime, pipeline, step } = input
|
|
29
|
+
const resolved: ResolvedStepInput[] = []
|
|
30
|
+
|
|
31
|
+
for (const [name, declaredDataset] of Object.entries(step.inputs)) {
|
|
32
|
+
const dataset = requireRegisteredDataset({
|
|
33
|
+
dataset: runtime.getDatasetById(declaredDataset.id),
|
|
34
|
+
pipelineId: pipeline.id,
|
|
35
|
+
stepId: step.id,
|
|
36
|
+
role: "input",
|
|
37
|
+
name,
|
|
38
|
+
datasetId: declaredDataset.id,
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
const version = await runtime.lakeStorage.getLatestVersion(dataset.id)
|
|
42
|
+
if (!version) {
|
|
43
|
+
throw new PipelineWorkerError(
|
|
44
|
+
`[SixbPipelineWorker] Pipeline '${pipeline.id}' step '${step.id}' input '${name}' dataset '${dataset.id}' has no committed version.`
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
resolved.push({
|
|
49
|
+
name,
|
|
50
|
+
dataset,
|
|
51
|
+
version,
|
|
52
|
+
ref: {
|
|
53
|
+
datasetId: version.datasetId,
|
|
54
|
+
versionId: version.versionId,
|
|
55
|
+
},
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return resolved
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function createStepInputs(
|
|
63
|
+
lakeStorage: PipelineWorkerContext["lakeStorage"],
|
|
64
|
+
resolved: readonly ResolvedStepInput[]
|
|
65
|
+
): PipelineStepRunContext["inputs"] {
|
|
66
|
+
const inputs: Record<string, PipelineStepInput> = {}
|
|
67
|
+
|
|
68
|
+
for (const input of resolved) {
|
|
69
|
+
inputs[input.name] = {
|
|
70
|
+
dataset: input.dataset,
|
|
71
|
+
version: input.version,
|
|
72
|
+
readRows(readInput: Omit<ReadDatasetRowsInput, "datasetId" | "versionId"> = {}) {
|
|
73
|
+
// Step readers stay pinned even if newer versions commit while the handler runs.
|
|
74
|
+
return lakeStorage.readRows({
|
|
75
|
+
...readInput,
|
|
76
|
+
datasetId: input.dataset.id,
|
|
77
|
+
versionId: input.version.versionId,
|
|
78
|
+
})
|
|
79
|
+
},
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return inputs
|
|
84
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DatasetDefinition,
|
|
3
|
+
DomainEventLog,
|
|
4
|
+
LakeStorage,
|
|
5
|
+
PipelineDefinition,
|
|
6
|
+
Queues,
|
|
7
|
+
Storage,
|
|
8
|
+
} from "@sixb/core"
|
|
9
|
+
import type { LogsRuntime } from "@sixb/core/internal/logging"
|
|
10
|
+
import type { DatasetVersion } from "@sixb/core/lake-storage"
|
|
11
|
+
import type {
|
|
12
|
+
PipelineRunRecord,
|
|
13
|
+
PipelineRunStorage,
|
|
14
|
+
PipelineStepRunRecord,
|
|
15
|
+
} from "@sixb/core/storage"
|
|
16
|
+
|
|
17
|
+
export type PipelineLogSession = ReturnType<LogsRuntime["startExecution"]>
|
|
18
|
+
|
|
19
|
+
export interface PipelineWorkerContext {
|
|
20
|
+
readonly id: string
|
|
21
|
+
readonly pipelineRunsStorage: PipelineRunStorage
|
|
22
|
+
readonly lakeStorage: LakeStorage
|
|
23
|
+
readonly logs?: LogsRuntime
|
|
24
|
+
getDatasetById(datasetId: string): DatasetDefinition | null
|
|
25
|
+
getPipelineById(pipelineId: string): PipelineDefinition | null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface PipelineWorkerSixb {
|
|
29
|
+
readonly id: string
|
|
30
|
+
readonly events?: DomainEventLog
|
|
31
|
+
readonly logs?: LogsRuntime
|
|
32
|
+
readonly lakeStorage: LakeStorage
|
|
33
|
+
readonly queues: Queues
|
|
34
|
+
readonly storage: Storage
|
|
35
|
+
listPipelines(): readonly PipelineDefinition[]
|
|
36
|
+
getPipelineById(pipelineId: string): PipelineDefinition | null
|
|
37
|
+
getDatasetById(datasetId: string): DatasetDefinition | null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface PipelineJob {
|
|
41
|
+
readonly id: string
|
|
42
|
+
readonly pipelineId: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface RunPipelineJobInput {
|
|
46
|
+
readonly runtime: PipelineWorkerContext
|
|
47
|
+
readonly job: PipelineJob
|
|
48
|
+
readonly signal?: AbortSignal
|
|
49
|
+
readonly onRunStarted?: PipelineRunStartedHandler
|
|
50
|
+
readonly onRunFailed?: PipelineRunFailedHandler
|
|
51
|
+
readonly onStepStarted?: PipelineStepStartedHandler
|
|
52
|
+
readonly onStepFinished?: PipelineStepFinishedHandler
|
|
53
|
+
readonly onStepCommitted?: PipelineStepCommittedHandler
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type PipelineStepCommittedHandler = (result: PipelineStepRunResult) => Promise<void> | void
|
|
57
|
+
|
|
58
|
+
export type PipelineRunStartedHandler = (run: PipelineRunRecord) => Promise<void> | void
|
|
59
|
+
|
|
60
|
+
export type PipelineRunFailedHandler = (error: unknown, run: PipelineRunRecord) => void
|
|
61
|
+
|
|
62
|
+
export interface PipelineStepLifecycleContext {
|
|
63
|
+
readonly stepIndex: number
|
|
64
|
+
readonly totalSteps: number
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export type PipelineStepStartedHandler = (
|
|
68
|
+
step: PipelineStepRunRecord,
|
|
69
|
+
context: PipelineStepLifecycleContext
|
|
70
|
+
) => Promise<void> | void
|
|
71
|
+
|
|
72
|
+
export type PipelineStepFinishedHandler = (
|
|
73
|
+
step: PipelineStepRunRecord,
|
|
74
|
+
context: PipelineStepLifecycleContext
|
|
75
|
+
) => Promise<void> | void
|
|
76
|
+
|
|
77
|
+
export interface PipelineStepRunResult {
|
|
78
|
+
readonly run: PipelineStepRunRecord
|
|
79
|
+
readonly version: DatasetVersion
|
|
80
|
+
/** True only when this step created the returned dataset version. */
|
|
81
|
+
readonly versionCreated: boolean
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface PipelineRunResult {
|
|
85
|
+
readonly run: PipelineRunRecord
|
|
86
|
+
readonly steps: readonly PipelineStepRunResult[]
|
|
87
|
+
readonly version?: DatasetVersion
|
|
88
|
+
}
|