fix(core): guarantee task visibility extension in start_tasks() - #667
Conversation
start_tasks() invoked pgflow.set_vt_batch() inside an unreferenced SELECT CTE, so PostgreSQL could skip the call and leave a claimed task with only the short initial PGMQ read visibility (#656). The final statement now consumes a guard over the renamed visibility_reset CTE: execution is structurally required, and when set_vt_batch() updates fewer messages than were claimed the statement fails ("start_tasks(): visibility updated N of M claimed messages"), rolling back the task transitions and attempt increments and returning nothing. Effective delay stays coalesce(step.opt_timeout, flow.opt_timeout) + 2, matching the #664 stalled-recovery threshold. A repeatedly visible message whose task is already started claims nothing and consumes no attempt (benign duplicate). Consolidates the three unreleased Core migrations on main (terminalize_skipped_tasks #638/#649, failed_run_terminalization #645/#663, effective_step_timeout #621/#664) plus the dev temp migration into one Atlas-generated task_lifecycle_hardening migration against the 0.15.0 baseline. Both migration-only data repairs are preserved in order: active tasks under skipped steps become skipped before remaining active tasks under failed runs become cancelled. A 0.15.0 upgrade fixture (supabase/upgrade_fixture + scripts/run-upgrade-fixture) applies the consolidated migration and asserts both repairs plus runtime visibility, wired into the core test:pgtap target. Adds pgTAP coverage for all seven issue #656 test bullets: effective-timeout visibility (shorter/longer/null fallback), rollback on total and partial visibility failure, benign duplicate batches, and mixed-batch atomicity.
🦋 Changeset detectedLatest commit: 6ab5d5b The changes in this PR will be included in the next version bump. This PR includes changesets to release 5 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
View your CI Pipeline Execution ↗ for commit 6ab5d5b
💡 Verify your cache is correct by running tasks in a sandbox. Read docs ↗ ☁️ Nx Cloud last updated this comment at |
Use clock_timestamp() in visibility-delay pgTAP assertions because set_vt_batch() uses wall-clock time while now() stays fixed at transaction start. Recheck attempts_count after a repeatedly visible started message so benign duplicates remain pinned end to end. Remove the unsupported sqruff --force option from the core fix-sql target. sqruff 0.39.0 rejects that flag, which blocked the schema-first migration workflow required for issue #656.
jumski
left a comment
There was a problem hiding this comment.
See inline comments.
| -- means a visibility extension did not run (#656). SQL functions cannot | ||
| -- RAISE, so the mismatch branch casts a descriptive message to int4: | ||
| -- the cast error fails the whole statement, rolling back the task | ||
| -- transition and attempt increment, and returns nothing. |
There was a problem hiding this comment.
is it really the best approach? can't we use "or raise"?
| 'start_tasks(): visibility updated %s of %s claimed messages', | ||
| updated.updated_count, | ||
| claimed.claimed_count | ||
| )::int4 |
There was a problem hiding this comment.
ok i get it now, the error message will surface as it will be casted to int4: 'cannot cast to int4' i think it is acceptable
at what circumstance it can happen?
| left join deps_outputs dep_out on | ||
| dep_out.run_id = st.run_id and | ||
| dep_out.step_slug = st.step_slug | ||
| cross join _vr |
There was a problem hiding this comment.
are those additional cross joins optimal or impacting performance much?
| psql_in < "$consolidated" | ||
|
|
||
| psql_in < supabase/upgrade_fixture/assertions.sql | ||
| echo "upgrade fixture: PASS" |
There was a problem hiding this comment.
why is this whole file even needed?
| (select ids from vispartial_msgs), | ||
| '11111111-1111-1111-1111-111111111111'::uuid | ||
| ) $$, | ||
| 'invalid input syntax for type integer: "start_tasks(): visibility updated 1 of 2 claimed messages"', |
There was a problem hiding this comment.
im thinking - is it optimal to fail whole batch if one of the messages is missing? just curious, convince me it is
| end if; | ||
| end $$; | ||
|
|
||
| select 'PASS: 0.15.0 upgrade fixture (repairs + runtime)' as result; |
| @@ -0,0 +1,88 @@ | |||
| -- Upgrade fixture seed: stale data both consolidated-migration repairs target. | |||
🚀 Production Deployment: Website✅ Successfully deployed to production! 🔗 Production URL: https://pgflow.dev 📝 Details:
Deployed at: 2026-09-04T22:10:06+02:00 |

Summary
start_tasks()invokedpgflow.set_vt_batch()inside an unreferencedSELECTCTE, so PostgreSQL could skip the call and leave a claimed task with only the short initial PGMQ read visibility. A message then became visible while itsstep_tasksrow stayedstarted, and workers repeatedly read it as a task that never ran.visibility_resetCTE, making the visibility update structurally required. Whenset_vt_batch()updates fewer messages than were claimed, the statement fails withstart_tasks(): visibility updated N of M claimed messages, rolling back the task transitions and attempt increments and returning nothing.coalesce(step.opt_timeout, flow.opt_timeout) + 2, matching the fix(core): use effective step timeout for stalled tasks #664 stalled-recovery threshold. A repeatedly visible message whose task is alreadystartedclaims nothing and consumes no attempt — a benign duplicate, per Add private per-step queues with typed step workers #651/Add explicit shared queues with multi-flow dispatch #652 direction.main(terminalize_skipped_tasksSkip paths archive sibling messages but never terminalize sibling step_tasks rows, leaving status='started' tasks on completed runs #638/Skip paths archive sibling messages but never terminalize sibling step_tasks rows, leaving status='started' tasks on completed runs #649,failed_run_terminalizationFailed runs leave unfinished sibling tasks queued or started instead of cancelled #645/fix(core): terminalize unfinished tasks when runs fail #663,effective_step_timeoutrequeue_stalled_tasks() uses flow timeout instead of effective step timeout #621/fix(core): use effective step timeout for stalled tasks #664) plus the development temp migration into one Atlas-generatedtask_lifecycle_hardeningmigration against the0.15.0baseline (Release 0.15.1 — task lifecycle hardening #666). Both migration-only data repairs are preserved in order: active tasks under skipped steps becomeskippedbefore remaining active tasks under failed runs becomecancelled.0.15.0upgrade fixture (supabase/upgrade_fixture/+scripts/run-upgrade-fixture): starts from the0.15.0baseline, applies the consolidated migration, and asserts both historical repairs plus final runtime visibility behavior. Wired into thecoretest:pgtapnx target so CI executes it.clock_timestamp()to matchset_vt_batch(), and repeated duplicates recheck that the attempt count stays unchanged.--forceoption so the requiredpnpm nx fix-sql coremigration step runs normally.@pgflow/corepatch changeset.Checks
pnpm nx verify-migrations core— pass (incl. schemas-synced)pnpm nx gen-types core+verify-gen-types core --skip-nx-cache— passpnpm nx fix-sql core— pass (37 files, nothing to fix)pnpm nx test:pgtap core --skip-nx-cache— pass (288 files, 1352 tests; focused start_tasks suites 26/26;upgrade fixture: PASS)pnpm nx affected --target=prepush --base=origin/main --head=HEAD— 28/28 successfulatlas migrate validate— pass;atlas.sumre-hashed after the manual repair editsCloses #656