@sixb/pg 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/README.md +7 -6
- package/dist/agents/agent-storage.d.ts +7 -4
- package/dist/agents/agent-storage.d.ts.map +1 -1
- package/dist/agents/checkpoints.d.ts +12 -0
- package/dist/agents/checkpoints.d.ts.map +1 -0
- package/dist/agents/index.d.ts +1 -0
- package/dist/agents/index.d.ts.map +1 -1
- package/dist/agents/messages.d.ts +2 -1
- package/dist/agents/messages.d.ts.map +1 -1
- package/dist/agents/rows.d.ts +22 -12
- package/dist/agents/rows.d.ts.map +1 -1
- package/dist/agents/runs.d.ts +3 -2
- package/dist/agents/runs.d.ts.map +1 -1
- package/dist/auth-storage/service-accounts.d.ts.map +1 -1
- package/dist/auth-storage/shared.d.ts +0 -4
- package/dist/auth-storage/shared.d.ts.map +1 -1
- package/dist/connector-connection-storage.d.ts +9 -0
- package/dist/connector-connection-storage.d.ts.map +1 -0
- package/dist/index.d.ts +9 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5010 -1837
- package/dist/index.js.map +31 -26
- package/dist/latest-run-query.d.ts.map +1 -1
- package/dist/migrations.d.ts.map +1 -1
- package/dist/ontology-storage/materialization-session.d.ts.map +1 -1
- package/dist/ontology-storage/materialization-state.d.ts +0 -2
- package/dist/ontology-storage/materialization-state.d.ts.map +1 -1
- package/dist/ontology-storage/materialization-writer.d.ts.map +1 -1
- package/dist/ontology-storage/materializations.d.ts.map +1 -1
- package/dist/ontology-storage/outbox.d.ts.map +1 -1
- package/dist/ontology-storage/shared.d.ts +3 -6
- package/dist/ontology-storage/shared.d.ts.map +1 -1
- package/dist/pg-action-run-storage.d.ts +3 -2
- package/dist/pg-action-run-storage.d.ts.map +1 -1
- package/dist/pg-ai-cost-storage.d.ts +12 -0
- package/dist/pg-ai-cost-storage.d.ts.map +1 -0
- package/dist/pg-ai-usage-storage.d.ts +16 -0
- package/dist/pg-ai-usage-storage.d.ts.map +1 -0
- package/dist/pg-execution-storage.d.ts +14 -0
- package/dist/pg-execution-storage.d.ts.map +1 -0
- package/dist/pg-pipeline-run-storage.d.ts +5 -2
- package/dist/pg-pipeline-run-storage.d.ts.map +1 -1
- package/dist/pg-projection-run-storage.d.ts +5 -2
- package/dist/pg-projection-run-storage.d.ts.map +1 -1
- package/dist/pg-sync-run-storage.d.ts +5 -2
- package/dist/pg-sync-run-storage.d.ts.map +1 -1
- package/dist/pg-webhook-run-storage.d.ts +5 -2
- package/dist/pg-webhook-run-storage.d.ts.map +1 -1
- package/dist/pg-workflow-run-storage.d.ts +5 -3
- package/dist/pg-workflow-run-storage.d.ts.map +1 -1
- package/dist/run-list-query.d.ts +1 -1
- package/dist/run-list-query.d.ts.map +1 -1
- package/dist/testing.d.ts +6 -0
- package/dist/testing.d.ts.map +1 -0
- package/package.json +3 -3
- package/src/agents/agent-storage.ts +9 -5
- package/src/agents/checkpoints.ts +175 -0
- package/src/agents/index.ts +1 -0
- package/src/agents/messages.ts +71 -13
- package/src/agents/rows.ts +57 -43
- package/src/agents/runs.ts +29 -15
- package/src/auth-storage/service-accounts.ts +14 -15
- package/src/auth-storage/shared.ts +0 -8
- package/src/connector-connection-storage.ts +760 -0
- package/src/index.ts +38 -15
- package/src/latest-run-query.ts +7 -1
- package/src/migrations/004-executions.sql +140 -0
- package/src/migrations/005-workflow-executions.sql +10 -0
- package/src/migrations/006-narrow-ontology-source-root-index.sql +4 -0
- package/src/migrations/007-split-overrides.sql +153 -0
- package/src/migrations/008-action-executions.sql +7 -0
- package/src/migrations/009-agent-executions.sql +21 -0
- package/src/migrations/010-ai-usage-accounting-foundation.sql +76 -0
- package/src/migrations/011-sync-failure-record.sql +23 -0
- package/src/migrations/012-pipeline-failure-record.sql +47 -0
- package/src/migrations/013-workflow-failure-record.sql +45 -0
- package/src/migrations/014-agent-failure-record.sql +56 -0
- package/src/migrations/015-projection-failure-record.sql +20 -0
- package/src/migrations/016-webhook-run-failure-record.sql +19 -0
- package/src/migrations/017-action-failure-record.sql +65 -0
- package/src/migrations/018-ontology-outbox-failure-record.sql +29 -0
- package/src/migrations/019-webhook-delivery-failure-record.sql +27 -0
- package/src/migrations/020-drop-run-usage-projections.sql +8 -0
- package/src/migrations/021-sync-pipeline-executions.sql +50 -0
- package/src/migrations/022-projection-executions.sql +115 -0
- package/src/migrations/023-webhook-executions.sql +30 -0
- package/src/migrations/024-ontology-commit-executions.sql +12 -0
- package/src/migrations/025-connector-connections.sql +231 -0
- package/src/migrations/026-ai-cost-accounting.sql +53 -0
- package/src/migrations/027-agent-context-checkpoints.sql +34 -0
- package/src/migrations/028-object-override-edit-times.sql +3 -0
- package/src/migrations.ts +82 -0
- package/src/ontology-storage/materialization-session.ts +25 -28
- package/src/ontology-storage/materialization-state.ts +161 -43
- package/src/ontology-storage/materialization-writer.ts +149 -76
- package/src/ontology-storage/materializations.ts +3 -1
- package/src/ontology-storage/outbox.ts +9 -2
- package/src/ontology-storage/shared.ts +10 -20
- package/src/pg-action-run-storage.ts +43 -67
- package/src/pg-ai-cost-storage.ts +707 -0
- package/src/pg-ai-usage-storage.ts +290 -0
- package/src/pg-execution-storage.ts +147 -0
- package/src/pg-pipeline-run-storage.ts +107 -41
- package/src/pg-projection-run-storage.ts +159 -91
- package/src/pg-sync-run-storage.ts +100 -28
- package/src/pg-webhook-run-storage.ts +89 -57
- package/src/pg-workflow-run-storage.ts +97 -130
- package/src/run-list-query.ts +9 -4
- package/src/testing.ts +18 -0
- package/dist/pg-webhook-delivery-storage.d.ts +0 -17
- package/dist/pg-webhook-delivery-storage.d.ts.map +0 -1
- package/src/pg-webhook-delivery-storage.ts +0 -180
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
import type { JsonValue } from "@sixb/core"
|
|
2
|
+
import { parseSixbFailure, serializeSixbFailure } from "@sixb/core/internal/errors"
|
|
3
|
+
import { assertPipelineRunExecution } from "@sixb/core/internal/pipeline-run-storage-provider"
|
|
1
4
|
import type { DatasetVersionRef } from "@sixb/core/lake-storage"
|
|
2
5
|
import type {
|
|
6
|
+
ExecutionStorage,
|
|
3
7
|
FinishPipelineRunInput,
|
|
4
8
|
FinishPipelineStepRunInput,
|
|
5
9
|
ListLatestPipelineRunsInput,
|
|
@@ -8,14 +12,18 @@ import type {
|
|
|
8
12
|
ListPipelineRunsResult,
|
|
9
13
|
ListPipelineStepRunsInput,
|
|
10
14
|
ListPipelineStepRunsResult,
|
|
11
|
-
PipelineRunFailure,
|
|
12
15
|
PipelineRunRecord,
|
|
13
16
|
PipelineRunStorage,
|
|
14
17
|
PipelineStepRunRecord,
|
|
18
|
+
QueuePipelineRunInput,
|
|
15
19
|
StartPipelineRunInput,
|
|
16
20
|
StartPipelineStepRunInput,
|
|
17
21
|
} from "@sixb/core/storage"
|
|
18
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
canRequeuePipelineRunAfterEnqueueFailure,
|
|
24
|
+
PIPELINE_RUN_FAILURE_CODES,
|
|
25
|
+
PipelineRunError,
|
|
26
|
+
} from "@sixb/core/storage"
|
|
19
27
|
import { queryLatestRunsByOwnerId } from "./latest-run-query"
|
|
20
28
|
import type { SqlParameter } from "./pg-client"
|
|
21
29
|
import { appendRunListFilters, hasEmptyStatuses, queryRunList } from "./run-list-query"
|
|
@@ -23,23 +31,37 @@ import { isUniqueViolation } from "./storage-errors"
|
|
|
23
31
|
import { type PgStoreClient, runPgTransaction } from "./transactions"
|
|
24
32
|
|
|
25
33
|
export class PgPipelineRunStorage implements PipelineRunStorage {
|
|
26
|
-
constructor(
|
|
27
|
-
|
|
28
|
-
|
|
34
|
+
constructor(
|
|
35
|
+
private readonly sql: PgStoreClient,
|
|
36
|
+
private readonly executions: ExecutionStorage
|
|
37
|
+
) {}
|
|
38
|
+
|
|
39
|
+
async queue(input: QueuePipelineRunInput): Promise<PipelineRunRecord> {
|
|
40
|
+
await assertPipelineRunExecution({
|
|
41
|
+
executions: this.executions,
|
|
42
|
+
projectId: input.projectId,
|
|
43
|
+
executionId: input.executionId,
|
|
44
|
+
runId: input.id,
|
|
45
|
+
pipelineId: input.pipelineId,
|
|
46
|
+
})
|
|
29
47
|
try {
|
|
30
48
|
const [row] = await this.sql<PipelineRunDatabaseRow[]>`
|
|
31
49
|
INSERT INTO pipeline_runs (
|
|
32
50
|
project_id,
|
|
33
51
|
id,
|
|
52
|
+
execution_id,
|
|
34
53
|
pipeline_id,
|
|
35
54
|
status,
|
|
55
|
+
queued_at,
|
|
36
56
|
started_at
|
|
37
57
|
) VALUES (
|
|
38
58
|
${input.projectId},
|
|
39
59
|
${input.id},
|
|
60
|
+
${input.executionId},
|
|
40
61
|
${input.pipelineId},
|
|
41
|
-
${"
|
|
42
|
-
${input.
|
|
62
|
+
${"queued"},
|
|
63
|
+
${input.queuedAt ?? new Date()},
|
|
64
|
+
${null}
|
|
43
65
|
)
|
|
44
66
|
RETURNING *
|
|
45
67
|
`
|
|
@@ -47,13 +69,62 @@ export class PgPipelineRunStorage implements PipelineRunStorage {
|
|
|
47
69
|
return rowToPipelineRunRecord(row)
|
|
48
70
|
} catch (error) {
|
|
49
71
|
if (isUniqueViolation(error)) {
|
|
72
|
+
return this.requeueAfterEnqueueFailure(input)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
throw error
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private async requeueAfterEnqueueFailure(
|
|
80
|
+
input: QueuePipelineRunInput
|
|
81
|
+
): Promise<PipelineRunRecord> {
|
|
82
|
+
return runPgTransaction(this.sql, async (tx) => {
|
|
83
|
+
const [existing] = await tx<PipelineRunDatabaseRow[]>`
|
|
84
|
+
SELECT * FROM pipeline_runs
|
|
85
|
+
WHERE project_id = ${input.projectId} AND id = ${input.id}
|
|
86
|
+
FOR UPDATE
|
|
87
|
+
`
|
|
88
|
+
if (
|
|
89
|
+
!existing ||
|
|
90
|
+
!canRequeuePipelineRunAfterEnqueueFailure(rowToPipelineRunRecord(existing), input)
|
|
91
|
+
) {
|
|
50
92
|
throw new PipelineRunError(
|
|
51
93
|
`[SixbPg] Pipeline run '${input.id}' already exists for project '${input.projectId}'.`
|
|
52
94
|
)
|
|
53
95
|
}
|
|
54
96
|
|
|
55
|
-
|
|
97
|
+
const [updated] = await tx<PipelineRunDatabaseRow[]>`
|
|
98
|
+
UPDATE pipeline_runs
|
|
99
|
+
SET status = ${"queued"}, queued_at = ${input.queuedAt ?? new Date()},
|
|
100
|
+
started_at = ${null}, finished_at = ${null}, output_dataset_id = ${null},
|
|
101
|
+
output_version_id = ${null}, error = ${null}
|
|
102
|
+
WHERE project_id = ${input.projectId} AND id = ${input.id}
|
|
103
|
+
RETURNING *
|
|
104
|
+
`
|
|
105
|
+
return rowToPipelineRunRecord(updated)
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async start(input: StartPipelineRunInput): Promise<PipelineRunRecord> {
|
|
110
|
+
const [updated] = await this.sql<PipelineRunDatabaseRow[]>`
|
|
111
|
+
UPDATE pipeline_runs
|
|
112
|
+
SET status = ${"running"}, started_at = ${input.startedAt ?? new Date()},
|
|
113
|
+
error = ${null}
|
|
114
|
+
WHERE project_id = ${input.projectId} AND id = ${input.id} AND status = ${"queued"}
|
|
115
|
+
RETURNING *
|
|
116
|
+
`
|
|
117
|
+
if (updated) return rowToPipelineRunRecord(updated)
|
|
118
|
+
|
|
119
|
+
const existing = await this.getById({ projectId: input.projectId, id: input.id })
|
|
120
|
+
if (!existing) {
|
|
121
|
+
throw new PipelineRunError(
|
|
122
|
+
`[SixbPg] Pipeline run '${input.id}' not found for project '${input.projectId}'.`
|
|
123
|
+
)
|
|
56
124
|
}
|
|
125
|
+
throw new PipelineRunError(
|
|
126
|
+
`[SixbPg] Pipeline run '${input.id}' cannot start from status '${existing.status}'.`
|
|
127
|
+
)
|
|
57
128
|
}
|
|
58
129
|
|
|
59
130
|
async finish(input: FinishPipelineRunInput): Promise<PipelineRunRecord> {
|
|
@@ -69,9 +140,14 @@ export class PgPipelineRunStorage implements PipelineRunStorage {
|
|
|
69
140
|
)
|
|
70
141
|
}
|
|
71
142
|
|
|
72
|
-
|
|
143
|
+
const enqueueFailure =
|
|
144
|
+
input.status === "failed" && input.error.code === "queue.enqueue_failed"
|
|
145
|
+
if (
|
|
146
|
+
(enqueueFailure && existing.status !== "queued") ||
|
|
147
|
+
(!enqueueFailure && existing.status !== "running")
|
|
148
|
+
) {
|
|
73
149
|
throw new PipelineRunError(
|
|
74
|
-
`[SixbPg] Pipeline run '${input.id}'
|
|
150
|
+
`[SixbPg] Pipeline run '${input.id}' cannot finish from status '${existing.status}'.`
|
|
75
151
|
)
|
|
76
152
|
}
|
|
77
153
|
|
|
@@ -84,8 +160,7 @@ export class PgPipelineRunStorage implements PipelineRunStorage {
|
|
|
84
160
|
finished_at = ${input.finishedAt ?? new Date()},
|
|
85
161
|
output_dataset_id = ${input.output?.datasetId ?? null},
|
|
86
162
|
output_version_id = ${input.output?.versionId ?? null},
|
|
87
|
-
|
|
88
|
-
error_message = ${null}
|
|
163
|
+
error = ${null}
|
|
89
164
|
WHERE project_id = ${input.projectId} AND id = ${input.id}
|
|
90
165
|
RETURNING *
|
|
91
166
|
`
|
|
@@ -96,8 +171,7 @@ export class PgPipelineRunStorage implements PipelineRunStorage {
|
|
|
96
171
|
finished_at = ${input.finishedAt ?? new Date()},
|
|
97
172
|
output_dataset_id = ${null},
|
|
98
173
|
output_version_id = ${null},
|
|
99
|
-
|
|
100
|
-
error_message = ${input.error?.message ?? null}
|
|
174
|
+
error = ${input.error === undefined ? null : serializeSixbFailure(input.error, PIPELINE_RUN_FAILURE_CODES)}::text::jsonb
|
|
101
175
|
WHERE project_id = ${input.projectId} AND id = ${input.id}
|
|
102
176
|
RETURNING *
|
|
103
177
|
`
|
|
@@ -208,8 +282,7 @@ export class PgPipelineRunStorage implements PipelineRunStorage {
|
|
|
208
282
|
finished_at = ${input.finishedAt ?? new Date()},
|
|
209
283
|
output_version_id = ${input.output.versionId},
|
|
210
284
|
rows_written = ${input.rowsWritten ?? existing.rows_written ?? null},
|
|
211
|
-
|
|
212
|
-
error_message = ${null}
|
|
285
|
+
error = ${null}
|
|
213
286
|
WHERE project_id = ${input.projectId} AND id = ${input.id}
|
|
214
287
|
RETURNING *
|
|
215
288
|
`
|
|
@@ -220,8 +293,7 @@ export class PgPipelineRunStorage implements PipelineRunStorage {
|
|
|
220
293
|
finished_at = ${input.finishedAt ?? new Date()},
|
|
221
294
|
output_version_id = ${null},
|
|
222
295
|
rows_written = ${input.rowsWritten ?? existing.rows_written ?? null},
|
|
223
|
-
|
|
224
|
-
error_message = ${input.error?.message ?? null}
|
|
296
|
+
error = ${input.error === undefined ? null : serializeSixbFailure(input.error, PIPELINE_RUN_FAILURE_CODES)}::text::jsonb
|
|
225
297
|
WHERE project_id = ${input.projectId} AND id = ${input.id}
|
|
226
298
|
RETURNING *
|
|
227
299
|
`
|
|
@@ -263,7 +335,13 @@ export class PgPipelineRunStorage implements PipelineRunStorage {
|
|
|
263
335
|
params.push(...input.pipelineIds)
|
|
264
336
|
}
|
|
265
337
|
|
|
266
|
-
index = appendRunListFilters(
|
|
338
|
+
index = appendRunListFilters(
|
|
339
|
+
whereClauses,
|
|
340
|
+
params,
|
|
341
|
+
index,
|
|
342
|
+
input,
|
|
343
|
+
"COALESCE(started_at, queued_at)"
|
|
344
|
+
)
|
|
267
345
|
|
|
268
346
|
const { rows, total, hasMore } = await queryRunList<PipelineRunDatabaseRow>({
|
|
269
347
|
sql: this.sql,
|
|
@@ -356,27 +434,15 @@ export class PgPipelineRunStorage implements PipelineRunStorage {
|
|
|
356
434
|
}
|
|
357
435
|
}
|
|
358
436
|
|
|
359
|
-
function toFailure(row: {
|
|
360
|
-
readonly error_name: string | null
|
|
361
|
-
readonly error_message: string | null
|
|
362
|
-
}): PipelineRunFailure | undefined {
|
|
363
|
-
if (!row.error_message) {
|
|
364
|
-
return undefined
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
return {
|
|
368
|
-
name: row.error_name ?? undefined,
|
|
369
|
-
message: row.error_message,
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
|
|
373
437
|
function rowToPipelineRunRecord(row: PipelineRunDatabaseRow): PipelineRunRecord {
|
|
374
438
|
return {
|
|
375
439
|
id: row.id,
|
|
376
440
|
projectId: row.project_id,
|
|
441
|
+
executionId: row.execution_id,
|
|
377
442
|
pipelineId: row.pipeline_id,
|
|
378
443
|
status: row.status,
|
|
379
|
-
|
|
444
|
+
queuedAt: new Date(row.queued_at),
|
|
445
|
+
startedAt: row.started_at ? new Date(row.started_at) : undefined,
|
|
380
446
|
finishedAt: row.finished_at ? new Date(row.finished_at) : undefined,
|
|
381
447
|
output:
|
|
382
448
|
row.output_dataset_id && row.output_version_id
|
|
@@ -385,7 +451,7 @@ function rowToPipelineRunRecord(row: PipelineRunDatabaseRow): PipelineRunRecord
|
|
|
385
451
|
versionId: row.output_version_id,
|
|
386
452
|
}
|
|
387
453
|
: undefined,
|
|
388
|
-
error:
|
|
454
|
+
error: row.error === null ? undefined : parseSixbFailure(row.error, PIPELINE_RUN_FAILURE_CODES),
|
|
389
455
|
}
|
|
390
456
|
}
|
|
391
457
|
|
|
@@ -409,7 +475,7 @@ function rowToPipelineStepRunRecord(row: PipelineStepRunDatabaseRow): PipelineSt
|
|
|
409
475
|
}
|
|
410
476
|
: undefined,
|
|
411
477
|
rowsWritten: row.rows_written != null ? Number(row.rows_written) : undefined,
|
|
412
|
-
error:
|
|
478
|
+
error: row.error === null ? undefined : parseSixbFailure(row.error, PIPELINE_RUN_FAILURE_CODES),
|
|
413
479
|
}
|
|
414
480
|
}
|
|
415
481
|
|
|
@@ -428,14 +494,15 @@ function assertOptionalNonNegativeInteger(value: number | undefined, fieldName:
|
|
|
428
494
|
interface PipelineRunDatabaseRow {
|
|
429
495
|
project_id: string
|
|
430
496
|
id: string
|
|
497
|
+
execution_id: string
|
|
431
498
|
pipeline_id: string
|
|
432
499
|
status: PipelineRunRecord["status"]
|
|
433
|
-
|
|
500
|
+
queued_at: Date | string
|
|
501
|
+
started_at: Date | string | null
|
|
434
502
|
finished_at: Date | string | null
|
|
435
503
|
output_dataset_id: string | null
|
|
436
504
|
output_version_id: string | null
|
|
437
|
-
|
|
438
|
-
error_message: string | null
|
|
505
|
+
error: JsonValue | null
|
|
439
506
|
}
|
|
440
507
|
|
|
441
508
|
interface PipelineStepRunDatabaseRow {
|
|
@@ -452,6 +519,5 @@ interface PipelineStepRunDatabaseRow {
|
|
|
452
519
|
inputs: readonly DatasetVersionRef[] | string
|
|
453
520
|
output_version_id: string | null
|
|
454
521
|
rows_written: number | string | null
|
|
455
|
-
|
|
456
|
-
error_message: string | null
|
|
522
|
+
error: JsonValue | null
|
|
457
523
|
}
|
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto"
|
|
2
|
+
import type { JsonValue } from "@sixb/core"
|
|
3
|
+
import { serializeSixbFailure } from "@sixb/core/internal/errors"
|
|
2
4
|
import {
|
|
3
5
|
advanceProjectionTelemetry,
|
|
4
6
|
assertGenericProgressDoesNotAdvanceTelemetry,
|
|
5
7
|
assertProjectionMissingTarget,
|
|
6
|
-
|
|
8
|
+
assertProjectionRunAttempt,
|
|
9
|
+
assertProjectionRunDurableExecution,
|
|
7
10
|
assertProjectionRunListWindow,
|
|
8
11
|
assertProjectionRunNonEmpty,
|
|
12
|
+
assertProjectionRunQueueInput,
|
|
9
13
|
assertProjectionRunRunning,
|
|
10
14
|
assertProjectionRunStartInput,
|
|
15
|
+
canRequeueProjectionRunAfterEnqueueFailure,
|
|
11
16
|
createProjectionRunClaim,
|
|
17
|
+
createProjectionRunRecord,
|
|
18
|
+
failProjectionRunEnqueue,
|
|
12
19
|
immutableDatasetVersionConflict,
|
|
13
20
|
mergeProjectionRunProgress,
|
|
14
21
|
type PersistedProjectionRunRecord,
|
|
@@ -16,7 +23,6 @@ import {
|
|
|
16
23
|
planProjectionRunReclaim,
|
|
17
24
|
projectionRunNotFound,
|
|
18
25
|
publicProjectionRunRecord,
|
|
19
|
-
requireProjectionRunExecutionToken,
|
|
20
26
|
requireTelemetryProjectionRun,
|
|
21
27
|
restoreProjectionRun,
|
|
22
28
|
type StoredProjectionRunRecord,
|
|
@@ -24,6 +30,8 @@ import {
|
|
|
24
30
|
} from "@sixb/core/internal/projection-run-storage-provider"
|
|
25
31
|
import type {
|
|
26
32
|
AdvanceProjectionTelemetryCheckpointInput,
|
|
33
|
+
ExecutionStorage,
|
|
34
|
+
FailProjectionRunEnqueueInput,
|
|
27
35
|
FinishProjectionRunInput,
|
|
28
36
|
ListLatestProjectionRunsInput,
|
|
29
37
|
ListLatestProjectionRunsResult,
|
|
@@ -35,21 +43,34 @@ import type {
|
|
|
35
43
|
ProjectionRunRecord,
|
|
36
44
|
ProjectionRunStatus,
|
|
37
45
|
ProjectionRunStorage,
|
|
46
|
+
QueueProjectionRunInput,
|
|
38
47
|
RecordProjectionMissingTargetInput,
|
|
39
48
|
StartOrReclaimProjectionRunInput,
|
|
40
49
|
TelemetryProjectionRunRecord,
|
|
41
50
|
UpdateProjectionRunInput,
|
|
42
51
|
} from "@sixb/core/storage"
|
|
43
|
-
import { ProjectionRunError } from "@sixb/core/storage"
|
|
52
|
+
import { PROJECTION_RUN_FAILURE_CODES, ProjectionRunError } from "@sixb/core/storage"
|
|
44
53
|
import { queryLatestRunsByOwnerId } from "./latest-run-query"
|
|
45
54
|
import type { SQLClient, SqlParameter } from "./pg-client"
|
|
46
55
|
import { lockAdvisoryKeys, type PgStoreClient, runPgTransaction } from "./transactions"
|
|
47
56
|
|
|
48
57
|
export class PgProjectionRunStorage implements ProjectionRunStorage {
|
|
49
|
-
constructor(
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
58
|
+
constructor(
|
|
59
|
+
private readonly sql: PgStoreClient,
|
|
60
|
+
private readonly executions: ExecutionStorage
|
|
61
|
+
) {}
|
|
62
|
+
|
|
63
|
+
async queue(input: QueueProjectionRunInput): Promise<ProjectionRunRecord> {
|
|
64
|
+
assertProjectionRunQueueInput(input)
|
|
65
|
+
await assertProjectionRunDurableExecution({
|
|
66
|
+
executions: this.executions,
|
|
67
|
+
projectId: input.projectId,
|
|
68
|
+
executionId: input.executionId,
|
|
69
|
+
runId: input.id,
|
|
70
|
+
projectionId: input.identity.projectionId,
|
|
71
|
+
datasetId: input.identity.datasetVersion.datasetId,
|
|
72
|
+
datasetVersionId: input.identity.datasetVersion.versionId,
|
|
73
|
+
})
|
|
53
74
|
|
|
54
75
|
return runPgTransaction(this.sql, async (tx) => {
|
|
55
76
|
await lockAdvisoryKeys(tx, [
|
|
@@ -60,98 +81,119 @@ export class PgProjectionRunStorage implements ProjectionRunStorage {
|
|
|
60
81
|
input.identity.datasetVersion.versionId
|
|
61
82
|
),
|
|
62
83
|
])
|
|
63
|
-
|
|
64
|
-
SELECT id FROM projection_runs
|
|
65
|
-
WHERE project_id = ${input.projectId}
|
|
66
|
-
AND id <> ${input.id}
|
|
67
|
-
AND dataset_id = ${input.identity.datasetVersion.datasetId}
|
|
68
|
-
AND dataset_version_id = ${input.identity.datasetVersion.versionId}
|
|
69
|
-
AND dataset_version_created_at IS NOT NULL
|
|
70
|
-
AND dataset_version_created_at <> ${input.identity.datasetVersion.createdAt}
|
|
71
|
-
LIMIT 1
|
|
72
|
-
`
|
|
73
|
-
if (metadataConflict) {
|
|
74
|
-
throw immutableDatasetVersionConflict(input.identity)
|
|
75
|
-
}
|
|
76
|
-
|
|
84
|
+
await assertDatasetVersionIsImmutable(tx, input)
|
|
77
85
|
const [existingRow] = await tx<DatabaseRow[]>`
|
|
78
86
|
SELECT * FROM projection_runs
|
|
79
87
|
WHERE project_id = ${input.projectId} AND id = ${input.id}
|
|
80
88
|
FOR UPDATE
|
|
81
89
|
`
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
const [updated] = await tx<DatabaseRow[]>`
|
|
90
|
+
if (existingRow) {
|
|
91
|
+
if (
|
|
92
|
+
!canRequeueProjectionRunAfterEnqueueFailure(rowToProjectionRunRecord(existingRow), input)
|
|
93
|
+
) {
|
|
94
|
+
throw duplicateProjectionRun(input)
|
|
95
|
+
}
|
|
96
|
+
const [requeued] = await tx<DatabaseRow[]>`
|
|
91
97
|
UPDATE projection_runs
|
|
92
|
-
SET
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
98
|
+
SET status = ${"queued"}, queued_at = ${input.queuedAt ?? new Date()},
|
|
99
|
+
started_at = ${null}, finished_at = ${null}, attempt = ${0}, execution_token = ${null},
|
|
100
|
+
next_batch_ordinal = ${input.fixedBatchSize === undefined ? null : 0},
|
|
101
|
+
next_row_offset = ${input.fixedBatchSize === undefined ? null : 0},
|
|
102
|
+
input_exhausted = ${input.fixedBatchSize === undefined ? null : false},
|
|
103
|
+
missing_target_object_type_id = ${null}, missing_target_object_id = ${null},
|
|
104
|
+
missing_target_batch_ordinal = ${null}, missing_target_first_seen_at = ${null},
|
|
105
|
+
source_rows_read = ${0}, source_rows_skipped = ${0}, error = ${null}
|
|
106
|
+
WHERE project_id = ${input.projectId} AND id = ${input.id}
|
|
107
|
+
AND status = ${"failed"} AND error->>'code' = ${"queue.enqueue_failed"}
|
|
97
108
|
RETURNING *
|
|
98
109
|
`
|
|
99
|
-
if (!
|
|
100
|
-
return
|
|
110
|
+
if (!requeued) throw duplicateProjectionRun(input)
|
|
111
|
+
return rowToProjectionRunRecord(requeued)
|
|
101
112
|
}
|
|
102
113
|
|
|
114
|
+
const record = createProjectionRunRecord(input)
|
|
115
|
+
const checkpoint = record.telemetryCheckpoint
|
|
103
116
|
const [inserted] = await tx<DatabaseRow[]>`
|
|
104
117
|
INSERT INTO projection_runs (
|
|
105
|
-
project_id,
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
source_object_type_id,
|
|
113
|
-
target_object_type_id,
|
|
114
|
-
status,
|
|
115
|
-
started_at,
|
|
116
|
-
attempt,
|
|
117
|
-
execution_token,
|
|
118
|
-
materialization_protocol,
|
|
119
|
-
dataset_version_created_at,
|
|
120
|
-
ontology_revision,
|
|
121
|
-
projection_revision,
|
|
122
|
-
ownership_hash,
|
|
123
|
-
fixed_batch_size,
|
|
124
|
-
next_batch_ordinal,
|
|
125
|
-
next_row_offset,
|
|
126
|
-
input_exhausted
|
|
118
|
+
project_id, id, execution_id, projection_id, projection_kind,
|
|
119
|
+
materialization_protocol, dataset_id, dataset_version_id,
|
|
120
|
+
dataset_version_created_at, ontology_revision, projection_revision, ownership_hash,
|
|
121
|
+
object_type_id, source_object_type_id, target_object_type_id, status, queued_at,
|
|
122
|
+
started_at, finished_at, attempt, execution_token, fixed_batch_size,
|
|
123
|
+
next_batch_ordinal, next_row_offset, input_exhausted, source_rows_read,
|
|
124
|
+
source_rows_skipped
|
|
127
125
|
) VALUES (
|
|
128
|
-
${
|
|
129
|
-
${
|
|
130
|
-
${
|
|
131
|
-
${
|
|
132
|
-
${
|
|
133
|
-
${
|
|
134
|
-
${"objectTypeId" in
|
|
135
|
-
${"sourceObjectTypeId" in
|
|
136
|
-
${"sourceObjectTypeId" in
|
|
137
|
-
${"
|
|
138
|
-
${
|
|
139
|
-
${
|
|
140
|
-
${executionToken},
|
|
141
|
-
${input.identity.protocol},
|
|
142
|
-
${input.identity.datasetVersion.createdAt},
|
|
143
|
-
${input.identity.ontologyRevision},
|
|
144
|
-
${input.identity.projectionRevision},
|
|
145
|
-
${input.identity.ownershipHash},
|
|
146
|
-
${input.fixedBatchSize ?? null},
|
|
147
|
-
${input.fixedBatchSize === undefined ? null : 0},
|
|
148
|
-
${input.fixedBatchSize === undefined ? null : 0},
|
|
149
|
-
${input.fixedBatchSize === undefined ? null : false}
|
|
126
|
+
${record.projectId}, ${record.id}, ${record.executionId},
|
|
127
|
+
${record.identity.projectionId}, ${record.identity.projectionKind},
|
|
128
|
+
${record.identity.protocol}, ${record.identity.datasetVersion.datasetId},
|
|
129
|
+
${record.identity.datasetVersion.versionId}, ${record.identity.datasetVersion.createdAt},
|
|
130
|
+
${record.identity.ontologyRevision}, ${record.identity.projectionRevision},
|
|
131
|
+
${record.identity.ownershipHash},
|
|
132
|
+
${"objectTypeId" in record.target ? record.target.objectTypeId : null},
|
|
133
|
+
${"sourceObjectTypeId" in record.target ? record.target.sourceObjectTypeId : null},
|
|
134
|
+
${"sourceObjectTypeId" in record.target ? record.target.targetObjectTypeId : null},
|
|
135
|
+
${"queued"}, ${record.queuedAt}, ${null}, ${null}, ${0}, ${null},
|
|
136
|
+
${checkpoint?.fixedBatchSize ?? null}, ${checkpoint?.nextBatchOrdinal ?? null},
|
|
137
|
+
${checkpoint?.nextRowOffset ?? null}, ${checkpoint?.inputExhausted ?? null}, ${0}, ${0}
|
|
150
138
|
)
|
|
151
139
|
RETURNING *
|
|
152
140
|
`
|
|
141
|
+
return rowToProjectionRunRecord(inserted)
|
|
142
|
+
})
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async startOrReclaim(input: StartOrReclaimProjectionRunInput): Promise<ProjectionRunClaim> {
|
|
146
|
+
assertProjectionRunStartInput(input)
|
|
153
147
|
|
|
154
|
-
|
|
148
|
+
return runPgTransaction(this.sql, async (tx) => {
|
|
149
|
+
await lockAdvisoryKeys(tx, [projectionRunLockKey(input.projectId, input.id)])
|
|
150
|
+
const [existingRow] = await tx<DatabaseRow[]>`
|
|
151
|
+
SELECT * FROM projection_runs
|
|
152
|
+
WHERE project_id = ${input.projectId} AND id = ${input.id}
|
|
153
|
+
FOR UPDATE
|
|
154
|
+
`
|
|
155
|
+
if (!existingRow) throw projectionRunNotFound(input.projectId, input.id)
|
|
156
|
+
const existing = restoreProjectionRunRow(existingRow)
|
|
157
|
+
const { attempt } = planProjectionRunReclaim(existing, input)
|
|
158
|
+
const executionToken = createFreshExecutionToken(existing.executionToken)
|
|
159
|
+
const [updated] = await tx<DatabaseRow[]>`
|
|
160
|
+
UPDATE projection_runs
|
|
161
|
+
SET status = ${"running"}, started_at = COALESCE(started_at, ${input.startedAt ?? new Date()}),
|
|
162
|
+
finished_at = ${null}, attempt = ${attempt}, execution_token = ${executionToken},
|
|
163
|
+
error = ${null}
|
|
164
|
+
WHERE project_id = ${input.projectId} AND id = ${input.id}
|
|
165
|
+
AND (
|
|
166
|
+
(status = ${"queued"} AND execution_token IS NULL)
|
|
167
|
+
OR (status = ${"running"} AND execution_token = ${existing.executionToken ?? null})
|
|
168
|
+
)
|
|
169
|
+
RETURNING *
|
|
170
|
+
`
|
|
171
|
+
if (!updated) throw staleProjectionRunExecution(input.id)
|
|
172
|
+
return projectionRunClaim(updated)
|
|
173
|
+
})
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async failEnqueue(input: FailProjectionRunEnqueueInput): Promise<ProjectionRunRecord> {
|
|
177
|
+
return runPgTransaction(this.sql, async (tx) => {
|
|
178
|
+
const [existingRow] = await tx<DatabaseRow[]>`
|
|
179
|
+
SELECT * FROM projection_runs
|
|
180
|
+
WHERE project_id = ${input.projectId} AND id = ${input.id}
|
|
181
|
+
FOR UPDATE
|
|
182
|
+
`
|
|
183
|
+
if (!existingRow) throw projectionRunNotFound(input.projectId, input.id)
|
|
184
|
+
const failed = failProjectionRunEnqueue(restoreProjectionRunRow(existingRow), input)
|
|
185
|
+
if (!failed.error) {
|
|
186
|
+
throw new ProjectionRunError(`[SixbPg] Projection run '${input.id}' has no failure.`)
|
|
187
|
+
}
|
|
188
|
+
const [updated] = await tx<DatabaseRow[]>`
|
|
189
|
+
UPDATE projection_runs
|
|
190
|
+
SET status = ${"failed"}, finished_at = ${failed.finishedAt ?? new Date()},
|
|
191
|
+
error = ${serializeSixbFailure(failed.error, PROJECTION_RUN_FAILURE_CODES)}::text::jsonb
|
|
192
|
+
WHERE project_id = ${input.projectId} AND id = ${input.id} AND status = ${"queued"}
|
|
193
|
+
RETURNING *
|
|
194
|
+
`
|
|
195
|
+
if (!updated) throw duplicateProjectionRun(input)
|
|
196
|
+
return rowToProjectionRunRecord(updated)
|
|
155
197
|
})
|
|
156
198
|
}
|
|
157
199
|
|
|
@@ -200,7 +242,7 @@ export class PgProjectionRunStorage implements ProjectionRunStorage {
|
|
|
200
242
|
source_rows_read = ${plan.progress.sourceRowsRead},
|
|
201
243
|
source_rows_skipped = ${plan.progress.sourceRowsSkipped},
|
|
202
244
|
input_exhausted = ${plan.inputExhausted ?? existingRow.input_exhausted},
|
|
203
|
-
|
|
245
|
+
error = ${plan.error === undefined ? null : serializeSixbFailure(plan.error, PROJECTION_RUN_FAILURE_CODES)}::text::jsonb
|
|
204
246
|
WHERE project_id = ${input.projectId}
|
|
205
247
|
AND id = ${input.id}
|
|
206
248
|
AND status = ${"running"}
|
|
@@ -328,11 +370,11 @@ export class PgProjectionRunStorage implements ProjectionRunStorage {
|
|
|
328
370
|
params.push(...input.statuses)
|
|
329
371
|
}
|
|
330
372
|
if (input.startedAfter) {
|
|
331
|
-
whereClauses.push(`started_at >= $${index++}`)
|
|
373
|
+
whereClauses.push(`COALESCE(started_at, queued_at) >= $${index++}`)
|
|
332
374
|
params.push(input.startedAfter)
|
|
333
375
|
}
|
|
334
376
|
if (input.startedBefore) {
|
|
335
|
-
whereClauses.push(`started_at <= $${index++}`)
|
|
377
|
+
whereClauses.push(`COALESCE(started_at, queued_at) <= $${index++}`)
|
|
336
378
|
params.push(input.startedBefore)
|
|
337
379
|
}
|
|
338
380
|
|
|
@@ -351,7 +393,7 @@ export class PgProjectionRunStorage implements ProjectionRunStorage {
|
|
|
351
393
|
let query = `
|
|
352
394
|
SELECT * FROM projection_runs
|
|
353
395
|
${where}
|
|
354
|
-
ORDER BY started_at ${order}, id ${order}
|
|
396
|
+
ORDER BY COALESCE(started_at, queued_at) ${order}, id ${order}
|
|
355
397
|
`
|
|
356
398
|
if (input.limit !== undefined) {
|
|
357
399
|
query += ` LIMIT $${index++} OFFSET $${index++}`
|
|
@@ -388,7 +430,7 @@ async function requireMaterializationExecution(
|
|
|
388
430
|
input: LockProjectionRunForMaterializationInput
|
|
389
431
|
): Promise<DatabaseRow> {
|
|
390
432
|
const row = await requireRunning(sql, input.projectId, input.id)
|
|
391
|
-
|
|
433
|
+
assertProjectionRunAttempt(restoreProjectionRunRow(row), input)
|
|
392
434
|
return row
|
|
393
435
|
}
|
|
394
436
|
|
|
@@ -406,6 +448,22 @@ async function requireRunning(sql: SQLClient, projectId: string, id: string): Pr
|
|
|
406
448
|
return row
|
|
407
449
|
}
|
|
408
450
|
|
|
451
|
+
async function assertDatasetVersionIsImmutable(
|
|
452
|
+
sql: SQLClient,
|
|
453
|
+
input: QueueProjectionRunInput
|
|
454
|
+
): Promise<void> {
|
|
455
|
+
const [conflict] = await sql<{ readonly id: string }[]>`
|
|
456
|
+
SELECT id FROM projection_runs
|
|
457
|
+
WHERE project_id = ${input.projectId}
|
|
458
|
+
AND id <> ${input.id}
|
|
459
|
+
AND dataset_id = ${input.identity.datasetVersion.datasetId}
|
|
460
|
+
AND dataset_version_id = ${input.identity.datasetVersion.versionId}
|
|
461
|
+
AND dataset_version_created_at <> ${input.identity.datasetVersion.createdAt}
|
|
462
|
+
LIMIT 1
|
|
463
|
+
`
|
|
464
|
+
if (conflict) throw immutableDatasetVersionConflict(input.identity)
|
|
465
|
+
}
|
|
466
|
+
|
|
409
467
|
function createFreshExecutionToken(previous: string | undefined): string {
|
|
410
468
|
let token = randomUUID()
|
|
411
469
|
while (token === previous) token = randomUUID()
|
|
@@ -436,6 +494,7 @@ function restoreProjectionRunRow(row: DatabaseRow): StoredProjectionRunRecord {
|
|
|
436
494
|
const persisted: PersistedProjectionRunRecord = {
|
|
437
495
|
id: row.id,
|
|
438
496
|
projectId: row.project_id,
|
|
497
|
+
executionId: row.execution_id,
|
|
439
498
|
projectionId: row.projection_id,
|
|
440
499
|
projectionKind: row.projection_kind,
|
|
441
500
|
protocol: row.materialization_protocol ?? undefined,
|
|
@@ -449,7 +508,8 @@ function restoreProjectionRunRow(row: DatabaseRow): StoredProjectionRunRecord {
|
|
|
449
508
|
sourceObjectTypeId: row.source_object_type_id ?? undefined,
|
|
450
509
|
targetObjectTypeId: row.target_object_type_id ?? undefined,
|
|
451
510
|
status: row.status,
|
|
452
|
-
|
|
511
|
+
queuedAt: new Date(row.queued_at),
|
|
512
|
+
startedAt: row.started_at ? new Date(row.started_at) : undefined,
|
|
453
513
|
finishedAt: row.finished_at ? new Date(row.finished_at) : undefined,
|
|
454
514
|
attempt: databaseSafeInteger(row.attempt, "attempt"),
|
|
455
515
|
executionToken: row.execution_token ?? undefined,
|
|
@@ -470,7 +530,7 @@ function restoreProjectionRunRow(row: DatabaseRow): StoredProjectionRunRecord {
|
|
|
470
530
|
sourceRowsRead: databaseSafeInteger(row.source_rows_read, "sourceRowsRead"),
|
|
471
531
|
sourceRowsSkipped: databaseSafeInteger(row.source_rows_skipped, "sourceRowsSkipped"),
|
|
472
532
|
},
|
|
473
|
-
|
|
533
|
+
error: row.error ?? undefined,
|
|
474
534
|
}
|
|
475
535
|
return restoreProjectionRun(persisted)
|
|
476
536
|
}
|
|
@@ -495,6 +555,7 @@ function databaseSafeInteger(value: number | string | null, fieldName: string):
|
|
|
495
555
|
interface DatabaseRow {
|
|
496
556
|
project_id: string
|
|
497
557
|
id: string
|
|
558
|
+
execution_id: string
|
|
498
559
|
projection_id: string
|
|
499
560
|
projection_kind: ProjectionKind
|
|
500
561
|
dataset_id: string
|
|
@@ -503,7 +564,8 @@ interface DatabaseRow {
|
|
|
503
564
|
source_object_type_id: string | null
|
|
504
565
|
target_object_type_id: string | null
|
|
505
566
|
status: ProjectionRunStatus
|
|
506
|
-
|
|
567
|
+
queued_at: Date | string
|
|
568
|
+
started_at: Date | string | null
|
|
507
569
|
finished_at: Date | string | null
|
|
508
570
|
attempt: number | string
|
|
509
571
|
execution_token: string | null
|
|
@@ -522,5 +584,11 @@ interface DatabaseRow {
|
|
|
522
584
|
missing_target_first_seen_at: Date | string | null
|
|
523
585
|
source_rows_read: number | string
|
|
524
586
|
source_rows_skipped: number | string
|
|
525
|
-
|
|
587
|
+
error: JsonValue | null
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function duplicateProjectionRun(input: { readonly projectId: string; readonly id: string }) {
|
|
591
|
+
return new ProjectionRunError(
|
|
592
|
+
`[SixbPg] Projection run '${input.id}' already exists for project '${input.projectId}'.`
|
|
593
|
+
)
|
|
526
594
|
}
|