@sixb/pg 0.1.18 → 0.1.19
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/dist/index.js +237 -276
- package/dist/index.js.map +4 -4
- package/dist/migrations.d.ts.map +1 -1
- package/dist/pg-action-run-storage.d.ts +2 -9
- package/dist/pg-action-run-storage.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/migrations/061-terminal-action-runs.sql +150 -0
- package/src/migrations.ts +2 -0
- package/src/pg-action-run-storage.ts +97 -374
package/dist/index.js
CHANGED
|
@@ -8015,6 +8015,159 @@ CREATE INDEX idx_ontology_sources_drained
|
|
|
8015
8015
|
WHERE status = 'superseded' AND root_count = 0;
|
|
8016
8016
|
`;
|
|
8017
8017
|
|
|
8018
|
+
// src/migrations/061-terminal-action-runs.sql
|
|
8019
|
+
var _061_terminal_action_runs_default = `-- Actions run in the request that asks for them and record their run once, when it ends. No run
|
|
8020
|
+
-- is queued, running, or cancelled any more, and every run has started and finished.
|
|
8021
|
+
|
|
8022
|
+
-- A run left running after its edits committed, as a crash between the commit and the end of the
|
|
8023
|
+
-- run left it, succeeded: its edits and their outbox facts are in the commit. A run that stopped
|
|
8024
|
+
-- during its effects keeps that phase, and records effects that never ended.
|
|
8025
|
+
UPDATE action_runs AS run
|
|
8026
|
+
SET
|
|
8027
|
+
status = 'succeeded',
|
|
8028
|
+
error = NULL,
|
|
8029
|
+
finished_at = committed.committed_at,
|
|
8030
|
+
phase = CASE WHEN run.phase = 'effects' THEN 'effects' ELSE 'commit' END,
|
|
8031
|
+
effects_status = CASE
|
|
8032
|
+
WHEN run.phase = 'effects' AND run.effects_status IS NULL THEN 'failed'
|
|
8033
|
+
ELSE run.effects_status
|
|
8034
|
+
END,
|
|
8035
|
+
effects_completed_at = CASE
|
|
8036
|
+
WHEN run.phase = 'effects' AND run.effects_status IS NULL THEN now()
|
|
8037
|
+
ELSE run.effects_completed_at
|
|
8038
|
+
END,
|
|
8039
|
+
effects_error = CASE
|
|
8040
|
+
WHEN run.phase = 'effects' AND run.effects_status IS NULL THEN jsonb_build_object(
|
|
8041
|
+
'code', 'internal.unexpected',
|
|
8042
|
+
'message', 'Abandoned: Actions now run synchronously; these effects never finished.',
|
|
8043
|
+
'retryable', false,
|
|
8044
|
+
'at', to_char(now() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'),
|
|
8045
|
+
'details', jsonb_build_object('actionId', run.action_id, 'runId', run.id, 'phase', 'effects')
|
|
8046
|
+
)
|
|
8047
|
+
ELSE run.effects_error
|
|
8048
|
+
END
|
|
8049
|
+
FROM ontology_commits AS committed
|
|
8050
|
+
WHERE run.status IN ('queued', 'running')
|
|
8051
|
+
AND committed.origin_kind = 'action'
|
|
8052
|
+
AND committed.project_id = run.project_id
|
|
8053
|
+
AND committed.origin_run_id = run.id;
|
|
8054
|
+
|
|
8055
|
+
-- Any other run left queued or running never finished, and nothing will resume it.
|
|
8056
|
+
UPDATE action_runs
|
|
8057
|
+
SET
|
|
8058
|
+
status = 'failed',
|
|
8059
|
+
finished_at = now(),
|
|
8060
|
+
error = jsonb_build_object(
|
|
8061
|
+
'code', 'internal.unexpected',
|
|
8062
|
+
'message', 'Abandoned: Actions now run synchronously; this run never finished.',
|
|
8063
|
+
'retryable', false,
|
|
8064
|
+
'at', to_char(now() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'),
|
|
8065
|
+
'details', jsonb_build_object('actionId', action_id, 'runId', id)
|
|
8066
|
+
)
|
|
8067
|
+
WHERE status IN ('queued', 'running');
|
|
8068
|
+
|
|
8069
|
+
-- A cancelled run failed: it keeps the failure it recorded.
|
|
8070
|
+
UPDATE action_runs SET status = 'failed' WHERE status = 'cancelled';
|
|
8071
|
+
|
|
8072
|
+
-- Every failed run carries a failure. One recorded before failures were kept explains itself.
|
|
8073
|
+
UPDATE action_runs
|
|
8074
|
+
SET error = jsonb_build_object(
|
|
8075
|
+
'code', 'internal.unexpected',
|
|
8076
|
+
'message', 'This run failed before Sixb recorded why.',
|
|
8077
|
+
'retryable', false,
|
|
8078
|
+
'at', to_char(COALESCE(finished_at, now()) AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'),
|
|
8079
|
+
'details', jsonb_build_object('actionId', action_id, 'runId', id)
|
|
8080
|
+
)
|
|
8081
|
+
WHERE status = 'failed' AND error IS NULL;
|
|
8082
|
+
|
|
8083
|
+
-- The phases \`request\`, \`enqueue\`, and \`cancelled\` are gone: a run that stopped in one of them
|
|
8084
|
+
-- never got past validation. A run starts when it was queued at the latest.
|
|
8085
|
+
UPDATE action_runs
|
|
8086
|
+
SET
|
|
8087
|
+
phase = CASE
|
|
8088
|
+
WHEN phase IN ('validation', 'writeback', 'edits', 'commit', 'effects') THEN phase
|
|
8089
|
+
ELSE 'validation'
|
|
8090
|
+
END,
|
|
8091
|
+
started_at = COALESCE(started_at, queued_at),
|
|
8092
|
+
finished_at = COALESCE(finished_at, now()),
|
|
8093
|
+
writeback_completed_at = CASE
|
|
8094
|
+
WHEN writeback_status IS NULL THEN NULL
|
|
8095
|
+
ELSE COALESCE(writeback_completed_at, finished_at, now())
|
|
8096
|
+
END,
|
|
8097
|
+
effects_completed_at = CASE
|
|
8098
|
+
WHEN effects_status IS NULL THEN NULL
|
|
8099
|
+
ELSE COALESCE(effects_completed_at, finished_at, now())
|
|
8100
|
+
END
|
|
8101
|
+
WHERE phase IS NULL
|
|
8102
|
+
OR phase NOT IN ('validation', 'writeback', 'edits', 'commit', 'effects')
|
|
8103
|
+
OR started_at IS NULL
|
|
8104
|
+
OR finished_at IS NULL
|
|
8105
|
+
OR (writeback_status IS NOT NULL AND writeback_completed_at IS NULL)
|
|
8106
|
+
OR (effects_status IS NOT NULL AND effects_completed_at IS NULL);
|
|
8107
|
+
|
|
8108
|
+
-- A failure names the phase its run ended in, and only Action codes: an enqueue failure, which no
|
|
8109
|
+
-- run can meet any more, reads as the unexpected error it now is.
|
|
8110
|
+
UPDATE action_runs
|
|
8111
|
+
SET error = jsonb_set(
|
|
8112
|
+
CASE
|
|
8113
|
+
WHEN error->>'code' = 'queue.enqueue_failed'
|
|
8114
|
+
THEN error || '{"code": "internal.unexpected", "retryable": false}'::jsonb
|
|
8115
|
+
ELSE error
|
|
8116
|
+
END,
|
|
8117
|
+
'{details,phase}',
|
|
8118
|
+
to_jsonb(phase)
|
|
8119
|
+
)
|
|
8120
|
+
WHERE error IS NOT NULL
|
|
8121
|
+
AND (
|
|
8122
|
+
error->'details'->>'phase' IS DISTINCT FROM phase
|
|
8123
|
+
OR error->>'code' = 'queue.enqueue_failed'
|
|
8124
|
+
);
|
|
8125
|
+
|
|
8126
|
+
-- The status and phase checks were unnamed. Drop exactly the two that admit the removed values.
|
|
8127
|
+
DO $$
|
|
8128
|
+
DECLARE lifecycle_constraint RECORD;
|
|
8129
|
+
BEGIN
|
|
8130
|
+
FOR lifecycle_constraint IN
|
|
8131
|
+
SELECT conname
|
|
8132
|
+
FROM pg_constraint
|
|
8133
|
+
WHERE conrelid = 'action_runs'::regclass
|
|
8134
|
+
AND contype = 'c'
|
|
8135
|
+
AND (
|
|
8136
|
+
pg_get_constraintdef(oid) LIKE '%''queued''%'
|
|
8137
|
+
OR pg_get_constraintdef(oid) LIKE '%''enqueue''%'
|
|
8138
|
+
)
|
|
8139
|
+
LOOP
|
|
8140
|
+
EXECUTE format('ALTER TABLE action_runs DROP CONSTRAINT %I', lifecycle_constraint.conname);
|
|
8141
|
+
END LOOP;
|
|
8142
|
+
END $$;
|
|
8143
|
+
|
|
8144
|
+
-- The listing indexes ordered runs by \`COALESCE(started_at, queued_at)\`.
|
|
8145
|
+
DROP INDEX idx_action_runs_project_started;
|
|
8146
|
+
DROP INDEX idx_action_runs_project_action_started;
|
|
8147
|
+
DROP INDEX idx_action_runs_project_object_started;
|
|
8148
|
+
DROP INDEX idx_action_runs_project_status_started;
|
|
8149
|
+
|
|
8150
|
+
ALTER TABLE action_runs
|
|
8151
|
+
DROP COLUMN queued_at,
|
|
8152
|
+
ALTER COLUMN phase SET NOT NULL,
|
|
8153
|
+
ALTER COLUMN started_at SET NOT NULL,
|
|
8154
|
+
ALTER COLUMN finished_at SET NOT NULL,
|
|
8155
|
+
ADD CONSTRAINT action_runs_status_check CHECK (status IN ('succeeded', 'failed')),
|
|
8156
|
+
ADD CONSTRAINT action_runs_phase_check CHECK (
|
|
8157
|
+
phase IN ('validation', 'writeback', 'edits', 'commit', 'effects')
|
|
8158
|
+
),
|
|
8159
|
+
ADD CONSTRAINT action_runs_failure_check CHECK ((status = 'failed') = (error IS NOT NULL));
|
|
8160
|
+
|
|
8161
|
+
CREATE INDEX idx_action_runs_project_started
|
|
8162
|
+
ON action_runs (project_id, started_at DESC);
|
|
8163
|
+
CREATE INDEX idx_action_runs_project_action_started
|
|
8164
|
+
ON action_runs (project_id, action_id, started_at DESC);
|
|
8165
|
+
CREATE INDEX idx_action_runs_project_object_started
|
|
8166
|
+
ON action_runs (project_id, object_type_id, primary_id, started_at DESC);
|
|
8167
|
+
CREATE INDEX idx_action_runs_project_status_started
|
|
8168
|
+
ON action_runs (project_id, status, started_at DESC);
|
|
8169
|
+
`;
|
|
8170
|
+
|
|
8018
8171
|
// src/migrations.ts
|
|
8019
8172
|
var POSTGRES_STORAGE_ADAPTER_ID = "SixbPostgresStorage";
|
|
8020
8173
|
function pgSql(id, sqlText) {
|
|
@@ -8241,7 +8394,8 @@ var postgresStorageMigrations = defineMigrations({
|
|
|
8241
8394
|
pgSql("057-audio-transcription", _057_audio_transcription_default),
|
|
8242
8395
|
pgSql("058-reranking-model-kind", _058_reranking_model_kind_default),
|
|
8243
8396
|
pgSql("059-file-download-grants", _059_file_download_grants_default),
|
|
8244
|
-
pgSql("060-drained-source-versions", _060_drained_source_versions_default)
|
|
8397
|
+
pgSql("060-drained-source-versions", _060_drained_source_versions_default),
|
|
8398
|
+
pgSql("061-terminal-action-runs", _061_terminal_action_runs_default)
|
|
8245
8399
|
]
|
|
8246
8400
|
});
|
|
8247
8401
|
|
|
@@ -15681,12 +15835,8 @@ import {
|
|
|
15681
15835
|
serializeActionRunFailure
|
|
15682
15836
|
} from "@sixb/core/internal/action-run-storage";
|
|
15683
15837
|
import { assertActionRunExecution } from "@sixb/core/internal/action-run-storage-provider";
|
|
15684
|
-
import {
|
|
15685
|
-
|
|
15686
|
-
canRequeueActionRunAfterEnqueueFailure,
|
|
15687
|
-
finishActionRunPhase
|
|
15688
|
-
} from "@sixb/core/internal/storage";
|
|
15689
|
-
import { ActionRunError, isTerminalActionRun } from "@sixb/core/storage";
|
|
15838
|
+
import { normalizeRecordActionRunInput, resolveActionRunEffects } from "@sixb/core/internal/storage";
|
|
15839
|
+
import { ActionRunError } from "@sixb/core/storage";
|
|
15690
15840
|
class PgActionRunStorage {
|
|
15691
15841
|
sql;
|
|
15692
15842
|
executions;
|
|
@@ -15694,227 +15844,83 @@ class PgActionRunStorage {
|
|
|
15694
15844
|
this.sql = sql;
|
|
15695
15845
|
this.executions = executions;
|
|
15696
15846
|
}
|
|
15697
|
-
async
|
|
15698
|
-
const
|
|
15699
|
-
SELECT *
|
|
15700
|
-
FROM action_runs
|
|
15701
|
-
WHERE project_id = ${input.projectId} AND id = ${input.runId}
|
|
15702
|
-
FOR UPDATE
|
|
15703
|
-
`;
|
|
15704
|
-
if (!row) {
|
|
15705
|
-
throw new ActionRunError(`[SixbPg] Action run '${input.runId}' not found for project '${input.projectId}'.`);
|
|
15706
|
-
}
|
|
15707
|
-
if (row.action_id !== input.actionId) {
|
|
15708
|
-
throw new ActionRunError(`[SixbPg] Action run '${input.runId}' does not belong to action '${input.actionId}'.`);
|
|
15709
|
-
}
|
|
15710
|
-
if (row.status !== "running") {
|
|
15711
|
-
throw new ActionRunError(`[SixbPg] Action run '${input.runId}' cannot materialize from status '${row.status}'.`);
|
|
15712
|
-
}
|
|
15713
|
-
return rowToActionRunRecord(row);
|
|
15714
|
-
}
|
|
15715
|
-
async queue(input) {
|
|
15847
|
+
async record(input) {
|
|
15848
|
+
const record = normalizeRecordActionRunInput(input);
|
|
15716
15849
|
await assertActionRunExecution({
|
|
15717
15850
|
executions: this.executions,
|
|
15718
|
-
projectId:
|
|
15719
|
-
executionId:
|
|
15720
|
-
runId:
|
|
15721
|
-
actionId:
|
|
15722
|
-
});
|
|
15723
|
-
try {
|
|
15724
|
-
const [row] = await this.sql`
|
|
15725
|
-
INSERT INTO action_runs (
|
|
15726
|
-
project_id,
|
|
15727
|
-
id,
|
|
15728
|
-
execution_id,
|
|
15729
|
-
action_id,
|
|
15730
|
-
subject_kind,
|
|
15731
|
-
object_type_id,
|
|
15732
|
-
primary_id,
|
|
15733
|
-
status,
|
|
15734
|
-
phase,
|
|
15735
|
-
queued_at,
|
|
15736
|
-
started_at,
|
|
15737
|
-
finished_at,
|
|
15738
|
-
params,
|
|
15739
|
-
idempotency_key,
|
|
15740
|
-
writeback_status,
|
|
15741
|
-
writeback_completed_at,
|
|
15742
|
-
writeback_result,
|
|
15743
|
-
writeback_error,
|
|
15744
|
-
effects_status,
|
|
15745
|
-
effects_completed_at,
|
|
15746
|
-
effects_error,
|
|
15747
|
-
error
|
|
15748
|
-
) VALUES (
|
|
15749
|
-
${input.projectId},
|
|
15750
|
-
${input.id},
|
|
15751
|
-
${input.executionId},
|
|
15752
|
-
${input.actionId},
|
|
15753
|
-
${input.subject.kind},
|
|
15754
|
-
${input.subject.kind === "object" ? input.subject.objectTypeId : null},
|
|
15755
|
-
${input.subject.kind === "object" ? input.subject.primaryId : null},
|
|
15756
|
-
${"queued"},
|
|
15757
|
-
${"request"},
|
|
15758
|
-
${input.queuedAt ?? new Date},
|
|
15759
|
-
${null},
|
|
15760
|
-
${null},
|
|
15761
|
-
${JSON.stringify(input.params)}::text::jsonb,
|
|
15762
|
-
${input.idempotencyKey},
|
|
15763
|
-
${null},
|
|
15764
|
-
${null},
|
|
15765
|
-
${null},
|
|
15766
|
-
${null},
|
|
15767
|
-
${null},
|
|
15768
|
-
${null},
|
|
15769
|
-
${null},
|
|
15770
|
-
${null}
|
|
15771
|
-
)
|
|
15772
|
-
RETURNING *
|
|
15773
|
-
`;
|
|
15774
|
-
return rowToActionRunRecord(row);
|
|
15775
|
-
} catch (error) {
|
|
15776
|
-
if (isUniqueViolation(error)) {
|
|
15777
|
-
return this.requeueAfterEnqueueFailure(input);
|
|
15778
|
-
}
|
|
15779
|
-
throw error;
|
|
15780
|
-
}
|
|
15781
|
-
}
|
|
15782
|
-
async requeueAfterEnqueueFailure(input) {
|
|
15783
|
-
return runPgTransaction(this.sql, async (tx) => {
|
|
15784
|
-
const [existing] = await tx`
|
|
15785
|
-
SELECT * FROM action_runs
|
|
15786
|
-
WHERE project_id = ${input.projectId} AND id = ${input.id}
|
|
15787
|
-
FOR UPDATE
|
|
15788
|
-
`;
|
|
15789
|
-
if (!existing || !canRequeueActionRunAfterEnqueueFailure(rowToActionRunRecord(existing), input)) {
|
|
15790
|
-
throw new ActionRunError(`[SixbPg] Action run '${input.id}' already exists for project '${input.projectId}'.`);
|
|
15791
|
-
}
|
|
15792
|
-
const [updated] = await tx`
|
|
15793
|
-
UPDATE action_runs
|
|
15794
|
-
SET
|
|
15795
|
-
status = ${"queued"},
|
|
15796
|
-
phase = ${"request"},
|
|
15797
|
-
queued_at = ${input.queuedAt ?? new Date},
|
|
15798
|
-
started_at = ${null},
|
|
15799
|
-
finished_at = ${null},
|
|
15800
|
-
writeback_status = ${null},
|
|
15801
|
-
writeback_completed_at = ${null},
|
|
15802
|
-
writeback_result = ${null},
|
|
15803
|
-
writeback_error = ${null},
|
|
15804
|
-
effects_status = ${null},
|
|
15805
|
-
effects_completed_at = ${null},
|
|
15806
|
-
effects_error = ${null},
|
|
15807
|
-
error = ${null}
|
|
15808
|
-
WHERE project_id = ${input.projectId} AND id = ${input.id}
|
|
15809
|
-
RETURNING *
|
|
15810
|
-
`;
|
|
15811
|
-
return rowToActionRunRecord(updated);
|
|
15851
|
+
projectId: record.projectId,
|
|
15852
|
+
executionId: record.executionId,
|
|
15853
|
+
runId: record.id,
|
|
15854
|
+
actionId: record.actionId
|
|
15812
15855
|
});
|
|
15813
|
-
|
|
15814
|
-
|
|
15815
|
-
|
|
15816
|
-
|
|
15817
|
-
|
|
15818
|
-
|
|
15819
|
-
|
|
15820
|
-
|
|
15821
|
-
|
|
15822
|
-
|
|
15823
|
-
|
|
15824
|
-
|
|
15856
|
+
const [row] = await this.sql`
|
|
15857
|
+
INSERT INTO action_runs (
|
|
15858
|
+
project_id,
|
|
15859
|
+
id,
|
|
15860
|
+
execution_id,
|
|
15861
|
+
action_id,
|
|
15862
|
+
subject_kind,
|
|
15863
|
+
object_type_id,
|
|
15864
|
+
primary_id,
|
|
15865
|
+
status,
|
|
15866
|
+
phase,
|
|
15867
|
+
started_at,
|
|
15868
|
+
finished_at,
|
|
15869
|
+
params,
|
|
15870
|
+
idempotency_key,
|
|
15871
|
+
writeback_status,
|
|
15872
|
+
writeback_completed_at,
|
|
15873
|
+
writeback_result,
|
|
15874
|
+
writeback_error,
|
|
15875
|
+
error
|
|
15876
|
+
) VALUES (
|
|
15877
|
+
${record.projectId},
|
|
15878
|
+
${record.id},
|
|
15879
|
+
${record.executionId},
|
|
15880
|
+
${record.actionId},
|
|
15881
|
+
${record.subject.kind},
|
|
15882
|
+
${record.subject.kind === "object" ? record.subject.objectTypeId : null},
|
|
15883
|
+
${record.subject.kind === "object" ? record.subject.primaryId : null},
|
|
15884
|
+
${record.status},
|
|
15885
|
+
${record.phase},
|
|
15886
|
+
${record.startedAt},
|
|
15887
|
+
${record.finishedAt},
|
|
15888
|
+
${JSON.stringify(record.params)}::text::jsonb,
|
|
15889
|
+
${record.idempotencyKey},
|
|
15890
|
+
${record.writeback?.status ?? null},
|
|
15891
|
+
${record.writeback?.completedAt ?? null},
|
|
15892
|
+
${record.writeback?.status === "succeeded" ? JSON.stringify(record.writeback.result) : null}::text::jsonb,
|
|
15893
|
+
${record.writeback?.status === "failed" ? serializeActionRunFailure(record.writeback.error, "writeback") : null}::text::jsonb,
|
|
15894
|
+
${record.status === "failed" ? serializeActionRunFailure(record.error) : null}::text::jsonb
|
|
15895
|
+
)
|
|
15896
|
+
ON CONFLICT DO NOTHING
|
|
15825
15897
|
RETURNING *
|
|
15826
15898
|
`;
|
|
15827
|
-
if (
|
|
15828
|
-
return rowToActionRunRecord(
|
|
15829
|
-
|
|
15830
|
-
|
|
15831
|
-
|
|
15832
|
-
|
|
15833
|
-
}
|
|
15834
|
-
throw new ActionRunError(`[SixbPg] Action run '${input.id}' cannot start from status '${existing.status}'.`);
|
|
15835
|
-
}
|
|
15836
|
-
async enterPhase(input) {
|
|
15837
|
-
return runPgTransaction(this.sql, async (tx) => {
|
|
15838
|
-
await this.requireRunningRun(tx, input.projectId, input.id, "transition phase");
|
|
15839
|
-
const [updated] = await tx`
|
|
15840
|
-
UPDATE action_runs
|
|
15841
|
-
SET phase = ${input.phase}
|
|
15842
|
-
WHERE project_id = ${input.projectId} AND id = ${input.id}
|
|
15843
|
-
RETURNING *
|
|
15844
|
-
`;
|
|
15845
|
-
return rowToActionRunRecord(updated);
|
|
15846
|
-
});
|
|
15847
|
-
}
|
|
15848
|
-
async recordWriteback(input) {
|
|
15849
|
-
return runPgTransaction(this.sql, async (tx) => {
|
|
15850
|
-
const existing = await this.requireRunningRun(tx, input.projectId, input.id, "record writeback");
|
|
15851
|
-
const nextWriteback = toWritebackRecord(input, new Date(input.completedAt ?? new Date));
|
|
15852
|
-
const currentWriteback = toActionRunWritebackRecord(existing);
|
|
15853
|
-
if (currentWriteback) {
|
|
15854
|
-
if (actionRunPhaseRecordsEqual(currentWriteback, nextWriteback)) {
|
|
15855
|
-
return rowToActionRunRecord(existing);
|
|
15856
|
-
}
|
|
15857
|
-
throw new ActionRunError(`[SixbPg] Action run '${input.id}' already has a different writeback record.`);
|
|
15858
|
-
}
|
|
15859
|
-
const [updated] = await tx`
|
|
15860
|
-
UPDATE action_runs
|
|
15861
|
-
SET
|
|
15862
|
-
phase = ${"writeback"},
|
|
15863
|
-
writeback_status = ${input.status},
|
|
15864
|
-
writeback_completed_at = ${nextWriteback.completedAt},
|
|
15865
|
-
writeback_result = ${input.status === "succeeded" ? JSON.stringify(input.result) : null}::text::jsonb,
|
|
15866
|
-
writeback_error = ${input.status === "failed" ? serializeActionRunFailure(input.error, "writeback") : null}::text::jsonb
|
|
15867
|
-
WHERE project_id = ${input.projectId} AND id = ${input.id}
|
|
15868
|
-
RETURNING *
|
|
15869
|
-
`;
|
|
15870
|
-
return rowToActionRunRecord(updated);
|
|
15871
|
-
});
|
|
15899
|
+
if (row)
|
|
15900
|
+
return rowToActionRunRecord(row);
|
|
15901
|
+
const [conflict] = await this.sql`
|
|
15902
|
+
SELECT id FROM action_runs
|
|
15903
|
+
WHERE project_id = ${record.projectId} AND id = ${record.id}
|
|
15904
|
+
`;
|
|
15905
|
+
throw new ActionRunError(conflict ? `[SixbPg] Action run '${record.id}' is already recorded for project '${record.projectId}'.` : `[SixbPg] Execution '${record.executionId}' already belongs to another Action run.`);
|
|
15872
15906
|
}
|
|
15873
15907
|
async recordEffects(input) {
|
|
15874
|
-
return runPgTransaction(this.sql, async (tx) => {
|
|
15875
|
-
const existing = await this.requireRunningRun(tx, input.projectId, input.id, "record effects");
|
|
15876
|
-
const nextEffects = toEffectsRecord(input, new Date(input.completedAt ?? new Date));
|
|
15877
|
-
const currentEffects = toActionRunEffectsRecord(existing);
|
|
15878
|
-
if (currentEffects) {
|
|
15879
|
-
if (actionRunPhaseRecordsEqual(currentEffects, nextEffects)) {
|
|
15880
|
-
return rowToActionRunRecord(existing);
|
|
15881
|
-
}
|
|
15882
|
-
throw new ActionRunError(`[SixbPg] Action run '${input.id}' already has a different effects record.`);
|
|
15883
|
-
}
|
|
15884
|
-
const [updated] = await tx`
|
|
15885
|
-
UPDATE action_runs
|
|
15886
|
-
SET
|
|
15887
|
-
phase = ${"effects"},
|
|
15888
|
-
effects_status = ${input.status},
|
|
15889
|
-
effects_completed_at = ${nextEffects.completedAt},
|
|
15890
|
-
effects_error = ${input.status === "failed" ? serializeActionRunFailure(input.error, "effects") : null}::text::jsonb
|
|
15891
|
-
WHERE project_id = ${input.projectId} AND id = ${input.id}
|
|
15892
|
-
RETURNING *
|
|
15893
|
-
`;
|
|
15894
|
-
return rowToActionRunRecord(updated);
|
|
15895
|
-
});
|
|
15896
|
-
}
|
|
15897
|
-
async finish(input) {
|
|
15898
15908
|
return runPgTransaction(this.sql, async (tx) => {
|
|
15899
15909
|
const [existing] = await tx`
|
|
15900
15910
|
SELECT * FROM action_runs
|
|
15901
15911
|
WHERE project_id = ${input.projectId} AND id = ${input.id}
|
|
15902
15912
|
FOR UPDATE
|
|
15903
15913
|
`;
|
|
15904
|
-
|
|
15905
|
-
|
|
15906
|
-
|
|
15907
|
-
if (isTerminalActionRun({ status: existing.status })) {
|
|
15908
|
-
throw new ActionRunError(`[SixbPg] Action run '${input.id}' cannot finish from terminal status '${existing.status}'.`);
|
|
15909
|
-
}
|
|
15910
|
-
const phase = finishActionRunPhase(input, existing.phase);
|
|
15914
|
+
const { run, effects } = resolveActionRunEffects(existing ? rowToActionRunRecord(existing) : null, input);
|
|
15915
|
+
if (!effects)
|
|
15916
|
+
return run;
|
|
15911
15917
|
const [updated] = await tx`
|
|
15912
15918
|
UPDATE action_runs
|
|
15913
15919
|
SET
|
|
15914
|
-
|
|
15915
|
-
|
|
15916
|
-
|
|
15917
|
-
|
|
15920
|
+
phase = ${"effects"},
|
|
15921
|
+
effects_status = ${effects.status},
|
|
15922
|
+
effects_completed_at = ${effects.completedAt},
|
|
15923
|
+
effects_error = ${effects.status === "failed" ? serializeActionRunFailure(effects.error, "effects") : null}::text::jsonb
|
|
15918
15924
|
WHERE project_id = ${input.projectId} AND id = ${input.id}
|
|
15919
15925
|
RETURNING *
|
|
15920
15926
|
`;
|
|
@@ -15987,11 +15993,11 @@ class PgActionRunStorage {
|
|
|
15987
15993
|
params.push(...input.statuses);
|
|
15988
15994
|
}
|
|
15989
15995
|
if (input.startedAfter) {
|
|
15990
|
-
whereClauses.push(`
|
|
15996
|
+
whereClauses.push(`started_at >= $${index++}`);
|
|
15991
15997
|
params.push(input.startedAfter);
|
|
15992
15998
|
}
|
|
15993
15999
|
if (input.startedBefore) {
|
|
15994
|
-
whereClauses.push(`
|
|
16000
|
+
whereClauses.push(`started_at <= $${index++}`);
|
|
15995
16001
|
params.push(input.startedBefore);
|
|
15996
16002
|
}
|
|
15997
16003
|
const where = `WHERE ${whereClauses.join(" AND ")}`;
|
|
@@ -16002,7 +16008,7 @@ class PgActionRunStorage {
|
|
|
16002
16008
|
let query = `
|
|
16003
16009
|
SELECT * FROM action_runs
|
|
16004
16010
|
${where}
|
|
16005
|
-
ORDER BY
|
|
16011
|
+
ORDER BY started_at ${order}, id ${order}
|
|
16006
16012
|
`;
|
|
16007
16013
|
if (input.limit !== undefined) {
|
|
16008
16014
|
query += ` LIMIT $${index++} OFFSET $${index++}`;
|
|
@@ -16020,32 +16026,12 @@ class PgActionRunStorage {
|
|
|
16020
16026
|
total
|
|
16021
16027
|
};
|
|
16022
16028
|
}
|
|
16023
|
-
async requireRunningRun(runner, projectId, id, operation) {
|
|
16024
|
-
const [existing] = await runner`
|
|
16025
|
-
SELECT * FROM action_runs
|
|
16026
|
-
WHERE project_id = ${projectId} AND id = ${id}
|
|
16027
|
-
FOR UPDATE
|
|
16028
|
-
`;
|
|
16029
|
-
if (!existing) {
|
|
16030
|
-
throw new ActionRunError(`[SixbPg] Action run '${id}' not found for project '${projectId}'.`);
|
|
16031
|
-
}
|
|
16032
|
-
if (existing.status !== "running") {
|
|
16033
|
-
throw new ActionRunError(`[SixbPg] Action run '${id}' cannot ${operation} from status '${existing.status}'.`);
|
|
16034
|
-
}
|
|
16035
|
-
return existing;
|
|
16036
|
-
}
|
|
16037
|
-
}
|
|
16038
|
-
function toActionRunFailure(row) {
|
|
16039
|
-
return row.error === null ? undefined : parseActionRunFailure(row.error);
|
|
16040
|
-
}
|
|
16041
|
-
function parsePhaseFailure(value, expectedPhase) {
|
|
16042
|
-
return parseActionRunFailure(value, expectedPhase);
|
|
16043
16029
|
}
|
|
16044
16030
|
function toActionRunWritebackRecord(row) {
|
|
16045
16031
|
if (!row.writeback_status) {
|
|
16046
16032
|
return;
|
|
16047
16033
|
}
|
|
16048
|
-
const completedAt =
|
|
16034
|
+
const completedAt = new Date(row.writeback_completed_at ?? row.finished_at);
|
|
16049
16035
|
if (row.writeback_status === "succeeded") {
|
|
16050
16036
|
return {
|
|
16051
16037
|
status: "succeeded",
|
|
@@ -16056,14 +16042,14 @@ function toActionRunWritebackRecord(row) {
|
|
|
16056
16042
|
return {
|
|
16057
16043
|
status: "failed",
|
|
16058
16044
|
completedAt,
|
|
16059
|
-
error:
|
|
16045
|
+
error: parseActionRunFailure(row.writeback_error, "writeback")
|
|
16060
16046
|
};
|
|
16061
16047
|
}
|
|
16062
16048
|
function toActionRunEffectsRecord(row) {
|
|
16063
16049
|
if (!row.effects_status) {
|
|
16064
16050
|
return;
|
|
16065
16051
|
}
|
|
16066
|
-
const completedAt =
|
|
16052
|
+
const completedAt = new Date(row.effects_completed_at ?? row.finished_at);
|
|
16067
16053
|
if (row.effects_status === "succeeded") {
|
|
16068
16054
|
return {
|
|
16069
16055
|
status: "succeeded",
|
|
@@ -16073,54 +16059,29 @@ function toActionRunEffectsRecord(row) {
|
|
|
16073
16059
|
return {
|
|
16074
16060
|
status: "failed",
|
|
16075
16061
|
completedAt,
|
|
16076
|
-
error:
|
|
16077
|
-
};
|
|
16078
|
-
}
|
|
16079
|
-
function toWritebackRecord(input, completedAt) {
|
|
16080
|
-
if (input.status === "succeeded") {
|
|
16081
|
-
return {
|
|
16082
|
-
status: "succeeded",
|
|
16083
|
-
completedAt,
|
|
16084
|
-
result: input.result
|
|
16085
|
-
};
|
|
16086
|
-
}
|
|
16087
|
-
return {
|
|
16088
|
-
status: "failed",
|
|
16089
|
-
completedAt,
|
|
16090
|
-
error: input.error
|
|
16091
|
-
};
|
|
16092
|
-
}
|
|
16093
|
-
function toEffectsRecord(input, completedAt) {
|
|
16094
|
-
if (input.status === "succeeded") {
|
|
16095
|
-
return {
|
|
16096
|
-
status: "succeeded",
|
|
16097
|
-
completedAt
|
|
16098
|
-
};
|
|
16099
|
-
}
|
|
16100
|
-
return {
|
|
16101
|
-
status: "failed",
|
|
16102
|
-
completedAt,
|
|
16103
|
-
error: input.error
|
|
16062
|
+
error: parseActionRunFailure(row.effects_error, "effects")
|
|
16104
16063
|
};
|
|
16105
16064
|
}
|
|
16106
16065
|
function rowToActionRunRecord(row) {
|
|
16107
|
-
|
|
16066
|
+
const writeback = toActionRunWritebackRecord(row);
|
|
16067
|
+
const effects = toActionRunEffectsRecord(row);
|
|
16068
|
+
const fields = {
|
|
16108
16069
|
id: row.id,
|
|
16109
16070
|
projectId: row.project_id,
|
|
16110
16071
|
executionId: row.execution_id,
|
|
16111
16072
|
actionId: row.action_id,
|
|
16112
16073
|
subject: rowToActionSubject(row),
|
|
16113
|
-
|
|
16114
|
-
|
|
16115
|
-
|
|
16116
|
-
startedAt: row.started_at ? new Date(row.started_at) : undefined,
|
|
16117
|
-
finishedAt: row.finished_at ? new Date(row.finished_at) : undefined,
|
|
16074
|
+
phase: row.phase,
|
|
16075
|
+
startedAt: new Date(row.started_at),
|
|
16076
|
+
finishedAt: new Date(row.finished_at),
|
|
16118
16077
|
params: row.params,
|
|
16119
16078
|
idempotencyKey: row.idempotency_key,
|
|
16120
|
-
writeback:
|
|
16121
|
-
effects:
|
|
16122
|
-
error: toActionRunFailure(row)
|
|
16079
|
+
...writeback === undefined ? {} : { writeback },
|
|
16080
|
+
...effects === undefined ? {} : { effects }
|
|
16123
16081
|
};
|
|
16082
|
+
if (row.status === "succeeded")
|
|
16083
|
+
return { ...fields, status: "succeeded" };
|
|
16084
|
+
return { ...fields, status: "failed", error: parseActionRunFailure(row.error) };
|
|
16124
16085
|
}
|
|
16125
16086
|
function rowToActionSubject(row) {
|
|
16126
16087
|
if (row.subject_kind === "none") {
|
|
@@ -21065,4 +21026,4 @@ export {
|
|
|
21065
21026
|
migratePostgresStorage
|
|
21066
21027
|
};
|
|
21067
21028
|
|
|
21068
|
-
//# debugId=
|
|
21029
|
+
//# debugId=295D429EF5EE4F7864756E2164756E21
|