@pgflow/core 0.0.5-prealpha.2
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/LICENSE.md +660 -0
- package/README.md +373 -0
- package/__tests__/mocks/index.ts +1 -0
- package/__tests__/mocks/postgres.ts +37 -0
- package/__tests__/types/PgflowSqlClient.test-d.ts +59 -0
- package/dist/LICENSE.md +660 -0
- package/dist/README.md +373 -0
- package/dist/index.js +54 -0
- package/docs/options_for_flow_and_steps.md +75 -0
- package/docs/pgflow-blob-reference-system.md +179 -0
- package/eslint.config.cjs +22 -0
- package/example-flow.mermaid +5 -0
- package/example-flow.svg +1 -0
- package/flow-lifecycle.mermaid +83 -0
- package/flow-lifecycle.svg +1 -0
- package/out-tsc/vitest/__tests__/mocks/index.d.ts +2 -0
- package/out-tsc/vitest/__tests__/mocks/index.d.ts.map +1 -0
- package/out-tsc/vitest/__tests__/mocks/postgres.d.ts +15 -0
- package/out-tsc/vitest/__tests__/mocks/postgres.d.ts.map +1 -0
- package/out-tsc/vitest/__tests__/types/PgflowSqlClient.test-d.d.ts +2 -0
- package/out-tsc/vitest/__tests__/types/PgflowSqlClient.test-d.d.ts.map +1 -0
- package/out-tsc/vitest/tsconfig.spec.tsbuildinfo +1 -0
- package/out-tsc/vitest/vite.config.d.ts +3 -0
- package/out-tsc/vitest/vite.config.d.ts.map +1 -0
- package/package.json +28 -0
- package/pkgs/core/dist/index.js +54 -0
- package/pkgs/core/dist/pkgs/core/LICENSE.md +660 -0
- package/pkgs/core/dist/pkgs/core/README.md +373 -0
- package/pkgs/dsl/dist/index.js +123 -0
- package/pkgs/dsl/dist/pkgs/dsl/README.md +11 -0
- package/project.json +125 -0
- package/prompts/architect.md +87 -0
- package/prompts/condition.md +33 -0
- package/prompts/declarative_sql.md +15 -0
- package/prompts/deps_in_payloads.md +20 -0
- package/prompts/dsl-multi-arg.ts +48 -0
- package/prompts/dsl-options.md +39 -0
- package/prompts/dsl-single-arg.ts +51 -0
- package/prompts/dsl-two-arg.ts +61 -0
- package/prompts/dsl.md +119 -0
- package/prompts/fanout_steps.md +1 -0
- package/prompts/json_schemas.md +36 -0
- package/prompts/one_shot.md +286 -0
- package/prompts/pgtap.md +229 -0
- package/prompts/sdk.md +59 -0
- package/prompts/step_types.md +62 -0
- package/prompts/versioning.md +16 -0
- package/queries/fail_permanently.sql +17 -0
- package/queries/fail_task.sql +21 -0
- package/queries/sequential.sql +47 -0
- package/queries/two_roots_left_right.sql +59 -0
- package/schema.svg +1 -0
- package/scripts/colorize-pgtap-output.awk +72 -0
- package/scripts/run-test-with-colors +5 -0
- package/scripts/watch-test +7 -0
- package/src/PgflowSqlClient.ts +85 -0
- package/src/database-types.ts +759 -0
- package/src/index.ts +3 -0
- package/src/types.ts +103 -0
- package/supabase/config.toml +32 -0
- package/supabase/migrations/000000_schema.sql +150 -0
- package/supabase/migrations/000005_create_flow.sql +29 -0
- package/supabase/migrations/000010_add_step.sql +48 -0
- package/supabase/migrations/000015_start_ready_steps.sql +45 -0
- package/supabase/migrations/000020_start_flow.sql +46 -0
- package/supabase/migrations/000030_read_with_poll_backport.sql +70 -0
- package/supabase/migrations/000040_poll_for_tasks.sql +100 -0
- package/supabase/migrations/000045_maybe_complete_run.sql +30 -0
- package/supabase/migrations/000050_complete_task.sql +98 -0
- package/supabase/migrations/000055_calculate_retry_delay.sql +11 -0
- package/supabase/migrations/000060_fail_task.sql +124 -0
- package/supabase/migrations/000_edge_worker_initial.sql +86 -0
- package/supabase/seed.sql +202 -0
- package/supabase/tests/add_step/basic_step_addition.test.sql +29 -0
- package/supabase/tests/add_step/circular_dependency.test.sql +21 -0
- package/supabase/tests/add_step/flow_isolation.test.sql +26 -0
- package/supabase/tests/add_step/idempotent_step_addition.test.sql +20 -0
- package/supabase/tests/add_step/invalid_step_slug.test.sql +16 -0
- package/supabase/tests/add_step/nonexistent_dependency.test.sql +16 -0
- package/supabase/tests/add_step/nonexistent_flow.test.sql +13 -0
- package/supabase/tests/add_step/options.test.sql +66 -0
- package/supabase/tests/add_step/step_with_dependency.test.sql +36 -0
- package/supabase/tests/add_step/step_with_multiple_dependencies.test.sql +46 -0
- package/supabase/tests/complete_task/archives_message.test.sql +67 -0
- package/supabase/tests/complete_task/completes_run_if_no_more_remaining_steps.test.sql +62 -0
- package/supabase/tests/complete_task/completes_task_and_updates_dependents.test.sql +64 -0
- package/supabase/tests/complete_task/decrements_remaining_steps_if_completing_step.test.sql +62 -0
- package/supabase/tests/complete_task/saves_output_when_completing_run.test.sql +57 -0
- package/supabase/tests/create_flow/flow_creation.test.sql +27 -0
- package/supabase/tests/create_flow/idempotency_and_duplicates.test.sql +26 -0
- package/supabase/tests/create_flow/invalid_slug.test.sql +13 -0
- package/supabase/tests/create_flow/options.test.sql +57 -0
- package/supabase/tests/fail_task/exponential_backoff.test.sql +70 -0
- package/supabase/tests/fail_task/mark_as_failed_if_no_retries_available.test.sql +49 -0
- package/supabase/tests/fail_task/respects_flow_retry_settings.test.sql +48 -0
- package/supabase/tests/fail_task/respects_step_retry_settings.test.sql +48 -0
- package/supabase/tests/fail_task/retry_task_if_retries_available.test.sql +39 -0
- package/supabase/tests/is_valid_slug.test.sql +72 -0
- package/supabase/tests/poll_for_tasks/builds_proper_input_from_deps_outputs.test.sql +35 -0
- package/supabase/tests/poll_for_tasks/hides_messages.test.sql +35 -0
- package/supabase/tests/poll_for_tasks/increments_attempts_count.test.sql +35 -0
- package/supabase/tests/poll_for_tasks/multiple_task_processing.test.sql +24 -0
- package/supabase/tests/poll_for_tasks/polls_only_queued_tasks.test.sql +35 -0
- package/supabase/tests/poll_for_tasks/reads_messages.test.sql +38 -0
- package/supabase/tests/poll_for_tasks/returns_no_tasks_if_no_step_task_for_message.test.sql +34 -0
- package/supabase/tests/poll_for_tasks/returns_no_tasks_if_queue_is_empty.test.sql +19 -0
- package/supabase/tests/poll_for_tasks/returns_no_tasks_when_qty_set_to_0.test.sql +22 -0
- package/supabase/tests/poll_for_tasks/sets_vt_delay_based_on_opt_timeout.test.sql +41 -0
- package/supabase/tests/poll_for_tasks/tasks_reapppear_if_not_processed_in_time.test.sql +59 -0
- package/supabase/tests/start_flow/creates_run.test.sql +24 -0
- package/supabase/tests/start_flow/creates_step_states_for_all_steps.test.sql +25 -0
- package/supabase/tests/start_flow/creates_step_tasks_only_for_root_steps.test.sql +54 -0
- package/supabase/tests/start_flow/returns_run.test.sql +24 -0
- package/supabase/tests/start_flow/sends_messages_on_the_queue.test.sql +50 -0
- package/supabase/tests/start_flow/starts_only_root_steps.test.sql +21 -0
- package/supabase/tests/step_dsl_is_idempotent.test.sql +34 -0
- package/tsconfig.json +16 -0
- package/tsconfig.lib.json +26 -0
- package/tsconfig.spec.json +35 -0
- package/vite.config.ts +57 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
-- drop function if exists pgflow.complete_task(uuid, text, int, jsonb);
|
|
2
|
+
create or replace function pgflow.complete_task(
|
|
3
|
+
run_id uuid,
|
|
4
|
+
step_slug text,
|
|
5
|
+
task_index int,
|
|
6
|
+
output jsonb
|
|
7
|
+
)
|
|
8
|
+
returns setof pgflow.step_tasks
|
|
9
|
+
language plpgsql
|
|
10
|
+
volatile
|
|
11
|
+
set search_path to ''
|
|
12
|
+
as $$
|
|
13
|
+
begin
|
|
14
|
+
|
|
15
|
+
WITH run_lock AS (
|
|
16
|
+
SELECT * FROM pgflow.runs
|
|
17
|
+
WHERE pgflow.runs.run_id = complete_task.run_id
|
|
18
|
+
FOR UPDATE
|
|
19
|
+
),
|
|
20
|
+
step_lock AS (
|
|
21
|
+
SELECT * FROM pgflow.step_states
|
|
22
|
+
WHERE pgflow.step_states.run_id = complete_task.run_id
|
|
23
|
+
AND pgflow.step_states.step_slug = complete_task.step_slug
|
|
24
|
+
FOR UPDATE
|
|
25
|
+
),
|
|
26
|
+
task AS (
|
|
27
|
+
UPDATE pgflow.step_tasks
|
|
28
|
+
SET
|
|
29
|
+
status = 'completed',
|
|
30
|
+
output = complete_task.output
|
|
31
|
+
WHERE pgflow.step_tasks.run_id = complete_task.run_id
|
|
32
|
+
AND pgflow.step_tasks.step_slug = complete_task.step_slug
|
|
33
|
+
AND pgflow.step_tasks.task_index = complete_task.task_index
|
|
34
|
+
RETURNING *
|
|
35
|
+
),
|
|
36
|
+
step_state AS (
|
|
37
|
+
UPDATE pgflow.step_states
|
|
38
|
+
SET
|
|
39
|
+
status = CASE
|
|
40
|
+
WHEN pgflow.step_states.remaining_tasks = 1 THEN 'completed' -- Will be 0 after decrement
|
|
41
|
+
ELSE 'started'
|
|
42
|
+
END,
|
|
43
|
+
remaining_tasks = pgflow.step_states.remaining_tasks - 1
|
|
44
|
+
FROM task
|
|
45
|
+
WHERE pgflow.step_states.run_id = complete_task.run_id
|
|
46
|
+
AND pgflow.step_states.step_slug = complete_task.step_slug
|
|
47
|
+
RETURNING pgflow.step_states.*
|
|
48
|
+
),
|
|
49
|
+
-- Find all dependent steps if the current step was completed
|
|
50
|
+
dependent_steps AS (
|
|
51
|
+
SELECT d.step_slug AS dependent_step_slug
|
|
52
|
+
FROM pgflow.deps d
|
|
53
|
+
JOIN step_state s ON s.status = 'completed' AND d.flow_slug = s.flow_slug
|
|
54
|
+
WHERE d.dep_slug = complete_task.step_slug
|
|
55
|
+
ORDER BY d.step_slug -- Ensure consistent ordering
|
|
56
|
+
),
|
|
57
|
+
-- Lock dependent steps before updating
|
|
58
|
+
dependent_steps_lock AS (
|
|
59
|
+
SELECT * FROM pgflow.step_states
|
|
60
|
+
WHERE pgflow.step_states.run_id = complete_task.run_id
|
|
61
|
+
AND pgflow.step_states.step_slug IN (SELECT dependent_step_slug FROM dependent_steps)
|
|
62
|
+
FOR UPDATE
|
|
63
|
+
),
|
|
64
|
+
-- Update all dependent steps
|
|
65
|
+
dependent_steps_update AS (
|
|
66
|
+
UPDATE pgflow.step_states
|
|
67
|
+
SET remaining_deps = pgflow.step_states.remaining_deps - 1
|
|
68
|
+
FROM dependent_steps
|
|
69
|
+
WHERE pgflow.step_states.run_id = complete_task.run_id
|
|
70
|
+
AND pgflow.step_states.step_slug = dependent_steps.dependent_step_slug
|
|
71
|
+
)
|
|
72
|
+
-- Only decrement remaining_steps, don't update status
|
|
73
|
+
UPDATE pgflow.runs
|
|
74
|
+
SET remaining_steps = pgflow.runs.remaining_steps - 1
|
|
75
|
+
FROM step_state
|
|
76
|
+
WHERE pgflow.runs.run_id = complete_task.run_id
|
|
77
|
+
AND step_state.status = 'completed';
|
|
78
|
+
|
|
79
|
+
PERFORM pgmq.archive(
|
|
80
|
+
queue_name => (SELECT run.flow_slug FROM pgflow.runs AS run WHERE run.run_id = complete_task.run_id),
|
|
81
|
+
msg_id => (SELECT message_id FROM pgflow.step_tasks AS step_task
|
|
82
|
+
WHERE step_task.run_id = complete_task.run_id
|
|
83
|
+
AND step_task.step_slug = complete_task.step_slug
|
|
84
|
+
AND step_task.task_index = complete_task.task_index)
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
PERFORM pgflow.start_ready_steps(complete_task.run_id);
|
|
88
|
+
|
|
89
|
+
PERFORM pgflow.maybe_complete_run(complete_task.run_id);
|
|
90
|
+
|
|
91
|
+
RETURN QUERY SELECT *
|
|
92
|
+
FROM pgflow.step_tasks AS step_task
|
|
93
|
+
WHERE step_task.run_id = complete_task.run_id
|
|
94
|
+
AND step_task.step_slug = complete_task.step_slug
|
|
95
|
+
AND step_task.task_index = complete_task.task_index;
|
|
96
|
+
|
|
97
|
+
end;
|
|
98
|
+
$$;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
create or replace function pgflow.fail_task(
|
|
2
|
+
run_id uuid,
|
|
3
|
+
step_slug text,
|
|
4
|
+
task_index int,
|
|
5
|
+
error_message text
|
|
6
|
+
)
|
|
7
|
+
returns setof pgflow.step_tasks
|
|
8
|
+
language plpgsql
|
|
9
|
+
volatile
|
|
10
|
+
set search_path to ''
|
|
11
|
+
as $$
|
|
12
|
+
begin
|
|
13
|
+
|
|
14
|
+
WITH run_lock AS (
|
|
15
|
+
SELECT * FROM pgflow.runs
|
|
16
|
+
WHERE pgflow.runs.run_id = fail_task.run_id
|
|
17
|
+
FOR UPDATE
|
|
18
|
+
),
|
|
19
|
+
step_lock AS (
|
|
20
|
+
SELECT * FROM pgflow.step_states
|
|
21
|
+
WHERE pgflow.step_states.run_id = fail_task.run_id
|
|
22
|
+
AND pgflow.step_states.step_slug = fail_task.step_slug
|
|
23
|
+
FOR UPDATE
|
|
24
|
+
),
|
|
25
|
+
flow_info AS (
|
|
26
|
+
SELECT r.flow_slug
|
|
27
|
+
FROM pgflow.runs r
|
|
28
|
+
WHERE r.run_id = fail_task.run_id
|
|
29
|
+
),
|
|
30
|
+
config AS (
|
|
31
|
+
SELECT
|
|
32
|
+
COALESCE(s.opt_max_attempts, f.opt_max_attempts) AS opt_max_attempts,
|
|
33
|
+
COALESCE(s.opt_base_delay, f.opt_base_delay) AS opt_base_delay
|
|
34
|
+
FROM pgflow.steps s
|
|
35
|
+
JOIN pgflow.flows f ON f.flow_slug = s.flow_slug
|
|
36
|
+
JOIN flow_info fi ON fi.flow_slug = s.flow_slug
|
|
37
|
+
WHERE s.flow_slug = fi.flow_slug AND s.step_slug = fail_task.step_slug
|
|
38
|
+
),
|
|
39
|
+
|
|
40
|
+
fail_or_retry_task as (
|
|
41
|
+
UPDATE pgflow.step_tasks as task
|
|
42
|
+
SET
|
|
43
|
+
status = CASE
|
|
44
|
+
WHEN task.attempts_count < (SELECT opt_max_attempts FROM config) THEN 'queued'
|
|
45
|
+
ELSE 'failed'
|
|
46
|
+
END,
|
|
47
|
+
error_message = fail_task.error_message
|
|
48
|
+
WHERE task.run_id = fail_task.run_id
|
|
49
|
+
AND task.step_slug = fail_task.step_slug
|
|
50
|
+
AND task.task_index = fail_task.task_index
|
|
51
|
+
AND task.status = 'queued'
|
|
52
|
+
RETURNING *
|
|
53
|
+
),
|
|
54
|
+
maybe_fail_step AS (
|
|
55
|
+
UPDATE pgflow.step_states
|
|
56
|
+
SET
|
|
57
|
+
status = CASE
|
|
58
|
+
WHEN (select fail_or_retry_task.status from fail_or_retry_task) = 'failed' THEN 'failed'
|
|
59
|
+
ELSE pgflow.step_states.status
|
|
60
|
+
END
|
|
61
|
+
FROM fail_or_retry_task
|
|
62
|
+
WHERE pgflow.step_states.run_id = fail_task.run_id
|
|
63
|
+
AND pgflow.step_states.step_slug = fail_task.step_slug
|
|
64
|
+
RETURNING pgflow.step_states.*
|
|
65
|
+
)
|
|
66
|
+
UPDATE pgflow.runs
|
|
67
|
+
SET status = CASE
|
|
68
|
+
WHEN (select status from maybe_fail_step) = 'failed' THEN 'failed'
|
|
69
|
+
ELSE status
|
|
70
|
+
END
|
|
71
|
+
WHERE pgflow.runs.run_id = fail_task.run_id;
|
|
72
|
+
|
|
73
|
+
-- For queued tasks: delay the message for retry with exponential backoff
|
|
74
|
+
PERFORM (
|
|
75
|
+
WITH retry_config AS (
|
|
76
|
+
SELECT
|
|
77
|
+
COALESCE(s.opt_base_delay, f.opt_base_delay) AS base_delay
|
|
78
|
+
FROM pgflow.steps s
|
|
79
|
+
JOIN pgflow.flows f ON f.flow_slug = s.flow_slug
|
|
80
|
+
JOIN pgflow.runs r ON r.flow_slug = f.flow_slug
|
|
81
|
+
WHERE r.run_id = fail_task.run_id
|
|
82
|
+
AND s.step_slug = fail_task.step_slug
|
|
83
|
+
),
|
|
84
|
+
queued_tasks AS (
|
|
85
|
+
SELECT
|
|
86
|
+
r.flow_slug,
|
|
87
|
+
st.message_id,
|
|
88
|
+
pgflow.calculate_retry_delay((SELECT base_delay FROM retry_config), st.attempts_count) AS calculated_delay
|
|
89
|
+
FROM pgflow.step_tasks st
|
|
90
|
+
JOIN pgflow.runs r ON st.run_id = r.run_id
|
|
91
|
+
WHERE st.run_id = fail_task.run_id
|
|
92
|
+
AND st.step_slug = fail_task.step_slug
|
|
93
|
+
AND st.task_index = fail_task.task_index
|
|
94
|
+
AND st.status = 'queued'
|
|
95
|
+
)
|
|
96
|
+
SELECT pgmq.set_vt(qt.flow_slug, qt.message_id, qt.calculated_delay)
|
|
97
|
+
FROM queued_tasks qt
|
|
98
|
+
WHERE EXISTS (SELECT 1 FROM queued_tasks)
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
-- For failed tasks: archive the message
|
|
102
|
+
PERFORM (
|
|
103
|
+
WITH failed_tasks AS (
|
|
104
|
+
SELECT r.flow_slug, st.message_id
|
|
105
|
+
FROM pgflow.step_tasks st
|
|
106
|
+
JOIN pgflow.runs r ON st.run_id = r.run_id
|
|
107
|
+
WHERE st.run_id = fail_task.run_id
|
|
108
|
+
AND st.step_slug = fail_task.step_slug
|
|
109
|
+
AND st.task_index = fail_task.task_index
|
|
110
|
+
AND st.status = 'failed'
|
|
111
|
+
)
|
|
112
|
+
SELECT pgmq.archive(ft.flow_slug, ft.message_id)
|
|
113
|
+
FROM failed_tasks ft
|
|
114
|
+
WHERE EXISTS (SELECT 1 FROM failed_tasks)
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
return query select *
|
|
118
|
+
from pgflow.step_tasks st
|
|
119
|
+
where st.run_id = fail_task.run_id
|
|
120
|
+
and st.step_slug = fail_task.step_slug
|
|
121
|
+
and st.task_index = fail_task.task_index;
|
|
122
|
+
|
|
123
|
+
end;
|
|
124
|
+
$$;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
create extension if not exists pgmq version '1.4.4';
|
|
2
|
+
|
|
3
|
+
create schema if not exists edge_worker;
|
|
4
|
+
|
|
5
|
+
-------------------------------------------------------------------------------
|
|
6
|
+
-- Workers Table --------------------------------------------------------------
|
|
7
|
+
-------------------------------------------------------------------------------
|
|
8
|
+
create table if not exists edge_worker.workers (
|
|
9
|
+
worker_id uuid not null primary key,
|
|
10
|
+
queue_name text not null,
|
|
11
|
+
function_name text not null,
|
|
12
|
+
started_at timestamptz not null default now(),
|
|
13
|
+
stopped_at timestamptz,
|
|
14
|
+
last_heartbeat_at timestamptz not null default now()
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
--------------------------------------------------------------------------------
|
|
18
|
+
-- Read With Poll --------------------------------------------------------------
|
|
19
|
+
-- --
|
|
20
|
+
-- This is a backport of the pgmq.read_with_poll function from version 1.5.0 --
|
|
21
|
+
-- It is required because it fixes a bug with high CPU usage and Supabase --
|
|
22
|
+
-- is still using version 1.4.4. --
|
|
23
|
+
-- --
|
|
24
|
+
-- It is slightly modified (removed headers which are not available in 1.4.1) --
|
|
25
|
+
-- --
|
|
26
|
+
-- This will be removed once Supabase upgrades to 1.5.0 or higher. --
|
|
27
|
+
--------------------------------------------------------------------------------
|
|
28
|
+
create function edge_worker.read_with_poll(
|
|
29
|
+
queue_name text,
|
|
30
|
+
vt integer,
|
|
31
|
+
qty integer,
|
|
32
|
+
max_poll_seconds integer default 5,
|
|
33
|
+
poll_interval_ms integer default 100,
|
|
34
|
+
conditional jsonb default '{}'
|
|
35
|
+
)
|
|
36
|
+
returns setof pgmq.message_record as $$
|
|
37
|
+
DECLARE
|
|
38
|
+
r pgmq.message_record;
|
|
39
|
+
stop_at timestamp;
|
|
40
|
+
sql text;
|
|
41
|
+
qtable text := pgmq.format_table_name(queue_name, 'q');
|
|
42
|
+
BEGIN
|
|
43
|
+
stop_at := clock_timestamp() + make_interval(secs => max_poll_seconds);
|
|
44
|
+
LOOP
|
|
45
|
+
IF (SELECT clock_timestamp() >= stop_at) THEN
|
|
46
|
+
RETURN;
|
|
47
|
+
END IF;
|
|
48
|
+
|
|
49
|
+
sql := FORMAT(
|
|
50
|
+
$QUERY$
|
|
51
|
+
WITH cte AS
|
|
52
|
+
(
|
|
53
|
+
SELECT msg_id
|
|
54
|
+
FROM pgmq.%I
|
|
55
|
+
WHERE vt <= clock_timestamp() AND CASE
|
|
56
|
+
WHEN %L != '{}'::jsonb THEN (message @> %2$L)::integer
|
|
57
|
+
ELSE 1
|
|
58
|
+
END = 1
|
|
59
|
+
ORDER BY msg_id ASC
|
|
60
|
+
LIMIT $1
|
|
61
|
+
FOR UPDATE SKIP LOCKED
|
|
62
|
+
)
|
|
63
|
+
UPDATE pgmq.%I m
|
|
64
|
+
SET
|
|
65
|
+
vt = clock_timestamp() + %L,
|
|
66
|
+
read_ct = read_ct + 1
|
|
67
|
+
FROM cte
|
|
68
|
+
WHERE m.msg_id = cte.msg_id
|
|
69
|
+
RETURNING m.msg_id, m.read_ct, m.enqueued_at, m.vt, m.message;
|
|
70
|
+
$QUERY$,
|
|
71
|
+
qtable, conditional, qtable, make_interval(secs => vt)
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
FOR r IN
|
|
75
|
+
EXECUTE sql USING qty
|
|
76
|
+
LOOP
|
|
77
|
+
RETURN NEXT r;
|
|
78
|
+
END LOOP;
|
|
79
|
+
IF FOUND THEN
|
|
80
|
+
RETURN;
|
|
81
|
+
ELSE
|
|
82
|
+
PERFORM pg_sleep(poll_interval_ms::numeric / 1000);
|
|
83
|
+
END IF;
|
|
84
|
+
END LOOP;
|
|
85
|
+
END;
|
|
86
|
+
$$ language plpgsql;
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
create schema if not exists pgflow_tests;
|
|
2
|
+
|
|
3
|
+
--------------------------------------------------------------------------------
|
|
4
|
+
--------- reset_db - clears all tables and drops all queues --------------------
|
|
5
|
+
--------------------------------------------------------------------------------
|
|
6
|
+
create or replace function pgflow_tests.reset_db() returns void as $$
|
|
7
|
+
DELETE FROM pgflow.step_tasks;
|
|
8
|
+
DELETE FROM pgflow.step_states;
|
|
9
|
+
DELETE FROM pgflow.runs;
|
|
10
|
+
DELETE FROM pgflow.deps;
|
|
11
|
+
DELETE FROM pgflow.steps;
|
|
12
|
+
DELETE FROM pgflow.flows;
|
|
13
|
+
|
|
14
|
+
SELECT pgmq.drop_queue(queue_name) FROM pgmq.list_queues();
|
|
15
|
+
$$ language sql;
|
|
16
|
+
|
|
17
|
+
--------------------------------------------------------------------------------
|
|
18
|
+
--------- setup_flow - creates a predefined flow and adds steps to it ----------
|
|
19
|
+
--------------------------------------------------------------------------------
|
|
20
|
+
create or replace function pgflow_tests.setup_flow(
|
|
21
|
+
flow_slug text
|
|
22
|
+
) returns void as $$
|
|
23
|
+
begin
|
|
24
|
+
|
|
25
|
+
if flow_slug = 'sequential' then
|
|
26
|
+
PERFORM pgflow.create_flow('sequential', timeout => 1);
|
|
27
|
+
PERFORM pgflow.add_step('sequential', 'first');
|
|
28
|
+
PERFORM pgflow.add_step('sequential', 'second', ARRAY['first']);
|
|
29
|
+
PERFORM pgflow.add_step('sequential', 'last', ARRAY['second']);
|
|
30
|
+
elsif flow_slug = 'sequential_other' then
|
|
31
|
+
PERFORM pgflow.create_flow('other', timeout => 1);
|
|
32
|
+
PERFORM pgflow.add_step('other', 'first');
|
|
33
|
+
PERFORM pgflow.add_step('other', 'second', ARRAY['first']);
|
|
34
|
+
PERFORM pgflow.add_step('other', 'last', ARRAY['second']);
|
|
35
|
+
elsif flow_slug = 'two_roots' then
|
|
36
|
+
PERFORM pgflow.create_flow('two_roots', timeout => 1);
|
|
37
|
+
PERFORM pgflow.add_step('two_roots', 'root_a');
|
|
38
|
+
PERFORM pgflow.add_step('two_roots', 'root_b');
|
|
39
|
+
PERFORM pgflow.add_step('two_roots', 'last', ARRAY['root_a', 'root_b']);
|
|
40
|
+
elsif flow_slug = 'two_roots_left_right' then
|
|
41
|
+
PERFORM pgflow.create_flow('two_roots_left_right', timeout => 1);
|
|
42
|
+
PERFORM pgflow.add_step('two_roots_left_right', 'connected_root');
|
|
43
|
+
PERFORM pgflow.add_step('two_roots_left_right', 'disconnected_root');
|
|
44
|
+
PERFORM pgflow.add_step('two_roots_left_right', 'left', ARRAY['connected_root']);
|
|
45
|
+
PERFORM pgflow.add_step('two_roots_left_right', 'right', ARRAY['connected_root']);
|
|
46
|
+
else
|
|
47
|
+
RAISE EXCEPTION 'Unknown test flow: %', flow_slug;
|
|
48
|
+
end if;
|
|
49
|
+
|
|
50
|
+
end;
|
|
51
|
+
$$ language plpgsql;
|
|
52
|
+
|
|
53
|
+
--------------------------------------------------------------------------------
|
|
54
|
+
------- poll_and_fail - polls for a task and fails it immediately --------------
|
|
55
|
+
--------------------------------------------------------------------------------
|
|
56
|
+
create or replace function pgflow_tests.poll_and_fail(
|
|
57
|
+
flow_slug text,
|
|
58
|
+
vt integer default 1,
|
|
59
|
+
qty integer default 1
|
|
60
|
+
) returns setof pgflow.step_tasks as $$
|
|
61
|
+
-- Poll for a task and complete it in one step
|
|
62
|
+
WITH task AS (
|
|
63
|
+
SELECT * FROM pgflow.poll_for_tasks(flow_slug, vt, qty) LIMIT 1
|
|
64
|
+
)
|
|
65
|
+
SELECT pgflow.fail_task(
|
|
66
|
+
(SELECT run_id FROM task),
|
|
67
|
+
(SELECT step_slug FROM task),
|
|
68
|
+
0,
|
|
69
|
+
concat(task.step_slug, ' FAILED')
|
|
70
|
+
)
|
|
71
|
+
FROM task;
|
|
72
|
+
$$ language sql;
|
|
73
|
+
|
|
74
|
+
--------------------------------------------------------------------------------
|
|
75
|
+
------- poll_and_complete - polls for a task and completes it immediately ------
|
|
76
|
+
--------------------------------------------------------------------------------
|
|
77
|
+
create or replace function pgflow_tests.poll_and_complete(
|
|
78
|
+
flow_slug text,
|
|
79
|
+
vt integer default 1,
|
|
80
|
+
qty integer default 1
|
|
81
|
+
) returns setof pgflow.step_tasks as $$
|
|
82
|
+
-- Poll for a task and complete it in one step
|
|
83
|
+
WITH task AS (
|
|
84
|
+
SELECT * FROM pgflow.poll_for_tasks(flow_slug, vt, qty) LIMIT 1
|
|
85
|
+
)
|
|
86
|
+
SELECT pgflow.complete_task(
|
|
87
|
+
(SELECT run_id FROM task),
|
|
88
|
+
(SELECT step_slug FROM task),
|
|
89
|
+
0,
|
|
90
|
+
jsonb_build_object('input', task.input)
|
|
91
|
+
)
|
|
92
|
+
FROM task;
|
|
93
|
+
$$ language sql;
|
|
94
|
+
|
|
95
|
+
--------------------------------------------------------------------------------
|
|
96
|
+
------- message_timing - returns messages with added vt_seconds int ------------
|
|
97
|
+
--------------------------------------------------------------------------------
|
|
98
|
+
create or replace function pgflow_tests.message_timing(step_slug text, queue_name text)
|
|
99
|
+
returns table (
|
|
100
|
+
msg_id bigint,
|
|
101
|
+
read_ct int,
|
|
102
|
+
enqueued_at timestamptz,
|
|
103
|
+
vt timestamptz,
|
|
104
|
+
message jsonb,
|
|
105
|
+
vt_seconds int
|
|
106
|
+
)
|
|
107
|
+
language plpgsql
|
|
108
|
+
as $$
|
|
109
|
+
DECLARE
|
|
110
|
+
qtable TEXT;
|
|
111
|
+
query TEXT;
|
|
112
|
+
BEGIN
|
|
113
|
+
qtable := pgmq.format_table_name(queue_name, 'q');
|
|
114
|
+
|
|
115
|
+
query := format('
|
|
116
|
+
SELECT
|
|
117
|
+
q.msg_id,
|
|
118
|
+
q.read_ct,
|
|
119
|
+
q.enqueued_at,
|
|
120
|
+
q.vt,
|
|
121
|
+
q.message,
|
|
122
|
+
extract(epoch from (q.vt - q.enqueued_at))::int as vt_seconds
|
|
123
|
+
FROM pgmq.%s q
|
|
124
|
+
JOIN pgflow.step_tasks st ON st.message_id = q.msg_id
|
|
125
|
+
WHERE st.step_slug = $1', qtable);
|
|
126
|
+
|
|
127
|
+
RETURN QUERY EXECUTE query USING step_slug;
|
|
128
|
+
END;
|
|
129
|
+
$$;
|
|
130
|
+
|
|
131
|
+
--------------------------------------------------------------------------------
|
|
132
|
+
------- reset_message_visibility -----------------------------------------------
|
|
133
|
+
--------------------------------------------------------------------------------
|
|
134
|
+
--
|
|
135
|
+
-- Makes all hidden messages in a queue immediately visible by setting their
|
|
136
|
+
-- visibility time (vt) to the current timestamp.
|
|
137
|
+
--
|
|
138
|
+
-- This is a test utility that allows testing retry logic without waiting for
|
|
139
|
+
-- actual delays to expire. It directly modifies the pgmq queue table.
|
|
140
|
+
--
|
|
141
|
+
-- @param queue_name The name of the queue to modify
|
|
142
|
+
-- @return The number of messages that were made visible
|
|
143
|
+
--
|
|
144
|
+
create or replace function pgflow_tests.reset_message_visibility(
|
|
145
|
+
queue_name text
|
|
146
|
+
) returns integer as $$
|
|
147
|
+
DECLARE
|
|
148
|
+
qtable TEXT;
|
|
149
|
+
query TEXT;
|
|
150
|
+
updated_count INTEGER;
|
|
151
|
+
BEGIN
|
|
152
|
+
-- Get the formatted table name for the queue
|
|
153
|
+
qtable := pgmq.format_table_name(queue_name, 'q');
|
|
154
|
+
|
|
155
|
+
-- Construct and execute the query to update all messages' visibility time
|
|
156
|
+
query := format('
|
|
157
|
+
UPDATE pgmq.%s
|
|
158
|
+
SET vt = clock_timestamp()
|
|
159
|
+
WHERE vt > clock_timestamp()
|
|
160
|
+
RETURNING 1', qtable);
|
|
161
|
+
|
|
162
|
+
-- Execute the query and count the number of updated rows
|
|
163
|
+
EXECUTE query INTO updated_count;
|
|
164
|
+
|
|
165
|
+
-- Return the number of messages that were made visible
|
|
166
|
+
RETURN COALESCE(updated_count, 0);
|
|
167
|
+
END;
|
|
168
|
+
$$ language plpgsql;
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
--------------------------------------------------------------------------------
|
|
172
|
+
------- assert_retry_delay -----------------------------------------------------
|
|
173
|
+
--------------------------------------------------------------------------------
|
|
174
|
+
--
|
|
175
|
+
-- Asserts that the calculated retry delay matches the expected value.
|
|
176
|
+
--
|
|
177
|
+
-- @param step_slug The slug of the step to check
|
|
178
|
+
-- @param queue_name The name of the queue to check
|
|
179
|
+
-- @param expected_delay The expected delay value
|
|
180
|
+
-- @param description A description of the test case
|
|
181
|
+
-- @return TEXT result from the is() function
|
|
182
|
+
--
|
|
183
|
+
create or replace function pgflow_tests.assert_retry_delay(
|
|
184
|
+
queue_name text,
|
|
185
|
+
step_slug text,
|
|
186
|
+
expected_delay integer,
|
|
187
|
+
description text
|
|
188
|
+
) returns text as $$
|
|
189
|
+
DECLARE
|
|
190
|
+
actual_delay INTEGER;
|
|
191
|
+
BEGIN
|
|
192
|
+
SELECT vt_seconds INTO actual_delay
|
|
193
|
+
FROM pgflow_tests.message_timing(step_slug, queue_name)
|
|
194
|
+
LIMIT 1;
|
|
195
|
+
|
|
196
|
+
RETURN is(
|
|
197
|
+
actual_delay,
|
|
198
|
+
expected_delay,
|
|
199
|
+
description
|
|
200
|
+
);
|
|
201
|
+
END;
|
|
202
|
+
$$ language plpgsql;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
begin;
|
|
2
|
+
select plan(3);
|
|
3
|
+
select pgflow_tests.reset_db();
|
|
4
|
+
|
|
5
|
+
-- Setup
|
|
6
|
+
select pgflow.create_flow('test_flow');
|
|
7
|
+
|
|
8
|
+
-- Test
|
|
9
|
+
select pgflow.add_step('test_flow', 'first_step');
|
|
10
|
+
select results_eq(
|
|
11
|
+
$$ SELECT step_slug FROM pgflow.steps WHERE flow_slug = 'test_flow' $$,
|
|
12
|
+
array['first_step']::text [],
|
|
13
|
+
'Step should be added to the steps table'
|
|
14
|
+
);
|
|
15
|
+
select is_empty(
|
|
16
|
+
$$ SELECT * FROM pgflow.deps WHERE flow_slug = 'test_flow' $$,
|
|
17
|
+
'No dependencies should be added for step with no dependencies'
|
|
18
|
+
);
|
|
19
|
+
select is(
|
|
20
|
+
(
|
|
21
|
+
select deps_count::int from pgflow.steps
|
|
22
|
+
where flow_slug = 'test_flow'
|
|
23
|
+
),
|
|
24
|
+
0::int,
|
|
25
|
+
'deps_count should be 0 because there are no dependencies'
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
select * from finish();
|
|
29
|
+
rollback;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
begin;
|
|
2
|
+
select plan(1);
|
|
3
|
+
select pgflow_tests.reset_db();
|
|
4
|
+
|
|
5
|
+
-- Setup
|
|
6
|
+
select pgflow.create_flow('test_flow');
|
|
7
|
+
select pgflow.add_step('test_flow', 'first_step');
|
|
8
|
+
select pgflow.add_step('test_flow', 'second_step', array['first_step']);
|
|
9
|
+
select pgflow.add_step('test_flow', 'third_step', array['second_step']);
|
|
10
|
+
select
|
|
11
|
+
pgflow.add_step('test_flow', 'fourth_step', array['second_step', 'third_step']);
|
|
12
|
+
|
|
13
|
+
-- Test
|
|
14
|
+
select throws_ok(
|
|
15
|
+
$$ SELECT pgflow.add_step('test_flow', 'circular_step', ARRAY['fourth_step', 'circular_step']) $$,
|
|
16
|
+
'new row for relation "deps" violates check constraint "deps_check"',
|
|
17
|
+
'Should not allow self-depending steps'
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
select * from finish();
|
|
21
|
+
rollback;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
begin;
|
|
2
|
+
select plan(1);
|
|
3
|
+
select pgflow_tests.reset_db();
|
|
4
|
+
|
|
5
|
+
-- Setup
|
|
6
|
+
select pgflow.create_flow('test_flow');
|
|
7
|
+
select pgflow.add_step('test_flow', 'first_step');
|
|
8
|
+
select pgflow.create_flow('another_flow');
|
|
9
|
+
|
|
10
|
+
-- Test
|
|
11
|
+
select pgflow.add_step('another_flow', 'first_step');
|
|
12
|
+
select pgflow.add_step('another_flow', 'another_step', array['first_step']);
|
|
13
|
+
select set_eq(
|
|
14
|
+
$$
|
|
15
|
+
SELECT flow_slug, step_slug
|
|
16
|
+
FROM pgflow.steps WHERE flow_slug = 'another_flow'
|
|
17
|
+
$$,
|
|
18
|
+
$$ VALUES
|
|
19
|
+
('another_flow', 'another_step'),
|
|
20
|
+
('another_flow', 'first_step')
|
|
21
|
+
$$,
|
|
22
|
+
'Steps in second flow should be isolated from first flow'
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
select * from finish();
|
|
26
|
+
rollback;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
begin;
|
|
2
|
+
select plan(1);
|
|
3
|
+
select pgflow_tests.reset_db();
|
|
4
|
+
|
|
5
|
+
-- Setup
|
|
6
|
+
select pgflow.create_flow('test_flow');
|
|
7
|
+
select pgflow.add_step('test_flow', 'first_step');
|
|
8
|
+
|
|
9
|
+
-- Test
|
|
10
|
+
select pgflow.add_step('test_flow', 'first_step');
|
|
11
|
+
select results_eq(
|
|
12
|
+
$$
|
|
13
|
+
SELECT count(*)::int FROM pgflow.steps WHERE flow_slug = 'test_flow' AND step_slug = 'first_step'
|
|
14
|
+
$$,
|
|
15
|
+
array[1]::int [],
|
|
16
|
+
'Calling add_step again for same step does not create a duplicate'
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
select * from finish();
|
|
20
|
+
rollback;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
begin;
|
|
2
|
+
select plan(1);
|
|
3
|
+
select pgflow_tests.reset_db();
|
|
4
|
+
|
|
5
|
+
-- Setup
|
|
6
|
+
select pgflow.create_flow('test_flow');
|
|
7
|
+
|
|
8
|
+
-- Test
|
|
9
|
+
select throws_ok(
|
|
10
|
+
$$ SELECT pgflow.add_step('test_flow', '1invalid-slug') $$,
|
|
11
|
+
'new row for relation "steps" violates check constraint "steps_step_slug_check"',
|
|
12
|
+
'Should detect and prevent invalid step slug'
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
select * from finish();
|
|
16
|
+
rollback;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
begin;
|
|
2
|
+
select plan(1);
|
|
3
|
+
select pgflow_tests.reset_db();
|
|
4
|
+
|
|
5
|
+
-- Setup
|
|
6
|
+
select pgflow.create_flow('test_flow');
|
|
7
|
+
|
|
8
|
+
-- Test
|
|
9
|
+
select throws_ok(
|
|
10
|
+
$$ SELECT pgflow.add_step('test_flow', 'invalid_dep_step', ARRAY['nonexistent_step']) $$,
|
|
11
|
+
'insert or update on table "deps" violates foreign key constraint "deps_flow_slug_dep_slug_fkey"',
|
|
12
|
+
'Should detect and prevent dependency on non-existent step'
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
select * from finish();
|
|
16
|
+
rollback;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
begin;
|
|
2
|
+
select plan(1);
|
|
3
|
+
select pgflow_tests.reset_db();
|
|
4
|
+
|
|
5
|
+
-- Test
|
|
6
|
+
select throws_ok(
|
|
7
|
+
$$ SELECT pgflow.add_step('nonexistent_flow', 'some_step') $$,
|
|
8
|
+
'insert or update on table "steps" violates foreign key constraint "steps_flow_slug_fkey"',
|
|
9
|
+
'Should not allow adding step to non-existent flow'
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
select * from finish();
|
|
13
|
+
rollback;
|