@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.
Files changed (38) hide show
  1. package/README.md +1 -7
  2. package/package.json +2 -2
  3. package/dist/ATLAS.md +0 -32
  4. package/dist/CHANGELOG.md +0 -645
  5. package/dist/PLAN_race_condition_testing.md +0 -176
  6. package/dist/PgflowSqlClient.d.ts +0 -17
  7. package/dist/PgflowSqlClient.d.ts.map +0 -1
  8. package/dist/PgflowSqlClient.js +0 -70
  9. package/dist/README.md +0 -399
  10. package/dist/database-types.d.ts +0 -832
  11. package/dist/database-types.d.ts.map +0 -1
  12. package/dist/database-types.js +0 -8
  13. package/dist/index.d.ts +0 -4
  14. package/dist/index.d.ts.map +0 -1
  15. package/dist/index.js +0 -2
  16. package/dist/package.json +0 -32
  17. package/dist/supabase/migrations/20250429164909_pgflow_initial.sql +0 -579
  18. package/dist/supabase/migrations/20250517072017_pgflow_fix_poll_for_tasks_to_use_separate_statement_for_polling.sql +0 -101
  19. package/dist/supabase/migrations/20250609105135_pgflow_add_start_tasks_and_started_status.sql +0 -371
  20. package/dist/supabase/migrations/20250610180554_pgflow_add_set_vt_batch_and_use_it_in_start_tasks.sql +0 -127
  21. package/dist/supabase/migrations/20250614124241_pgflow_add_realtime.sql +0 -501
  22. package/dist/supabase/migrations/20250619195327_pgflow_fix_fail_task_missing_realtime_event.sql +0 -185
  23. package/dist/supabase/migrations/20250627090700_pgflow_fix_function_search_paths.sql +0 -6
  24. package/dist/supabase/migrations/20250707210212_pgflow_add_opt_start_delay.sql +0 -103
  25. package/dist/supabase/migrations/20250719205006_pgflow_worker_deprecation.sql +0 -2
  26. package/dist/supabase/migrations/20250912075001_pgflow_temp_pr1_schema.sql +0 -185
  27. package/dist/supabase/migrations/20250912080800_pgflow_temp_pr2_root_maps.sql +0 -95
  28. package/dist/supabase/migrations/20250912125339_pgflow_TEMP_task_spawning_optimization.sql +0 -146
  29. package/dist/supabase/migrations/20250916093518_pgflow_temp_add_cascade_complete.sql +0 -321
  30. package/dist/supabase/migrations/20250916142327_pgflow_temp_make_initial_tasks_nullable.sql +0 -624
  31. package/dist/supabase/migrations/20250916203905_pgflow_temp_handle_arrays_in_start_tasks.sql +0 -157
  32. package/dist/supabase/migrations/20250918042753_pgflow_temp_handle_map_output_aggregation.sql +0 -489
  33. package/dist/supabase/migrations/20250919101802_pgflow_temp_orphaned_messages_index.sql +0 -688
  34. package/dist/supabase/migrations/20250919135211_pgflow_temp_return_task_index_in_start_tasks.sql +0 -178
  35. package/dist/tsconfig.lib.tsbuildinfo +0 -1
  36. package/dist/types.d.ts +0 -95
  37. package/dist/types.d.ts.map +0 -1
  38. 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
- $$;