@pgflow/core 0.0.0-add-workerconfig-to-context--20250905094004-b98e1fec-20250905074005 → 0.0.0-array-map-steps-302d00a8-20250922101336
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -1
- package/dist/CHANGELOG.md +6 -0
- package/dist/PLAN_race_condition_testing.md +176 -0
- package/dist/PgflowSqlClient.js +2 -2
- package/dist/README.md +7 -1
- package/dist/database-types.d.ts +15 -11
- package/dist/database-types.d.ts.map +1 -1
- package/dist/package.json +1 -1
- package/dist/supabase/migrations/20250912075001_pgflow_temp_pr1_schema.sql +185 -0
- package/dist/supabase/migrations/20250912080800_pgflow_temp_pr2_root_maps.sql +95 -0
- package/dist/supabase/migrations/20250912125339_pgflow_TEMP_task_spawning_optimization.sql +146 -0
- package/dist/supabase/migrations/20250916093518_pgflow_temp_add_cascade_complete.sql +321 -0
- package/dist/supabase/migrations/20250916142327_pgflow_temp_make_initial_tasks_nullable.sql +624 -0
- package/dist/supabase/migrations/20250916203905_pgflow_temp_handle_arrays_in_start_tasks.sql +157 -0
- package/dist/supabase/migrations/20250918042753_pgflow_temp_handle_map_output_aggregation.sql +489 -0
- package/dist/supabase/migrations/20250919101802_pgflow_temp_orphaned_messages_index.sql +688 -0
- package/dist/supabase/migrations/20250919135211_pgflow_temp_return_task_index_in_start_tasks.sql +178 -0
- package/dist/types.d.ts +2 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
-- Modify "step_states" table
|
|
2
|
+
ALTER TABLE "pgflow"."step_states" ALTER COLUMN "initial_tasks" SET NOT NULL;
|
|
3
|
+
-- Modify "start_flow" function
|
|
4
|
+
CREATE OR REPLACE FUNCTION "pgflow"."start_flow" ("flow_slug" text, "input" jsonb, "run_id" uuid DEFAULT NULL::uuid) RETURNS SETOF "pgflow"."runs" LANGUAGE plpgsql SET "search_path" = '' AS $$
|
|
5
|
+
declare
|
|
6
|
+
v_created_run pgflow.runs%ROWTYPE;
|
|
7
|
+
v_root_map_count int;
|
|
8
|
+
begin
|
|
9
|
+
|
|
10
|
+
-- Check for root map steps and validate input
|
|
11
|
+
WITH root_maps AS (
|
|
12
|
+
SELECT step_slug
|
|
13
|
+
FROM pgflow.steps
|
|
14
|
+
WHERE steps.flow_slug = start_flow.flow_slug
|
|
15
|
+
AND steps.step_type = 'map'
|
|
16
|
+
AND steps.deps_count = 0
|
|
17
|
+
)
|
|
18
|
+
SELECT COUNT(*) INTO v_root_map_count FROM root_maps;
|
|
19
|
+
|
|
20
|
+
-- If we have root map steps, validate that input is an array
|
|
21
|
+
IF v_root_map_count > 0 THEN
|
|
22
|
+
-- First check for NULL (should be caught by NOT NULL constraint, but be defensive)
|
|
23
|
+
IF start_flow.input IS NULL THEN
|
|
24
|
+
RAISE EXCEPTION 'Flow % has root map steps but input is NULL', start_flow.flow_slug;
|
|
25
|
+
END IF;
|
|
26
|
+
|
|
27
|
+
-- Then check if it's not an array
|
|
28
|
+
IF jsonb_typeof(start_flow.input) != 'array' THEN
|
|
29
|
+
RAISE EXCEPTION 'Flow % has root map steps but input is not an array (got %)',
|
|
30
|
+
start_flow.flow_slug, jsonb_typeof(start_flow.input);
|
|
31
|
+
END IF;
|
|
32
|
+
END IF;
|
|
33
|
+
|
|
34
|
+
WITH
|
|
35
|
+
flow_steps AS (
|
|
36
|
+
SELECT steps.flow_slug, steps.step_slug, steps.step_type, steps.deps_count
|
|
37
|
+
FROM pgflow.steps
|
|
38
|
+
WHERE steps.flow_slug = start_flow.flow_slug
|
|
39
|
+
),
|
|
40
|
+
created_run AS (
|
|
41
|
+
INSERT INTO pgflow.runs (run_id, flow_slug, input, remaining_steps)
|
|
42
|
+
VALUES (
|
|
43
|
+
COALESCE(start_flow.run_id, gen_random_uuid()),
|
|
44
|
+
start_flow.flow_slug,
|
|
45
|
+
start_flow.input,
|
|
46
|
+
(SELECT count(*) FROM flow_steps)
|
|
47
|
+
)
|
|
48
|
+
RETURNING *
|
|
49
|
+
),
|
|
50
|
+
created_step_states AS (
|
|
51
|
+
INSERT INTO pgflow.step_states (flow_slug, run_id, step_slug, remaining_deps, initial_tasks)
|
|
52
|
+
SELECT
|
|
53
|
+
fs.flow_slug,
|
|
54
|
+
(SELECT created_run.run_id FROM created_run),
|
|
55
|
+
fs.step_slug,
|
|
56
|
+
fs.deps_count,
|
|
57
|
+
-- For root map steps (map with no deps), set initial_tasks to array length
|
|
58
|
+
-- For all other steps, set initial_tasks to 1
|
|
59
|
+
CASE
|
|
60
|
+
WHEN fs.step_type = 'map' AND fs.deps_count = 0 THEN
|
|
61
|
+
CASE
|
|
62
|
+
WHEN jsonb_typeof(start_flow.input) = 'array' THEN
|
|
63
|
+
jsonb_array_length(start_flow.input)
|
|
64
|
+
ELSE
|
|
65
|
+
1
|
|
66
|
+
END
|
|
67
|
+
ELSE
|
|
68
|
+
1
|
|
69
|
+
END
|
|
70
|
+
FROM flow_steps fs
|
|
71
|
+
)
|
|
72
|
+
SELECT * FROM created_run INTO v_created_run;
|
|
73
|
+
|
|
74
|
+
-- Send broadcast event for run started
|
|
75
|
+
PERFORM realtime.send(
|
|
76
|
+
jsonb_build_object(
|
|
77
|
+
'event_type', 'run:started',
|
|
78
|
+
'run_id', v_created_run.run_id,
|
|
79
|
+
'flow_slug', v_created_run.flow_slug,
|
|
80
|
+
'input', v_created_run.input,
|
|
81
|
+
'status', 'started',
|
|
82
|
+
'remaining_steps', v_created_run.remaining_steps,
|
|
83
|
+
'started_at', v_created_run.started_at
|
|
84
|
+
),
|
|
85
|
+
'run:started',
|
|
86
|
+
concat('pgflow:run:', v_created_run.run_id),
|
|
87
|
+
false
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
PERFORM pgflow.start_ready_steps(v_created_run.run_id);
|
|
91
|
+
|
|
92
|
+
RETURN QUERY SELECT * FROM pgflow.runs where pgflow.runs.run_id = v_created_run.run_id;
|
|
93
|
+
|
|
94
|
+
end;
|
|
95
|
+
$$;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
-- Modify "start_ready_steps" function
|
|
2
|
+
CREATE OR REPLACE FUNCTION "pgflow"."start_ready_steps" ("run_id" uuid) RETURNS void LANGUAGE sql SET "search_path" = '' AS $$
|
|
3
|
+
-- First handle empty array map steps (initial_tasks = 0) - direct transition to completed
|
|
4
|
+
WITH empty_map_steps AS (
|
|
5
|
+
SELECT step_state.*
|
|
6
|
+
FROM pgflow.step_states AS step_state
|
|
7
|
+
JOIN pgflow.steps AS step
|
|
8
|
+
ON step.flow_slug = step_state.flow_slug
|
|
9
|
+
AND step.step_slug = step_state.step_slug
|
|
10
|
+
WHERE step_state.run_id = start_ready_steps.run_id
|
|
11
|
+
AND step_state.status = 'created'
|
|
12
|
+
AND step_state.remaining_deps = 0
|
|
13
|
+
AND step.step_type = 'map'
|
|
14
|
+
AND step_state.initial_tasks = 0
|
|
15
|
+
ORDER BY step_state.step_slug
|
|
16
|
+
FOR UPDATE OF step_state
|
|
17
|
+
),
|
|
18
|
+
completed_empty_steps AS (
|
|
19
|
+
UPDATE pgflow.step_states
|
|
20
|
+
SET status = 'completed',
|
|
21
|
+
started_at = now(),
|
|
22
|
+
completed_at = now(),
|
|
23
|
+
remaining_tasks = 0
|
|
24
|
+
FROM empty_map_steps
|
|
25
|
+
WHERE pgflow.step_states.run_id = start_ready_steps.run_id
|
|
26
|
+
AND pgflow.step_states.step_slug = empty_map_steps.step_slug
|
|
27
|
+
RETURNING pgflow.step_states.*
|
|
28
|
+
),
|
|
29
|
+
broadcast_empty_completed AS (
|
|
30
|
+
SELECT
|
|
31
|
+
realtime.send(
|
|
32
|
+
jsonb_build_object(
|
|
33
|
+
'event_type', 'step:completed',
|
|
34
|
+
'run_id', completed_step.run_id,
|
|
35
|
+
'step_slug', completed_step.step_slug,
|
|
36
|
+
'status', 'completed',
|
|
37
|
+
'started_at', completed_step.started_at,
|
|
38
|
+
'completed_at', completed_step.completed_at,
|
|
39
|
+
'remaining_tasks', 0,
|
|
40
|
+
'remaining_deps', 0,
|
|
41
|
+
'output', '[]'::jsonb
|
|
42
|
+
),
|
|
43
|
+
concat('step:', completed_step.step_slug, ':completed'),
|
|
44
|
+
concat('pgflow:run:', completed_step.run_id),
|
|
45
|
+
false
|
|
46
|
+
)
|
|
47
|
+
FROM completed_empty_steps AS completed_step
|
|
48
|
+
),
|
|
49
|
+
|
|
50
|
+
-- Now handle non-empty steps (both single and map with initial_tasks > 0)
|
|
51
|
+
ready_steps AS (
|
|
52
|
+
SELECT *
|
|
53
|
+
FROM pgflow.step_states AS step_state
|
|
54
|
+
WHERE step_state.run_id = start_ready_steps.run_id
|
|
55
|
+
AND step_state.status = 'created'
|
|
56
|
+
AND step_state.remaining_deps = 0
|
|
57
|
+
-- Exclude empty map steps already handled
|
|
58
|
+
AND NOT EXISTS (
|
|
59
|
+
SELECT 1 FROM empty_map_steps
|
|
60
|
+
WHERE empty_map_steps.run_id = step_state.run_id
|
|
61
|
+
AND empty_map_steps.step_slug = step_state.step_slug
|
|
62
|
+
)
|
|
63
|
+
ORDER BY step_state.step_slug
|
|
64
|
+
FOR UPDATE
|
|
65
|
+
),
|
|
66
|
+
started_step_states AS (
|
|
67
|
+
UPDATE pgflow.step_states
|
|
68
|
+
SET status = 'started',
|
|
69
|
+
started_at = now(),
|
|
70
|
+
remaining_tasks = ready_steps.initial_tasks -- Copy initial_tasks to remaining_tasks when starting
|
|
71
|
+
FROM ready_steps
|
|
72
|
+
WHERE pgflow.step_states.run_id = start_ready_steps.run_id
|
|
73
|
+
AND pgflow.step_states.step_slug = ready_steps.step_slug
|
|
74
|
+
RETURNING pgflow.step_states.*
|
|
75
|
+
),
|
|
76
|
+
|
|
77
|
+
-- Generate tasks based on initial_tasks count
|
|
78
|
+
-- For single steps: initial_tasks = 1, so generate_series(0, 0) = single task with index 0
|
|
79
|
+
-- For map steps: initial_tasks = N, so generate_series(0, N-1) = N tasks with indices 0..N-1
|
|
80
|
+
-- Group messages by step for batch sending
|
|
81
|
+
message_batches AS (
|
|
82
|
+
SELECT
|
|
83
|
+
started_step.flow_slug,
|
|
84
|
+
started_step.run_id,
|
|
85
|
+
started_step.step_slug,
|
|
86
|
+
COALESCE(step.opt_start_delay, 0) as delay,
|
|
87
|
+
array_agg(
|
|
88
|
+
jsonb_build_object(
|
|
89
|
+
'flow_slug', started_step.flow_slug,
|
|
90
|
+
'run_id', started_step.run_id,
|
|
91
|
+
'step_slug', started_step.step_slug,
|
|
92
|
+
'task_index', task_idx.task_index
|
|
93
|
+
) ORDER BY task_idx.task_index
|
|
94
|
+
) AS messages,
|
|
95
|
+
array_agg(task_idx.task_index ORDER BY task_idx.task_index) AS task_indices
|
|
96
|
+
FROM started_step_states AS started_step
|
|
97
|
+
JOIN pgflow.steps AS step
|
|
98
|
+
ON step.flow_slug = started_step.flow_slug
|
|
99
|
+
AND step.step_slug = started_step.step_slug
|
|
100
|
+
-- Generate task indices from 0 to initial_tasks-1
|
|
101
|
+
CROSS JOIN LATERAL generate_series(0, started_step.initial_tasks - 1) AS task_idx(task_index)
|
|
102
|
+
GROUP BY started_step.flow_slug, started_step.run_id, started_step.step_slug, step.opt_start_delay
|
|
103
|
+
),
|
|
104
|
+
-- Send messages in batch for better performance with large arrays
|
|
105
|
+
sent_messages AS (
|
|
106
|
+
SELECT
|
|
107
|
+
mb.flow_slug,
|
|
108
|
+
mb.run_id,
|
|
109
|
+
mb.step_slug,
|
|
110
|
+
task_indices.task_index,
|
|
111
|
+
msg_ids.msg_id
|
|
112
|
+
FROM message_batches mb
|
|
113
|
+
CROSS JOIN LATERAL unnest(mb.task_indices) WITH ORDINALITY AS task_indices(task_index, idx_ord)
|
|
114
|
+
CROSS JOIN LATERAL pgmq.send_batch(mb.flow_slug, mb.messages, mb.delay) WITH ORDINALITY AS msg_ids(msg_id, msg_ord)
|
|
115
|
+
WHERE task_indices.idx_ord = msg_ids.msg_ord
|
|
116
|
+
),
|
|
117
|
+
|
|
118
|
+
broadcast_events AS (
|
|
119
|
+
SELECT
|
|
120
|
+
realtime.send(
|
|
121
|
+
jsonb_build_object(
|
|
122
|
+
'event_type', 'step:started',
|
|
123
|
+
'run_id', started_step.run_id,
|
|
124
|
+
'step_slug', started_step.step_slug,
|
|
125
|
+
'status', 'started',
|
|
126
|
+
'started_at', started_step.started_at,
|
|
127
|
+
'remaining_tasks', started_step.remaining_tasks,
|
|
128
|
+
'remaining_deps', started_step.remaining_deps
|
|
129
|
+
),
|
|
130
|
+
concat('step:', started_step.step_slug, ':started'),
|
|
131
|
+
concat('pgflow:run:', started_step.run_id),
|
|
132
|
+
false
|
|
133
|
+
)
|
|
134
|
+
FROM started_step_states AS started_step
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
-- Insert all generated tasks with their respective task_index values
|
|
138
|
+
INSERT INTO pgflow.step_tasks (flow_slug, run_id, step_slug, task_index, message_id)
|
|
139
|
+
SELECT
|
|
140
|
+
sent_messages.flow_slug,
|
|
141
|
+
sent_messages.run_id,
|
|
142
|
+
sent_messages.step_slug,
|
|
143
|
+
sent_messages.task_index,
|
|
144
|
+
sent_messages.msg_id
|
|
145
|
+
FROM sent_messages;
|
|
146
|
+
$$;
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
-- Create "cascade_complete_taskless_steps" function
|
|
2
|
+
CREATE FUNCTION "pgflow"."cascade_complete_taskless_steps" ("run_id" uuid) RETURNS integer LANGUAGE plpgsql AS $$
|
|
3
|
+
DECLARE
|
|
4
|
+
v_total_completed int := 0;
|
|
5
|
+
v_iteration_completed int;
|
|
6
|
+
v_iterations int := 0;
|
|
7
|
+
v_max_iterations int := 50;
|
|
8
|
+
BEGIN
|
|
9
|
+
LOOP
|
|
10
|
+
-- Safety counter to prevent infinite loops
|
|
11
|
+
v_iterations := v_iterations + 1;
|
|
12
|
+
IF v_iterations > v_max_iterations THEN
|
|
13
|
+
RAISE EXCEPTION 'Cascade loop exceeded safety limit of % iterations', v_max_iterations;
|
|
14
|
+
END IF;
|
|
15
|
+
|
|
16
|
+
WITH completed AS (
|
|
17
|
+
-- Complete all ready taskless steps in topological order
|
|
18
|
+
UPDATE pgflow.step_states ss
|
|
19
|
+
SET status = 'completed',
|
|
20
|
+
started_at = now(),
|
|
21
|
+
completed_at = now(),
|
|
22
|
+
remaining_tasks = 0
|
|
23
|
+
FROM pgflow.steps s
|
|
24
|
+
WHERE ss.run_id = cascade_complete_taskless_steps.run_id
|
|
25
|
+
AND ss.flow_slug = s.flow_slug
|
|
26
|
+
AND ss.step_slug = s.step_slug
|
|
27
|
+
AND ss.status = 'created'
|
|
28
|
+
AND ss.remaining_deps = 0
|
|
29
|
+
AND ss.initial_tasks = 0
|
|
30
|
+
-- Process in topological order to ensure proper cascade
|
|
31
|
+
RETURNING ss.*
|
|
32
|
+
),
|
|
33
|
+
dep_updates AS (
|
|
34
|
+
-- Update remaining_deps and initial_tasks for dependents of completed steps
|
|
35
|
+
UPDATE pgflow.step_states ss
|
|
36
|
+
SET remaining_deps = ss.remaining_deps - dep_count.count,
|
|
37
|
+
-- If the dependent is a map step and its dependency completed with 0 tasks,
|
|
38
|
+
-- set its initial_tasks to 0 as well
|
|
39
|
+
initial_tasks = CASE
|
|
40
|
+
WHEN s.step_type = 'map' AND dep_count.has_zero_tasks
|
|
41
|
+
THEN 0
|
|
42
|
+
ELSE ss.initial_tasks
|
|
43
|
+
END
|
|
44
|
+
FROM (
|
|
45
|
+
-- Count how many completed steps are dependencies of each dependent
|
|
46
|
+
SELECT
|
|
47
|
+
d.flow_slug,
|
|
48
|
+
d.step_slug as dependent_slug,
|
|
49
|
+
COUNT(*) as count,
|
|
50
|
+
BOOL_OR(c.initial_tasks = 0) as has_zero_tasks
|
|
51
|
+
FROM completed c
|
|
52
|
+
JOIN pgflow.deps d ON d.flow_slug = c.flow_slug
|
|
53
|
+
AND d.dep_slug = c.step_slug
|
|
54
|
+
GROUP BY d.flow_slug, d.step_slug
|
|
55
|
+
) dep_count,
|
|
56
|
+
pgflow.steps s
|
|
57
|
+
WHERE ss.run_id = cascade_complete_taskless_steps.run_id
|
|
58
|
+
AND ss.flow_slug = dep_count.flow_slug
|
|
59
|
+
AND ss.step_slug = dep_count.dependent_slug
|
|
60
|
+
AND s.flow_slug = ss.flow_slug
|
|
61
|
+
AND s.step_slug = ss.step_slug
|
|
62
|
+
),
|
|
63
|
+
run_updates AS (
|
|
64
|
+
-- Update run's remaining_steps count
|
|
65
|
+
UPDATE pgflow.runs r
|
|
66
|
+
SET remaining_steps = r.remaining_steps - c.completed_count,
|
|
67
|
+
status = CASE
|
|
68
|
+
WHEN r.remaining_steps - c.completed_count = 0
|
|
69
|
+
THEN 'completed'
|
|
70
|
+
ELSE r.status
|
|
71
|
+
END,
|
|
72
|
+
completed_at = CASE
|
|
73
|
+
WHEN r.remaining_steps - c.completed_count = 0
|
|
74
|
+
THEN now()
|
|
75
|
+
ELSE r.completed_at
|
|
76
|
+
END
|
|
77
|
+
FROM (SELECT COUNT(*) AS completed_count FROM completed) c
|
|
78
|
+
WHERE r.run_id = cascade_complete_taskless_steps.run_id
|
|
79
|
+
AND c.completed_count > 0
|
|
80
|
+
)
|
|
81
|
+
SELECT COUNT(*) INTO v_iteration_completed FROM completed;
|
|
82
|
+
|
|
83
|
+
EXIT WHEN v_iteration_completed = 0;
|
|
84
|
+
v_total_completed := v_total_completed + v_iteration_completed;
|
|
85
|
+
END LOOP;
|
|
86
|
+
|
|
87
|
+
RETURN v_total_completed;
|
|
88
|
+
END;
|
|
89
|
+
$$;
|
|
90
|
+
-- Modify "complete_task" function
|
|
91
|
+
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 $$
|
|
92
|
+
declare
|
|
93
|
+
v_step_state pgflow.step_states%ROWTYPE;
|
|
94
|
+
begin
|
|
95
|
+
|
|
96
|
+
WITH run_lock AS (
|
|
97
|
+
SELECT * FROM pgflow.runs
|
|
98
|
+
WHERE pgflow.runs.run_id = complete_task.run_id
|
|
99
|
+
FOR UPDATE
|
|
100
|
+
),
|
|
101
|
+
step_lock AS (
|
|
102
|
+
SELECT * FROM pgflow.step_states
|
|
103
|
+
WHERE pgflow.step_states.run_id = complete_task.run_id
|
|
104
|
+
AND pgflow.step_states.step_slug = complete_task.step_slug
|
|
105
|
+
FOR UPDATE
|
|
106
|
+
),
|
|
107
|
+
task AS (
|
|
108
|
+
UPDATE pgflow.step_tasks
|
|
109
|
+
SET
|
|
110
|
+
status = 'completed',
|
|
111
|
+
completed_at = now(),
|
|
112
|
+
output = complete_task.output
|
|
113
|
+
WHERE pgflow.step_tasks.run_id = complete_task.run_id
|
|
114
|
+
AND pgflow.step_tasks.step_slug = complete_task.step_slug
|
|
115
|
+
AND pgflow.step_tasks.task_index = complete_task.task_index
|
|
116
|
+
AND pgflow.step_tasks.status = 'started'
|
|
117
|
+
RETURNING *
|
|
118
|
+
),
|
|
119
|
+
step_state AS (
|
|
120
|
+
UPDATE pgflow.step_states
|
|
121
|
+
SET
|
|
122
|
+
status = CASE
|
|
123
|
+
WHEN pgflow.step_states.remaining_tasks = 1 THEN 'completed' -- Will be 0 after decrement
|
|
124
|
+
ELSE 'started'
|
|
125
|
+
END,
|
|
126
|
+
completed_at = CASE
|
|
127
|
+
WHEN pgflow.step_states.remaining_tasks = 1 THEN now() -- Will be 0 after decrement
|
|
128
|
+
ELSE NULL
|
|
129
|
+
END,
|
|
130
|
+
remaining_tasks = pgflow.step_states.remaining_tasks - 1
|
|
131
|
+
FROM task
|
|
132
|
+
WHERE pgflow.step_states.run_id = complete_task.run_id
|
|
133
|
+
AND pgflow.step_states.step_slug = complete_task.step_slug
|
|
134
|
+
RETURNING pgflow.step_states.*
|
|
135
|
+
),
|
|
136
|
+
-- Find all dependent steps if the current step was completed
|
|
137
|
+
dependent_steps AS (
|
|
138
|
+
SELECT d.step_slug AS dependent_step_slug
|
|
139
|
+
FROM pgflow.deps d
|
|
140
|
+
JOIN step_state s ON s.status = 'completed' AND d.flow_slug = s.flow_slug
|
|
141
|
+
WHERE d.dep_slug = complete_task.step_slug
|
|
142
|
+
ORDER BY d.step_slug -- Ensure consistent ordering
|
|
143
|
+
),
|
|
144
|
+
-- Lock dependent steps before updating
|
|
145
|
+
dependent_steps_lock AS (
|
|
146
|
+
SELECT * FROM pgflow.step_states
|
|
147
|
+
WHERE pgflow.step_states.run_id = complete_task.run_id
|
|
148
|
+
AND pgflow.step_states.step_slug IN (SELECT dependent_step_slug FROM dependent_steps)
|
|
149
|
+
FOR UPDATE
|
|
150
|
+
),
|
|
151
|
+
-- Update all dependent steps
|
|
152
|
+
dependent_steps_update AS (
|
|
153
|
+
UPDATE pgflow.step_states ss
|
|
154
|
+
SET remaining_deps = ss.remaining_deps - 1,
|
|
155
|
+
-- For map dependents of single steps producing arrays, set initial_tasks
|
|
156
|
+
initial_tasks = CASE
|
|
157
|
+
WHEN s.step_type = 'map' AND jsonb_typeof(complete_task.output) = 'array'
|
|
158
|
+
THEN jsonb_array_length(complete_task.output)
|
|
159
|
+
ELSE ss.initial_tasks
|
|
160
|
+
END
|
|
161
|
+
FROM dependent_steps ds, pgflow.steps s
|
|
162
|
+
WHERE ss.run_id = complete_task.run_id
|
|
163
|
+
AND ss.step_slug = ds.dependent_step_slug
|
|
164
|
+
AND s.flow_slug = ss.flow_slug
|
|
165
|
+
AND s.step_slug = ss.step_slug
|
|
166
|
+
)
|
|
167
|
+
-- Only decrement remaining_steps, don't update status
|
|
168
|
+
UPDATE pgflow.runs
|
|
169
|
+
SET remaining_steps = pgflow.runs.remaining_steps - 1
|
|
170
|
+
FROM step_state
|
|
171
|
+
WHERE pgflow.runs.run_id = complete_task.run_id
|
|
172
|
+
AND step_state.status = 'completed';
|
|
173
|
+
|
|
174
|
+
-- Get the updated step state for broadcasting
|
|
175
|
+
SELECT * INTO v_step_state FROM pgflow.step_states
|
|
176
|
+
WHERE pgflow.step_states.run_id = complete_task.run_id AND pgflow.step_states.step_slug = complete_task.step_slug;
|
|
177
|
+
|
|
178
|
+
-- Send broadcast event for step completed if the step is completed
|
|
179
|
+
IF v_step_state.status = 'completed' THEN
|
|
180
|
+
-- Step just completed, cascade any ready taskless steps
|
|
181
|
+
PERFORM pgflow.cascade_complete_taskless_steps(complete_task.run_id);
|
|
182
|
+
|
|
183
|
+
PERFORM realtime.send(
|
|
184
|
+
jsonb_build_object(
|
|
185
|
+
'event_type', 'step:completed',
|
|
186
|
+
'run_id', complete_task.run_id,
|
|
187
|
+
'step_slug', complete_task.step_slug,
|
|
188
|
+
'status', 'completed',
|
|
189
|
+
'output', complete_task.output,
|
|
190
|
+
'completed_at', v_step_state.completed_at
|
|
191
|
+
),
|
|
192
|
+
concat('step:', complete_task.step_slug, ':completed'),
|
|
193
|
+
concat('pgflow:run:', complete_task.run_id),
|
|
194
|
+
false
|
|
195
|
+
);
|
|
196
|
+
END IF;
|
|
197
|
+
|
|
198
|
+
-- For completed tasks: archive the message
|
|
199
|
+
PERFORM (
|
|
200
|
+
WITH completed_tasks AS (
|
|
201
|
+
SELECT r.flow_slug, st.message_id
|
|
202
|
+
FROM pgflow.step_tasks st
|
|
203
|
+
JOIN pgflow.runs r ON st.run_id = r.run_id
|
|
204
|
+
WHERE st.run_id = complete_task.run_id
|
|
205
|
+
AND st.step_slug = complete_task.step_slug
|
|
206
|
+
AND st.task_index = complete_task.task_index
|
|
207
|
+
AND st.status = 'completed'
|
|
208
|
+
)
|
|
209
|
+
SELECT pgmq.archive(ct.flow_slug, ct.message_id)
|
|
210
|
+
FROM completed_tasks ct
|
|
211
|
+
WHERE EXISTS (SELECT 1 FROM completed_tasks)
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
PERFORM pgflow.start_ready_steps(complete_task.run_id);
|
|
215
|
+
|
|
216
|
+
PERFORM pgflow.maybe_complete_run(complete_task.run_id);
|
|
217
|
+
|
|
218
|
+
RETURN QUERY SELECT *
|
|
219
|
+
FROM pgflow.step_tasks AS step_task
|
|
220
|
+
WHERE step_task.run_id = complete_task.run_id
|
|
221
|
+
AND step_task.step_slug = complete_task.step_slug
|
|
222
|
+
AND step_task.task_index = complete_task.task_index;
|
|
223
|
+
|
|
224
|
+
end;
|
|
225
|
+
$$;
|
|
226
|
+
-- Modify "start_flow" function
|
|
227
|
+
CREATE OR REPLACE FUNCTION "pgflow"."start_flow" ("flow_slug" text, "input" jsonb, "run_id" uuid DEFAULT NULL::uuid) RETURNS SETOF "pgflow"."runs" LANGUAGE plpgsql SET "search_path" = '' AS $$
|
|
228
|
+
declare
|
|
229
|
+
v_created_run pgflow.runs%ROWTYPE;
|
|
230
|
+
v_root_map_count int;
|
|
231
|
+
begin
|
|
232
|
+
|
|
233
|
+
-- Check for root map steps and validate input
|
|
234
|
+
WITH root_maps AS (
|
|
235
|
+
SELECT step_slug
|
|
236
|
+
FROM pgflow.steps
|
|
237
|
+
WHERE steps.flow_slug = start_flow.flow_slug
|
|
238
|
+
AND steps.step_type = 'map'
|
|
239
|
+
AND steps.deps_count = 0
|
|
240
|
+
)
|
|
241
|
+
SELECT COUNT(*) INTO v_root_map_count FROM root_maps;
|
|
242
|
+
|
|
243
|
+
-- If we have root map steps, validate that input is an array
|
|
244
|
+
IF v_root_map_count > 0 THEN
|
|
245
|
+
-- First check for NULL (should be caught by NOT NULL constraint, but be defensive)
|
|
246
|
+
IF start_flow.input IS NULL THEN
|
|
247
|
+
RAISE EXCEPTION 'Flow % has root map steps but input is NULL', start_flow.flow_slug;
|
|
248
|
+
END IF;
|
|
249
|
+
|
|
250
|
+
-- Then check if it's not an array
|
|
251
|
+
IF jsonb_typeof(start_flow.input) != 'array' THEN
|
|
252
|
+
RAISE EXCEPTION 'Flow % has root map steps but input is not an array (got %)',
|
|
253
|
+
start_flow.flow_slug, jsonb_typeof(start_flow.input);
|
|
254
|
+
END IF;
|
|
255
|
+
END IF;
|
|
256
|
+
|
|
257
|
+
WITH
|
|
258
|
+
flow_steps AS (
|
|
259
|
+
SELECT steps.flow_slug, steps.step_slug, steps.step_type, steps.deps_count
|
|
260
|
+
FROM pgflow.steps
|
|
261
|
+
WHERE steps.flow_slug = start_flow.flow_slug
|
|
262
|
+
),
|
|
263
|
+
created_run AS (
|
|
264
|
+
INSERT INTO pgflow.runs (run_id, flow_slug, input, remaining_steps)
|
|
265
|
+
VALUES (
|
|
266
|
+
COALESCE(start_flow.run_id, gen_random_uuid()),
|
|
267
|
+
start_flow.flow_slug,
|
|
268
|
+
start_flow.input,
|
|
269
|
+
(SELECT count(*) FROM flow_steps)
|
|
270
|
+
)
|
|
271
|
+
RETURNING *
|
|
272
|
+
),
|
|
273
|
+
created_step_states AS (
|
|
274
|
+
INSERT INTO pgflow.step_states (flow_slug, run_id, step_slug, remaining_deps, initial_tasks)
|
|
275
|
+
SELECT
|
|
276
|
+
fs.flow_slug,
|
|
277
|
+
(SELECT created_run.run_id FROM created_run),
|
|
278
|
+
fs.step_slug,
|
|
279
|
+
fs.deps_count,
|
|
280
|
+
-- For root map steps (map with no deps), set initial_tasks to array length
|
|
281
|
+
-- For all other steps, set initial_tasks to 1
|
|
282
|
+
CASE
|
|
283
|
+
WHEN fs.step_type = 'map' AND fs.deps_count = 0 THEN
|
|
284
|
+
CASE
|
|
285
|
+
WHEN jsonb_typeof(start_flow.input) = 'array' THEN
|
|
286
|
+
jsonb_array_length(start_flow.input)
|
|
287
|
+
ELSE
|
|
288
|
+
1
|
|
289
|
+
END
|
|
290
|
+
ELSE
|
|
291
|
+
1
|
|
292
|
+
END
|
|
293
|
+
FROM flow_steps fs
|
|
294
|
+
)
|
|
295
|
+
SELECT * FROM created_run INTO v_created_run;
|
|
296
|
+
|
|
297
|
+
-- Send broadcast event for run started
|
|
298
|
+
PERFORM realtime.send(
|
|
299
|
+
jsonb_build_object(
|
|
300
|
+
'event_type', 'run:started',
|
|
301
|
+
'run_id', v_created_run.run_id,
|
|
302
|
+
'flow_slug', v_created_run.flow_slug,
|
|
303
|
+
'input', v_created_run.input,
|
|
304
|
+
'status', 'started',
|
|
305
|
+
'remaining_steps', v_created_run.remaining_steps,
|
|
306
|
+
'started_at', v_created_run.started_at
|
|
307
|
+
),
|
|
308
|
+
'run:started',
|
|
309
|
+
concat('pgflow:run:', v_created_run.run_id),
|
|
310
|
+
false
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
-- Complete any taskless steps that are ready (e.g., empty array maps)
|
|
314
|
+
PERFORM pgflow.cascade_complete_taskless_steps(v_created_run.run_id);
|
|
315
|
+
|
|
316
|
+
PERFORM pgflow.start_ready_steps(v_created_run.run_id);
|
|
317
|
+
|
|
318
|
+
RETURN QUERY SELECT * FROM pgflow.runs where pgflow.runs.run_id = v_created_run.run_id;
|
|
319
|
+
|
|
320
|
+
end;
|
|
321
|
+
$$;
|