@pgflow/core 0.0.0-array-map-steps-302d00a8-20250922101336 → 0.0.0-test-snapshot-releases-8d5d9bc1-20250922101013
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 +1 -7
- package/package.json +2 -2
- package/dist/ATLAS.md +0 -32
- package/dist/CHANGELOG.md +0 -645
- package/dist/PLAN_race_condition_testing.md +0 -176
- package/dist/PgflowSqlClient.d.ts +0 -17
- package/dist/PgflowSqlClient.d.ts.map +0 -1
- package/dist/PgflowSqlClient.js +0 -70
- package/dist/README.md +0 -399
- package/dist/database-types.d.ts +0 -832
- package/dist/database-types.d.ts.map +0 -1
- package/dist/database-types.js +0 -8
- package/dist/index.d.ts +0 -4
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -2
- package/dist/package.json +0 -32
- package/dist/supabase/migrations/20250429164909_pgflow_initial.sql +0 -579
- package/dist/supabase/migrations/20250517072017_pgflow_fix_poll_for_tasks_to_use_separate_statement_for_polling.sql +0 -101
- package/dist/supabase/migrations/20250609105135_pgflow_add_start_tasks_and_started_status.sql +0 -371
- package/dist/supabase/migrations/20250610180554_pgflow_add_set_vt_batch_and_use_it_in_start_tasks.sql +0 -127
- package/dist/supabase/migrations/20250614124241_pgflow_add_realtime.sql +0 -501
- package/dist/supabase/migrations/20250619195327_pgflow_fix_fail_task_missing_realtime_event.sql +0 -185
- package/dist/supabase/migrations/20250627090700_pgflow_fix_function_search_paths.sql +0 -6
- package/dist/supabase/migrations/20250707210212_pgflow_add_opt_start_delay.sql +0 -103
- package/dist/supabase/migrations/20250719205006_pgflow_worker_deprecation.sql +0 -2
- package/dist/supabase/migrations/20250912075001_pgflow_temp_pr1_schema.sql +0 -185
- package/dist/supabase/migrations/20250912080800_pgflow_temp_pr2_root_maps.sql +0 -95
- package/dist/supabase/migrations/20250912125339_pgflow_TEMP_task_spawning_optimization.sql +0 -146
- package/dist/supabase/migrations/20250916093518_pgflow_temp_add_cascade_complete.sql +0 -321
- package/dist/supabase/migrations/20250916142327_pgflow_temp_make_initial_tasks_nullable.sql +0 -624
- package/dist/supabase/migrations/20250916203905_pgflow_temp_handle_arrays_in_start_tasks.sql +0 -157
- package/dist/supabase/migrations/20250918042753_pgflow_temp_handle_map_output_aggregation.sql +0 -489
- package/dist/supabase/migrations/20250919101802_pgflow_temp_orphaned_messages_index.sql +0 -688
- package/dist/supabase/migrations/20250919135211_pgflow_temp_return_task_index_in_start_tasks.sql +0 -178
- package/dist/tsconfig.lib.tsbuildinfo +0 -1
- package/dist/types.d.ts +0 -95
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -1
|
@@ -1,688 +0,0 @@
|
|
|
1
|
-
-- Modify "step_tasks" table
|
|
2
|
-
ALTER TABLE "pgflow"."step_tasks" DROP CONSTRAINT "output_valid_only_for_completed", ADD CONSTRAINT "output_valid_only_for_completed" CHECK ((output IS NULL) OR (status = ANY (ARRAY['completed'::text, 'failed'::text])));
|
|
3
|
-
-- Modify "start_ready_steps" function
|
|
4
|
-
CREATE OR REPLACE FUNCTION "pgflow"."start_ready_steps" ("run_id" uuid) RETURNS void LANGUAGE plpgsql SET "search_path" = '' AS $$
|
|
5
|
-
begin
|
|
6
|
-
-- ==========================================
|
|
7
|
-
-- GUARD: No mutations on failed runs
|
|
8
|
-
-- ==========================================
|
|
9
|
-
IF EXISTS (SELECT 1 FROM pgflow.runs WHERE pgflow.runs.run_id = start_ready_steps.run_id AND pgflow.runs.status = 'failed') THEN
|
|
10
|
-
RETURN;
|
|
11
|
-
END IF;
|
|
12
|
-
|
|
13
|
-
-- ==========================================
|
|
14
|
-
-- HANDLE EMPTY ARRAY MAPS (initial_tasks = 0)
|
|
15
|
-
-- ==========================================
|
|
16
|
-
-- These complete immediately without spawning tasks
|
|
17
|
-
WITH empty_map_steps AS (
|
|
18
|
-
SELECT step_state.*
|
|
19
|
-
FROM pgflow.step_states AS step_state
|
|
20
|
-
JOIN pgflow.steps AS step
|
|
21
|
-
ON step.flow_slug = step_state.flow_slug
|
|
22
|
-
AND step.step_slug = step_state.step_slug
|
|
23
|
-
WHERE step_state.run_id = start_ready_steps.run_id
|
|
24
|
-
AND step_state.status = 'created'
|
|
25
|
-
AND step_state.remaining_deps = 0
|
|
26
|
-
AND step.step_type = 'map'
|
|
27
|
-
AND step_state.initial_tasks = 0
|
|
28
|
-
ORDER BY step_state.step_slug
|
|
29
|
-
FOR UPDATE OF step_state
|
|
30
|
-
),
|
|
31
|
-
-- ---------- Complete empty map steps ----------
|
|
32
|
-
completed_empty_steps AS (
|
|
33
|
-
UPDATE pgflow.step_states
|
|
34
|
-
SET status = 'completed',
|
|
35
|
-
started_at = now(),
|
|
36
|
-
completed_at = now(),
|
|
37
|
-
remaining_tasks = 0
|
|
38
|
-
FROM empty_map_steps
|
|
39
|
-
WHERE pgflow.step_states.run_id = start_ready_steps.run_id
|
|
40
|
-
AND pgflow.step_states.step_slug = empty_map_steps.step_slug
|
|
41
|
-
RETURNING pgflow.step_states.*
|
|
42
|
-
),
|
|
43
|
-
-- ---------- Broadcast completion events ----------
|
|
44
|
-
broadcast_empty_completed AS (
|
|
45
|
-
SELECT
|
|
46
|
-
realtime.send(
|
|
47
|
-
jsonb_build_object(
|
|
48
|
-
'event_type', 'step:completed',
|
|
49
|
-
'run_id', completed_step.run_id,
|
|
50
|
-
'step_slug', completed_step.step_slug,
|
|
51
|
-
'status', 'completed',
|
|
52
|
-
'started_at', completed_step.started_at,
|
|
53
|
-
'completed_at', completed_step.completed_at,
|
|
54
|
-
'remaining_tasks', 0,
|
|
55
|
-
'remaining_deps', 0,
|
|
56
|
-
'output', '[]'::jsonb
|
|
57
|
-
),
|
|
58
|
-
concat('step:', completed_step.step_slug, ':completed'),
|
|
59
|
-
concat('pgflow:run:', completed_step.run_id),
|
|
60
|
-
false
|
|
61
|
-
)
|
|
62
|
-
FROM completed_empty_steps AS completed_step
|
|
63
|
-
),
|
|
64
|
-
|
|
65
|
-
-- ==========================================
|
|
66
|
-
-- HANDLE NORMAL STEPS (initial_tasks > 0)
|
|
67
|
-
-- ==========================================
|
|
68
|
-
-- ---------- Find ready steps ----------
|
|
69
|
-
-- Steps with no remaining deps and known task count
|
|
70
|
-
ready_steps AS (
|
|
71
|
-
SELECT *
|
|
72
|
-
FROM pgflow.step_states AS step_state
|
|
73
|
-
WHERE step_state.run_id = start_ready_steps.run_id
|
|
74
|
-
AND step_state.status = 'created'
|
|
75
|
-
AND step_state.remaining_deps = 0
|
|
76
|
-
AND step_state.initial_tasks IS NOT NULL -- NEW: Cannot start with unknown count
|
|
77
|
-
AND step_state.initial_tasks > 0 -- Don't start taskless steps
|
|
78
|
-
-- Exclude empty map steps already handled
|
|
79
|
-
AND NOT EXISTS (
|
|
80
|
-
SELECT 1 FROM empty_map_steps
|
|
81
|
-
WHERE empty_map_steps.run_id = step_state.run_id
|
|
82
|
-
AND empty_map_steps.step_slug = step_state.step_slug
|
|
83
|
-
)
|
|
84
|
-
ORDER BY step_state.step_slug
|
|
85
|
-
FOR UPDATE
|
|
86
|
-
),
|
|
87
|
-
-- ---------- Mark steps as started ----------
|
|
88
|
-
started_step_states AS (
|
|
89
|
-
UPDATE pgflow.step_states
|
|
90
|
-
SET status = 'started',
|
|
91
|
-
started_at = now(),
|
|
92
|
-
remaining_tasks = ready_steps.initial_tasks -- Copy initial_tasks to remaining_tasks when starting
|
|
93
|
-
FROM ready_steps
|
|
94
|
-
WHERE pgflow.step_states.run_id = start_ready_steps.run_id
|
|
95
|
-
AND pgflow.step_states.step_slug = ready_steps.step_slug
|
|
96
|
-
RETURNING pgflow.step_states.*
|
|
97
|
-
),
|
|
98
|
-
|
|
99
|
-
-- ==========================================
|
|
100
|
-
-- TASK GENERATION AND QUEUE MESSAGES
|
|
101
|
-
-- ==========================================
|
|
102
|
-
-- ---------- Generate tasks and batch messages ----------
|
|
103
|
-
-- Single steps: 1 task (index 0)
|
|
104
|
-
-- Map steps: N tasks (indices 0..N-1)
|
|
105
|
-
message_batches AS (
|
|
106
|
-
SELECT
|
|
107
|
-
started_step.flow_slug,
|
|
108
|
-
started_step.run_id,
|
|
109
|
-
started_step.step_slug,
|
|
110
|
-
COALESCE(step.opt_start_delay, 0) as delay,
|
|
111
|
-
array_agg(
|
|
112
|
-
jsonb_build_object(
|
|
113
|
-
'flow_slug', started_step.flow_slug,
|
|
114
|
-
'run_id', started_step.run_id,
|
|
115
|
-
'step_slug', started_step.step_slug,
|
|
116
|
-
'task_index', task_idx.task_index
|
|
117
|
-
) ORDER BY task_idx.task_index
|
|
118
|
-
) AS messages,
|
|
119
|
-
array_agg(task_idx.task_index ORDER BY task_idx.task_index) AS task_indices
|
|
120
|
-
FROM started_step_states AS started_step
|
|
121
|
-
JOIN pgflow.steps AS step
|
|
122
|
-
ON step.flow_slug = started_step.flow_slug
|
|
123
|
-
AND step.step_slug = started_step.step_slug
|
|
124
|
-
-- Generate task indices from 0 to initial_tasks-1
|
|
125
|
-
CROSS JOIN LATERAL generate_series(0, started_step.initial_tasks - 1) AS task_idx(task_index)
|
|
126
|
-
GROUP BY started_step.flow_slug, started_step.run_id, started_step.step_slug, step.opt_start_delay
|
|
127
|
-
),
|
|
128
|
-
-- ---------- Send messages to queue ----------
|
|
129
|
-
-- Uses batch sending for performance with large arrays
|
|
130
|
-
sent_messages AS (
|
|
131
|
-
SELECT
|
|
132
|
-
mb.flow_slug,
|
|
133
|
-
mb.run_id,
|
|
134
|
-
mb.step_slug,
|
|
135
|
-
task_indices.task_index,
|
|
136
|
-
msg_ids.msg_id
|
|
137
|
-
FROM message_batches mb
|
|
138
|
-
CROSS JOIN LATERAL unnest(mb.task_indices) WITH ORDINALITY AS task_indices(task_index, idx_ord)
|
|
139
|
-
CROSS JOIN LATERAL pgmq.send_batch(mb.flow_slug, mb.messages, mb.delay) WITH ORDINALITY AS msg_ids(msg_id, msg_ord)
|
|
140
|
-
WHERE task_indices.idx_ord = msg_ids.msg_ord
|
|
141
|
-
),
|
|
142
|
-
|
|
143
|
-
-- ---------- Broadcast step:started events ----------
|
|
144
|
-
broadcast_events AS (
|
|
145
|
-
SELECT
|
|
146
|
-
realtime.send(
|
|
147
|
-
jsonb_build_object(
|
|
148
|
-
'event_type', 'step:started',
|
|
149
|
-
'run_id', started_step.run_id,
|
|
150
|
-
'step_slug', started_step.step_slug,
|
|
151
|
-
'status', 'started',
|
|
152
|
-
'started_at', started_step.started_at,
|
|
153
|
-
'remaining_tasks', started_step.remaining_tasks,
|
|
154
|
-
'remaining_deps', started_step.remaining_deps
|
|
155
|
-
),
|
|
156
|
-
concat('step:', started_step.step_slug, ':started'),
|
|
157
|
-
concat('pgflow:run:', started_step.run_id),
|
|
158
|
-
false
|
|
159
|
-
)
|
|
160
|
-
FROM started_step_states AS started_step
|
|
161
|
-
)
|
|
162
|
-
|
|
163
|
-
-- ==========================================
|
|
164
|
-
-- RECORD TASKS IN DATABASE
|
|
165
|
-
-- ==========================================
|
|
166
|
-
INSERT INTO pgflow.step_tasks (flow_slug, run_id, step_slug, task_index, message_id)
|
|
167
|
-
SELECT
|
|
168
|
-
sent_messages.flow_slug,
|
|
169
|
-
sent_messages.run_id,
|
|
170
|
-
sent_messages.step_slug,
|
|
171
|
-
sent_messages.task_index,
|
|
172
|
-
sent_messages.msg_id
|
|
173
|
-
FROM sent_messages;
|
|
174
|
-
|
|
175
|
-
end;
|
|
176
|
-
$$;
|
|
177
|
-
-- Modify "complete_task" function
|
|
178
|
-
CREATE OR REPLACE FUNCTION "pgflow"."complete_task" ("run_id" uuid, "step_slug" text, "task_index" integer, "output" jsonb) RETURNS SETOF "pgflow"."step_tasks" LANGUAGE plpgsql SET "search_path" = '' AS $$
|
|
179
|
-
declare
|
|
180
|
-
v_step_state pgflow.step_states%ROWTYPE;
|
|
181
|
-
v_dependent_map_slug text;
|
|
182
|
-
v_run_record pgflow.runs%ROWTYPE;
|
|
183
|
-
v_step_record pgflow.step_states%ROWTYPE;
|
|
184
|
-
begin
|
|
185
|
-
|
|
186
|
-
-- ==========================================
|
|
187
|
-
-- GUARD: No mutations on failed runs
|
|
188
|
-
-- ==========================================
|
|
189
|
-
IF EXISTS (SELECT 1 FROM pgflow.runs WHERE pgflow.runs.run_id = complete_task.run_id AND pgflow.runs.status = 'failed') THEN
|
|
190
|
-
RETURN QUERY SELECT * FROM pgflow.step_tasks
|
|
191
|
-
WHERE pgflow.step_tasks.run_id = complete_task.run_id
|
|
192
|
-
AND pgflow.step_tasks.step_slug = complete_task.step_slug
|
|
193
|
-
AND pgflow.step_tasks.task_index = complete_task.task_index;
|
|
194
|
-
RETURN;
|
|
195
|
-
END IF;
|
|
196
|
-
|
|
197
|
-
-- ==========================================
|
|
198
|
-
-- LOCK ACQUISITION AND TYPE VALIDATION
|
|
199
|
-
-- ==========================================
|
|
200
|
-
-- Acquire locks first to prevent race conditions
|
|
201
|
-
SELECT * INTO v_run_record FROM pgflow.runs
|
|
202
|
-
WHERE pgflow.runs.run_id = complete_task.run_id
|
|
203
|
-
FOR UPDATE;
|
|
204
|
-
|
|
205
|
-
SELECT * INTO v_step_record FROM pgflow.step_states
|
|
206
|
-
WHERE pgflow.step_states.run_id = complete_task.run_id
|
|
207
|
-
AND pgflow.step_states.step_slug = complete_task.step_slug
|
|
208
|
-
FOR UPDATE;
|
|
209
|
-
|
|
210
|
-
-- Check for type violations AFTER acquiring locks
|
|
211
|
-
SELECT child_step.step_slug INTO v_dependent_map_slug
|
|
212
|
-
FROM pgflow.deps dependency
|
|
213
|
-
JOIN pgflow.steps child_step ON child_step.flow_slug = dependency.flow_slug
|
|
214
|
-
AND child_step.step_slug = dependency.step_slug
|
|
215
|
-
JOIN pgflow.steps parent_step ON parent_step.flow_slug = dependency.flow_slug
|
|
216
|
-
AND parent_step.step_slug = dependency.dep_slug
|
|
217
|
-
JOIN pgflow.step_states child_state ON child_state.flow_slug = child_step.flow_slug
|
|
218
|
-
AND child_state.step_slug = child_step.step_slug
|
|
219
|
-
WHERE dependency.dep_slug = complete_task.step_slug -- parent is the completing step
|
|
220
|
-
AND dependency.flow_slug = v_run_record.flow_slug
|
|
221
|
-
AND parent_step.step_type = 'single' -- Only validate single steps
|
|
222
|
-
AND child_step.step_type = 'map'
|
|
223
|
-
AND child_state.run_id = complete_task.run_id
|
|
224
|
-
AND child_state.initial_tasks IS NULL
|
|
225
|
-
AND (complete_task.output IS NULL OR jsonb_typeof(complete_task.output) != 'array')
|
|
226
|
-
LIMIT 1;
|
|
227
|
-
|
|
228
|
-
-- Handle type violation if detected
|
|
229
|
-
IF v_dependent_map_slug IS NOT NULL THEN
|
|
230
|
-
-- Mark run as failed immediately
|
|
231
|
-
UPDATE pgflow.runs
|
|
232
|
-
SET status = 'failed',
|
|
233
|
-
failed_at = now()
|
|
234
|
-
WHERE pgflow.runs.run_id = complete_task.run_id;
|
|
235
|
-
|
|
236
|
-
-- Archive all active messages (both queued and started) to prevent orphaned messages
|
|
237
|
-
PERFORM pgmq.archive(
|
|
238
|
-
v_run_record.flow_slug,
|
|
239
|
-
array_agg(st.message_id)
|
|
240
|
-
)
|
|
241
|
-
FROM pgflow.step_tasks st
|
|
242
|
-
WHERE st.run_id = complete_task.run_id
|
|
243
|
-
AND st.status IN ('queued', 'started')
|
|
244
|
-
AND st.message_id IS NOT NULL
|
|
245
|
-
HAVING count(*) > 0; -- Only call archive if there are messages to archive
|
|
246
|
-
|
|
247
|
-
-- Mark current task as failed and store the output
|
|
248
|
-
UPDATE pgflow.step_tasks
|
|
249
|
-
SET status = 'failed',
|
|
250
|
-
failed_at = now(),
|
|
251
|
-
output = complete_task.output, -- Store the output that caused the violation
|
|
252
|
-
error_message = '[TYPE_VIOLATION] Produced ' ||
|
|
253
|
-
CASE WHEN complete_task.output IS NULL THEN 'null'
|
|
254
|
-
ELSE jsonb_typeof(complete_task.output) END ||
|
|
255
|
-
' instead of array'
|
|
256
|
-
WHERE pgflow.step_tasks.run_id = complete_task.run_id
|
|
257
|
-
AND pgflow.step_tasks.step_slug = complete_task.step_slug
|
|
258
|
-
AND pgflow.step_tasks.task_index = complete_task.task_index;
|
|
259
|
-
|
|
260
|
-
-- Mark step state as failed
|
|
261
|
-
UPDATE pgflow.step_states
|
|
262
|
-
SET status = 'failed',
|
|
263
|
-
failed_at = now(),
|
|
264
|
-
error_message = '[TYPE_VIOLATION] Map step ' || v_dependent_map_slug ||
|
|
265
|
-
' expects array input but dependency ' || complete_task.step_slug ||
|
|
266
|
-
' produced ' || CASE WHEN complete_task.output IS NULL THEN 'null'
|
|
267
|
-
ELSE jsonb_typeof(complete_task.output) END
|
|
268
|
-
WHERE pgflow.step_states.run_id = complete_task.run_id
|
|
269
|
-
AND pgflow.step_states.step_slug = complete_task.step_slug;
|
|
270
|
-
|
|
271
|
-
-- Archive the current task's message (it was started, now failed)
|
|
272
|
-
PERFORM pgmq.archive(
|
|
273
|
-
v_run_record.flow_slug,
|
|
274
|
-
st.message_id -- Single message, use scalar form
|
|
275
|
-
)
|
|
276
|
-
FROM pgflow.step_tasks st
|
|
277
|
-
WHERE st.run_id = complete_task.run_id
|
|
278
|
-
AND st.step_slug = complete_task.step_slug
|
|
279
|
-
AND st.task_index = complete_task.task_index
|
|
280
|
-
AND st.message_id IS NOT NULL;
|
|
281
|
-
|
|
282
|
-
-- Return empty result
|
|
283
|
-
RETURN QUERY SELECT * FROM pgflow.step_tasks WHERE false;
|
|
284
|
-
RETURN;
|
|
285
|
-
END IF;
|
|
286
|
-
|
|
287
|
-
-- ==========================================
|
|
288
|
-
-- MAIN CTE CHAIN: Update task and propagate changes
|
|
289
|
-
-- ==========================================
|
|
290
|
-
WITH
|
|
291
|
-
-- ---------- Task completion ----------
|
|
292
|
-
-- Update the task record with completion status and output
|
|
293
|
-
task AS (
|
|
294
|
-
UPDATE pgflow.step_tasks
|
|
295
|
-
SET
|
|
296
|
-
status = 'completed',
|
|
297
|
-
completed_at = now(),
|
|
298
|
-
output = complete_task.output
|
|
299
|
-
WHERE pgflow.step_tasks.run_id = complete_task.run_id
|
|
300
|
-
AND pgflow.step_tasks.step_slug = complete_task.step_slug
|
|
301
|
-
AND pgflow.step_tasks.task_index = complete_task.task_index
|
|
302
|
-
AND pgflow.step_tasks.status = 'started'
|
|
303
|
-
RETURNING *
|
|
304
|
-
),
|
|
305
|
-
-- ---------- Step state update ----------
|
|
306
|
-
-- Decrement remaining_tasks and potentially mark step as completed
|
|
307
|
-
step_state AS (
|
|
308
|
-
UPDATE pgflow.step_states
|
|
309
|
-
SET
|
|
310
|
-
status = CASE
|
|
311
|
-
WHEN pgflow.step_states.remaining_tasks = 1 THEN 'completed' -- Will be 0 after decrement
|
|
312
|
-
ELSE 'started'
|
|
313
|
-
END,
|
|
314
|
-
completed_at = CASE
|
|
315
|
-
WHEN pgflow.step_states.remaining_tasks = 1 THEN now() -- Will be 0 after decrement
|
|
316
|
-
ELSE NULL
|
|
317
|
-
END,
|
|
318
|
-
remaining_tasks = pgflow.step_states.remaining_tasks - 1
|
|
319
|
-
FROM task
|
|
320
|
-
WHERE pgflow.step_states.run_id = complete_task.run_id
|
|
321
|
-
AND pgflow.step_states.step_slug = complete_task.step_slug
|
|
322
|
-
RETURNING pgflow.step_states.*
|
|
323
|
-
),
|
|
324
|
-
-- ---------- Dependency resolution ----------
|
|
325
|
-
-- Find all child steps that depend on the completed parent step (only if parent completed)
|
|
326
|
-
child_steps AS (
|
|
327
|
-
SELECT deps.step_slug AS child_step_slug
|
|
328
|
-
FROM pgflow.deps deps
|
|
329
|
-
JOIN step_state parent_state ON parent_state.status = 'completed' AND deps.flow_slug = parent_state.flow_slug
|
|
330
|
-
WHERE deps.dep_slug = complete_task.step_slug -- dep_slug is the parent, step_slug is the child
|
|
331
|
-
ORDER BY deps.step_slug -- Ensure consistent ordering
|
|
332
|
-
),
|
|
333
|
-
-- ---------- Lock child steps ----------
|
|
334
|
-
-- Acquire locks on all child steps before updating them
|
|
335
|
-
child_steps_lock AS (
|
|
336
|
-
SELECT * FROM pgflow.step_states
|
|
337
|
-
WHERE pgflow.step_states.run_id = complete_task.run_id
|
|
338
|
-
AND pgflow.step_states.step_slug IN (SELECT child_step_slug FROM child_steps)
|
|
339
|
-
FOR UPDATE
|
|
340
|
-
),
|
|
341
|
-
-- ---------- Update child steps ----------
|
|
342
|
-
-- Decrement remaining_deps and resolve NULL initial_tasks for map steps
|
|
343
|
-
child_steps_update AS (
|
|
344
|
-
UPDATE pgflow.step_states child_state
|
|
345
|
-
SET remaining_deps = child_state.remaining_deps - 1,
|
|
346
|
-
-- Resolve NULL initial_tasks for child map steps
|
|
347
|
-
-- This is where child maps learn their array size from the parent
|
|
348
|
-
-- This CTE only runs when the parent step is complete (see child_steps JOIN)
|
|
349
|
-
initial_tasks = CASE
|
|
350
|
-
WHEN child_step.step_type = 'map' AND child_state.initial_tasks IS NULL THEN
|
|
351
|
-
CASE
|
|
352
|
-
WHEN parent_step.step_type = 'map' THEN
|
|
353
|
-
-- Map->map: Count all completed tasks from parent map
|
|
354
|
-
-- We add 1 because the current task is being completed in this transaction
|
|
355
|
-
-- but isn't yet visible as 'completed' in the step_tasks table
|
|
356
|
-
-- TODO: Refactor to use future column step_states.total_tasks
|
|
357
|
-
-- Would eliminate the COUNT query and just use parent_state.total_tasks
|
|
358
|
-
(SELECT COUNT(*)::int + 1
|
|
359
|
-
FROM pgflow.step_tasks parent_tasks
|
|
360
|
-
WHERE parent_tasks.run_id = complete_task.run_id
|
|
361
|
-
AND parent_tasks.step_slug = complete_task.step_slug
|
|
362
|
-
AND parent_tasks.status = 'completed'
|
|
363
|
-
AND parent_tasks.task_index != complete_task.task_index)
|
|
364
|
-
ELSE
|
|
365
|
-
-- Single->map: Use output array length (single steps complete immediately)
|
|
366
|
-
CASE
|
|
367
|
-
WHEN complete_task.output IS NOT NULL
|
|
368
|
-
AND jsonb_typeof(complete_task.output) = 'array' THEN
|
|
369
|
-
jsonb_array_length(complete_task.output)
|
|
370
|
-
ELSE NULL -- Keep NULL if not an array
|
|
371
|
-
END
|
|
372
|
-
END
|
|
373
|
-
ELSE child_state.initial_tasks -- Keep existing value (including NULL)
|
|
374
|
-
END
|
|
375
|
-
FROM child_steps children
|
|
376
|
-
JOIN pgflow.steps child_step ON child_step.flow_slug = (SELECT r.flow_slug FROM pgflow.runs r WHERE r.run_id = complete_task.run_id)
|
|
377
|
-
AND child_step.step_slug = children.child_step_slug
|
|
378
|
-
JOIN pgflow.steps parent_step ON parent_step.flow_slug = (SELECT r.flow_slug FROM pgflow.runs r WHERE r.run_id = complete_task.run_id)
|
|
379
|
-
AND parent_step.step_slug = complete_task.step_slug
|
|
380
|
-
WHERE child_state.run_id = complete_task.run_id
|
|
381
|
-
AND child_state.step_slug = children.child_step_slug
|
|
382
|
-
)
|
|
383
|
-
-- ---------- Update run remaining_steps ----------
|
|
384
|
-
-- Decrement the run's remaining_steps counter if step completed
|
|
385
|
-
UPDATE pgflow.runs
|
|
386
|
-
SET remaining_steps = pgflow.runs.remaining_steps - 1
|
|
387
|
-
FROM step_state
|
|
388
|
-
WHERE pgflow.runs.run_id = complete_task.run_id
|
|
389
|
-
AND step_state.status = 'completed';
|
|
390
|
-
|
|
391
|
-
-- ==========================================
|
|
392
|
-
-- POST-COMPLETION ACTIONS
|
|
393
|
-
-- ==========================================
|
|
394
|
-
|
|
395
|
-
-- ---------- Get updated state for broadcasting ----------
|
|
396
|
-
SELECT * INTO v_step_state FROM pgflow.step_states
|
|
397
|
-
WHERE pgflow.step_states.run_id = complete_task.run_id AND pgflow.step_states.step_slug = complete_task.step_slug;
|
|
398
|
-
|
|
399
|
-
-- ---------- Handle step completion ----------
|
|
400
|
-
IF v_step_state.status = 'completed' THEN
|
|
401
|
-
-- Cascade complete any taskless steps that are now ready
|
|
402
|
-
PERFORM pgflow.cascade_complete_taskless_steps(complete_task.run_id);
|
|
403
|
-
|
|
404
|
-
-- Broadcast step:completed event
|
|
405
|
-
-- For map steps, aggregate all task outputs; for single steps, use the task output
|
|
406
|
-
PERFORM realtime.send(
|
|
407
|
-
jsonb_build_object(
|
|
408
|
-
'event_type', 'step:completed',
|
|
409
|
-
'run_id', complete_task.run_id,
|
|
410
|
-
'step_slug', complete_task.step_slug,
|
|
411
|
-
'status', 'completed',
|
|
412
|
-
'output', CASE
|
|
413
|
-
WHEN (SELECT s.step_type FROM pgflow.steps s
|
|
414
|
-
WHERE s.flow_slug = v_step_state.flow_slug
|
|
415
|
-
AND s.step_slug = complete_task.step_slug) = 'map' THEN
|
|
416
|
-
-- Aggregate all task outputs for map steps
|
|
417
|
-
(SELECT COALESCE(jsonb_agg(st.output ORDER BY st.task_index), '[]'::jsonb)
|
|
418
|
-
FROM pgflow.step_tasks st
|
|
419
|
-
WHERE st.run_id = complete_task.run_id
|
|
420
|
-
AND st.step_slug = complete_task.step_slug
|
|
421
|
-
AND st.status = 'completed')
|
|
422
|
-
ELSE
|
|
423
|
-
-- Single step: use the individual task output
|
|
424
|
-
complete_task.output
|
|
425
|
-
END,
|
|
426
|
-
'completed_at', v_step_state.completed_at
|
|
427
|
-
),
|
|
428
|
-
concat('step:', complete_task.step_slug, ':completed'),
|
|
429
|
-
concat('pgflow:run:', complete_task.run_id),
|
|
430
|
-
false
|
|
431
|
-
);
|
|
432
|
-
END IF;
|
|
433
|
-
|
|
434
|
-
-- ---------- Archive completed task message ----------
|
|
435
|
-
-- Move message from active queue to archive table
|
|
436
|
-
PERFORM (
|
|
437
|
-
WITH completed_tasks AS (
|
|
438
|
-
SELECT r.flow_slug, st.message_id
|
|
439
|
-
FROM pgflow.step_tasks st
|
|
440
|
-
JOIN pgflow.runs r ON st.run_id = r.run_id
|
|
441
|
-
WHERE st.run_id = complete_task.run_id
|
|
442
|
-
AND st.step_slug = complete_task.step_slug
|
|
443
|
-
AND st.task_index = complete_task.task_index
|
|
444
|
-
AND st.status = 'completed'
|
|
445
|
-
)
|
|
446
|
-
SELECT pgmq.archive(ct.flow_slug, ct.message_id)
|
|
447
|
-
FROM completed_tasks ct
|
|
448
|
-
WHERE EXISTS (SELECT 1 FROM completed_tasks)
|
|
449
|
-
);
|
|
450
|
-
|
|
451
|
-
-- ---------- Trigger next steps ----------
|
|
452
|
-
-- Start any steps that are now ready (deps satisfied)
|
|
453
|
-
PERFORM pgflow.start_ready_steps(complete_task.run_id);
|
|
454
|
-
|
|
455
|
-
-- Check if the entire run is complete
|
|
456
|
-
PERFORM pgflow.maybe_complete_run(complete_task.run_id);
|
|
457
|
-
|
|
458
|
-
-- ---------- Return completed task ----------
|
|
459
|
-
RETURN QUERY SELECT *
|
|
460
|
-
FROM pgflow.step_tasks AS step_task
|
|
461
|
-
WHERE step_task.run_id = complete_task.run_id
|
|
462
|
-
AND step_task.step_slug = complete_task.step_slug
|
|
463
|
-
AND step_task.task_index = complete_task.task_index;
|
|
464
|
-
|
|
465
|
-
end;
|
|
466
|
-
$$;
|
|
467
|
-
-- Modify "fail_task" function
|
|
468
|
-
CREATE OR REPLACE FUNCTION "pgflow"."fail_task" ("run_id" uuid, "step_slug" text, "task_index" integer, "error_message" text) RETURNS SETOF "pgflow"."step_tasks" LANGUAGE plpgsql SET "search_path" = '' AS $$
|
|
469
|
-
DECLARE
|
|
470
|
-
v_run_failed boolean;
|
|
471
|
-
v_step_failed boolean;
|
|
472
|
-
begin
|
|
473
|
-
|
|
474
|
-
-- If run is already failed, no retries allowed
|
|
475
|
-
IF EXISTS (SELECT 1 FROM pgflow.runs WHERE pgflow.runs.run_id = fail_task.run_id AND pgflow.runs.status = 'failed') THEN
|
|
476
|
-
UPDATE pgflow.step_tasks
|
|
477
|
-
SET status = 'failed',
|
|
478
|
-
failed_at = now(),
|
|
479
|
-
error_message = fail_task.error_message
|
|
480
|
-
WHERE pgflow.step_tasks.run_id = fail_task.run_id
|
|
481
|
-
AND pgflow.step_tasks.step_slug = fail_task.step_slug
|
|
482
|
-
AND pgflow.step_tasks.task_index = fail_task.task_index
|
|
483
|
-
AND pgflow.step_tasks.status = 'started';
|
|
484
|
-
|
|
485
|
-
-- Archive the task's message
|
|
486
|
-
PERFORM pgmq.archive(r.flow_slug, ARRAY_AGG(st.message_id))
|
|
487
|
-
FROM pgflow.step_tasks st
|
|
488
|
-
JOIN pgflow.runs r ON st.run_id = r.run_id
|
|
489
|
-
WHERE st.run_id = fail_task.run_id
|
|
490
|
-
AND st.step_slug = fail_task.step_slug
|
|
491
|
-
AND st.task_index = fail_task.task_index
|
|
492
|
-
AND st.message_id IS NOT NULL
|
|
493
|
-
GROUP BY r.flow_slug
|
|
494
|
-
HAVING COUNT(st.message_id) > 0;
|
|
495
|
-
|
|
496
|
-
RETURN QUERY SELECT * FROM pgflow.step_tasks
|
|
497
|
-
WHERE pgflow.step_tasks.run_id = fail_task.run_id
|
|
498
|
-
AND pgflow.step_tasks.step_slug = fail_task.step_slug
|
|
499
|
-
AND pgflow.step_tasks.task_index = fail_task.task_index;
|
|
500
|
-
RETURN;
|
|
501
|
-
END IF;
|
|
502
|
-
|
|
503
|
-
WITH run_lock AS (
|
|
504
|
-
SELECT * FROM pgflow.runs
|
|
505
|
-
WHERE pgflow.runs.run_id = fail_task.run_id
|
|
506
|
-
FOR UPDATE
|
|
507
|
-
),
|
|
508
|
-
step_lock AS (
|
|
509
|
-
SELECT * FROM pgflow.step_states
|
|
510
|
-
WHERE pgflow.step_states.run_id = fail_task.run_id
|
|
511
|
-
AND pgflow.step_states.step_slug = fail_task.step_slug
|
|
512
|
-
FOR UPDATE
|
|
513
|
-
),
|
|
514
|
-
flow_info AS (
|
|
515
|
-
SELECT r.flow_slug
|
|
516
|
-
FROM pgflow.runs r
|
|
517
|
-
WHERE r.run_id = fail_task.run_id
|
|
518
|
-
),
|
|
519
|
-
config AS (
|
|
520
|
-
SELECT
|
|
521
|
-
COALESCE(s.opt_max_attempts, f.opt_max_attempts) AS opt_max_attempts,
|
|
522
|
-
COALESCE(s.opt_base_delay, f.opt_base_delay) AS opt_base_delay
|
|
523
|
-
FROM pgflow.steps s
|
|
524
|
-
JOIN pgflow.flows f ON f.flow_slug = s.flow_slug
|
|
525
|
-
JOIN flow_info fi ON fi.flow_slug = s.flow_slug
|
|
526
|
-
WHERE s.flow_slug = fi.flow_slug AND s.step_slug = fail_task.step_slug
|
|
527
|
-
),
|
|
528
|
-
fail_or_retry_task as (
|
|
529
|
-
UPDATE pgflow.step_tasks as task
|
|
530
|
-
SET
|
|
531
|
-
status = CASE
|
|
532
|
-
WHEN task.attempts_count < (SELECT opt_max_attempts FROM config) THEN 'queued'
|
|
533
|
-
ELSE 'failed'
|
|
534
|
-
END,
|
|
535
|
-
failed_at = CASE
|
|
536
|
-
WHEN task.attempts_count >= (SELECT opt_max_attempts FROM config) THEN now()
|
|
537
|
-
ELSE NULL
|
|
538
|
-
END,
|
|
539
|
-
started_at = CASE
|
|
540
|
-
WHEN task.attempts_count < (SELECT opt_max_attempts FROM config) THEN NULL
|
|
541
|
-
ELSE task.started_at
|
|
542
|
-
END,
|
|
543
|
-
error_message = fail_task.error_message
|
|
544
|
-
WHERE task.run_id = fail_task.run_id
|
|
545
|
-
AND task.step_slug = fail_task.step_slug
|
|
546
|
-
AND task.task_index = fail_task.task_index
|
|
547
|
-
AND task.status = 'started'
|
|
548
|
-
RETURNING *
|
|
549
|
-
),
|
|
550
|
-
maybe_fail_step AS (
|
|
551
|
-
UPDATE pgflow.step_states
|
|
552
|
-
SET
|
|
553
|
-
status = CASE
|
|
554
|
-
WHEN (select fail_or_retry_task.status from fail_or_retry_task) = 'failed' THEN 'failed'
|
|
555
|
-
ELSE pgflow.step_states.status
|
|
556
|
-
END,
|
|
557
|
-
failed_at = CASE
|
|
558
|
-
WHEN (select fail_or_retry_task.status from fail_or_retry_task) = 'failed' THEN now()
|
|
559
|
-
ELSE NULL
|
|
560
|
-
END,
|
|
561
|
-
error_message = CASE
|
|
562
|
-
WHEN (select fail_or_retry_task.status from fail_or_retry_task) = 'failed' THEN fail_task.error_message
|
|
563
|
-
ELSE NULL
|
|
564
|
-
END
|
|
565
|
-
FROM fail_or_retry_task
|
|
566
|
-
WHERE pgflow.step_states.run_id = fail_task.run_id
|
|
567
|
-
AND pgflow.step_states.step_slug = fail_task.step_slug
|
|
568
|
-
RETURNING pgflow.step_states.*
|
|
569
|
-
)
|
|
570
|
-
-- Update run status
|
|
571
|
-
UPDATE pgflow.runs
|
|
572
|
-
SET status = CASE
|
|
573
|
-
WHEN (select status from maybe_fail_step) = 'failed' THEN 'failed'
|
|
574
|
-
ELSE status
|
|
575
|
-
END,
|
|
576
|
-
failed_at = CASE
|
|
577
|
-
WHEN (select status from maybe_fail_step) = 'failed' THEN now()
|
|
578
|
-
ELSE NULL
|
|
579
|
-
END
|
|
580
|
-
WHERE pgflow.runs.run_id = fail_task.run_id
|
|
581
|
-
RETURNING (status = 'failed') INTO v_run_failed;
|
|
582
|
-
|
|
583
|
-
-- Check if step failed by querying the step_states table
|
|
584
|
-
SELECT (status = 'failed') INTO v_step_failed
|
|
585
|
-
FROM pgflow.step_states
|
|
586
|
-
WHERE pgflow.step_states.run_id = fail_task.run_id
|
|
587
|
-
AND pgflow.step_states.step_slug = fail_task.step_slug;
|
|
588
|
-
|
|
589
|
-
-- Send broadcast event for step failure if the step was failed
|
|
590
|
-
IF v_step_failed THEN
|
|
591
|
-
PERFORM realtime.send(
|
|
592
|
-
jsonb_build_object(
|
|
593
|
-
'event_type', 'step:failed',
|
|
594
|
-
'run_id', fail_task.run_id,
|
|
595
|
-
'step_slug', fail_task.step_slug,
|
|
596
|
-
'status', 'failed',
|
|
597
|
-
'error_message', fail_task.error_message,
|
|
598
|
-
'failed_at', now()
|
|
599
|
-
),
|
|
600
|
-
concat('step:', fail_task.step_slug, ':failed'),
|
|
601
|
-
concat('pgflow:run:', fail_task.run_id),
|
|
602
|
-
false
|
|
603
|
-
);
|
|
604
|
-
END IF;
|
|
605
|
-
|
|
606
|
-
-- Send broadcast event for run failure if the run was failed
|
|
607
|
-
IF v_run_failed THEN
|
|
608
|
-
DECLARE
|
|
609
|
-
v_flow_slug text;
|
|
610
|
-
BEGIN
|
|
611
|
-
SELECT flow_slug INTO v_flow_slug FROM pgflow.runs WHERE pgflow.runs.run_id = fail_task.run_id;
|
|
612
|
-
|
|
613
|
-
PERFORM realtime.send(
|
|
614
|
-
jsonb_build_object(
|
|
615
|
-
'event_type', 'run:failed',
|
|
616
|
-
'run_id', fail_task.run_id,
|
|
617
|
-
'flow_slug', v_flow_slug,
|
|
618
|
-
'status', 'failed',
|
|
619
|
-
'error_message', fail_task.error_message,
|
|
620
|
-
'failed_at', now()
|
|
621
|
-
),
|
|
622
|
-
'run:failed',
|
|
623
|
-
concat('pgflow:run:', fail_task.run_id),
|
|
624
|
-
false
|
|
625
|
-
);
|
|
626
|
-
END;
|
|
627
|
-
END IF;
|
|
628
|
-
|
|
629
|
-
-- Archive all active messages (both queued and started) when run fails
|
|
630
|
-
IF v_run_failed THEN
|
|
631
|
-
PERFORM pgmq.archive(r.flow_slug, ARRAY_AGG(st.message_id))
|
|
632
|
-
FROM pgflow.step_tasks st
|
|
633
|
-
JOIN pgflow.runs r ON st.run_id = r.run_id
|
|
634
|
-
WHERE st.run_id = fail_task.run_id
|
|
635
|
-
AND st.status IN ('queued', 'started')
|
|
636
|
-
AND st.message_id IS NOT NULL
|
|
637
|
-
GROUP BY r.flow_slug
|
|
638
|
-
HAVING COUNT(st.message_id) > 0;
|
|
639
|
-
END IF;
|
|
640
|
-
|
|
641
|
-
-- For queued tasks: delay the message for retry with exponential backoff
|
|
642
|
-
PERFORM (
|
|
643
|
-
WITH retry_config AS (
|
|
644
|
-
SELECT
|
|
645
|
-
COALESCE(s.opt_base_delay, f.opt_base_delay) AS base_delay
|
|
646
|
-
FROM pgflow.steps s
|
|
647
|
-
JOIN pgflow.flows f ON f.flow_slug = s.flow_slug
|
|
648
|
-
JOIN pgflow.runs r ON r.flow_slug = f.flow_slug
|
|
649
|
-
WHERE r.run_id = fail_task.run_id
|
|
650
|
-
AND s.step_slug = fail_task.step_slug
|
|
651
|
-
),
|
|
652
|
-
queued_tasks AS (
|
|
653
|
-
SELECT
|
|
654
|
-
r.flow_slug,
|
|
655
|
-
st.message_id,
|
|
656
|
-
pgflow.calculate_retry_delay((SELECT base_delay FROM retry_config), st.attempts_count) AS calculated_delay
|
|
657
|
-
FROM pgflow.step_tasks st
|
|
658
|
-
JOIN pgflow.runs r ON st.run_id = r.run_id
|
|
659
|
-
WHERE st.run_id = fail_task.run_id
|
|
660
|
-
AND st.step_slug = fail_task.step_slug
|
|
661
|
-
AND st.task_index = fail_task.task_index
|
|
662
|
-
AND st.status = 'queued'
|
|
663
|
-
)
|
|
664
|
-
SELECT pgmq.set_vt(qt.flow_slug, qt.message_id, qt.calculated_delay)
|
|
665
|
-
FROM queued_tasks qt
|
|
666
|
-
WHERE EXISTS (SELECT 1 FROM queued_tasks)
|
|
667
|
-
);
|
|
668
|
-
|
|
669
|
-
-- For failed tasks: archive the message
|
|
670
|
-
PERFORM pgmq.archive(r.flow_slug, ARRAY_AGG(st.message_id))
|
|
671
|
-
FROM pgflow.step_tasks st
|
|
672
|
-
JOIN pgflow.runs r ON st.run_id = r.run_id
|
|
673
|
-
WHERE st.run_id = fail_task.run_id
|
|
674
|
-
AND st.step_slug = fail_task.step_slug
|
|
675
|
-
AND st.task_index = fail_task.task_index
|
|
676
|
-
AND st.status = 'failed'
|
|
677
|
-
AND st.message_id IS NOT NULL
|
|
678
|
-
GROUP BY r.flow_slug
|
|
679
|
-
HAVING COUNT(st.message_id) > 0;
|
|
680
|
-
|
|
681
|
-
return query select *
|
|
682
|
-
from pgflow.step_tasks st
|
|
683
|
-
where st.run_id = fail_task.run_id
|
|
684
|
-
and st.step_slug = fail_task.step_slug
|
|
685
|
-
and st.task_index = fail_task.task_index;
|
|
686
|
-
|
|
687
|
-
end;
|
|
688
|
-
$$;
|