Repository navigation
feat(workflow): end hooks B — afterExecution runs, kill stops it, sweeps read execution_is_idle - #761
Merged
Merged
Conversation
advance gains the post-terminal phase (spec D8, Kestra ExecutorService.handleAfterExecution): once the execution's last state is terminal, after_execution::drive walks the flow's afterExecution: list one task at a time under no parent, and never emits SetExecutionState, ScheduleRetry or PauseRun (debug_assert + corpus test). KILLING keeps advance_killing, so the phase starts once the execution is KILLED; a named B3 hook marks where the kill of the phase (Q3) will stop it. - Plan::after_execution, flattened last and then drained (D3): the A1 prefix fixture is unchanged. - executor::terminated / drives_after (D10). - D11: retry::policy_for answers None for a run under afterExecution:, so a flowable in the phase never waits for a retry nobody schedules. Oracle: 8 entries bank (301/60/60/0 -> 301/52/52/0); derived constants moved with dated notes; handler-surface regenerated (ceiling 273 -> 281). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ry there is warned Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…s child once it is terminated The settle tail reports a terminal to a Subflow or Loop parent when executor::terminated first becomes true (spec D10), so a child whose afterExecution: is still running keeps its parent waiting. render_updates also runs while the post-terminal phase does, so a SetVariables or Labels task inside afterExecution: is computed. Flow-trigger events stay per execution state change. New DB tests in tests/end_hooks.rs. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…tion's state alone A kill of a terminal execution whose afterExecution: still has a live run is accepted (user answer to Q3, 2026-10-27; a security divergence from Kestra, which answers 409): kill_phase kills the phase's live runs through the existing kill path, the cascade reaches only the phase's own children, and the execution's state, history and ended_at are not written. advance then closes the phase and creates no further after-task; terminated holds once every run of a killed phase is terminal, so a parent hears the child. A terminal execution with nothing of its phase live is still a 409. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… a cascade stops a terminal child's afterExecution A kill of a terminal execution whose only live after-run is a WorkingDirectory block waiting on its prepare or finish claim writes nothing and answers 409 kill_not_applied instead of an accepted kill. An ancestor's kill cascade now reaches a terminal descendant whose afterExecution: still has a live run and kills that phase only, leaving the descendant's state, history and ended_at alone (a recorded divergence from Kestra, which skips terminal children). DESCENDANTS pairs each phase run index with its own link. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…afterExecution Loop, Subflow and WorkingDirectory run to the end inside afterExecution. LoopUntil is kept as an ignored test: it waits between iterations with a PauseRun, which the post-terminal phase refuses (panic at after_execution.rs:68). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A LoopUntil (and its WaitFor alias) waits between iterations with PauseRun, which the post-terminal phase cannot apply; it is refused at start with unsupported_after_execution_task, naming roadmap package #9. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…minal (roadmap Rule 2) Migration 0210 adds workflow.execution_is_idle(id): terminal AND no task run live (spec D13, section 6). A terminal execution can still run work (afterExecution:, an Exit sibling), so the readers that meant "nothing of this execution runs" now read idle: - run_liveness: a superseded container is judged only once its execution is idle, with the retention measured from the last run's end; - the _wd sweep also requires the block itself to be terminal; - the storage pruner's protected set and late-dependent check read NOT execution_is_idle, and its retention runs from the last run's end; - the logs route's `complete` waits for idle and the settle lag after the last run's end. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… Exit-sibling leak The superseded-container clock, the pruner's retention clock and the logs route's settle clock take each run's end as COALESCE(ended_at, updated_at), as run_liveness's own terminal rule does: a terminal run written with no ended_at ended no earlier than its last write. A new test measures which Exit shapes leave a main-list run live for good (a CREATED leaf never started, a flowable never closed) and so keep their execution from ever being idle; the same rows stay on the commit before package #2. The prose about a LoopUntil inside afterExecution (refused since ff39653c) now names the live case, a LoopUntil beside an Exit. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…on runs Terminal execution with a live run (an afterExecution run) stays live for AFTER_RUN_FOLLOW_MS (30 min) after ended_at: polling, topology, Gantt and Kill follow it. Kill of such an execution says the afterExecution tasks are being stopped; a 409 kill_not_applied is a retryable message. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…rExecution runs ChildExecutions polls while executionLive, so a child an after-run starts appears; the Gantt clock stops once the follow window has passed; Kill reuses the frame's clock. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The end-hooks spec gets a #2b status line and dated as-built notes under D9 (superseded by Q3: a kill stops live afterExecution runs, 409 execution_terminated without one, 409 kill_not_applied when it cannot apply yet, the cascade reaches a terminal child's phase), D10, D11, D12 (LoopUntil/WaitFor refused too), D15, 4.2, 5, 6 (with the Exit-sibling leak and protected_set cost debts), 7.1 (301/52/52/0, all 8 banked), 8, 9, 10, 11 and the answers, plus a #2b deploy note. The roadmap marks row #2 done with measured banks, records both kill divergences in 3.1 as built, and adds the Exit-in-afterExecution refusal and the Exit-sibling stuck run to row #20. The kill-and-exit spec, the debt spec 4.7, the programme spec 9 and the plan's B8 docs list get dated notes. The oracle README gains the #2b as-built section; the ninth MISMATCH_ON_ORDER entry is verified as valids/after-execution. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…haviour The kill route answers 202 for a terminal execution whose afterExecution runs are live, 409 kill_not_applied while a block claim is in flight, and 409 execution_terminated otherwise. Log complete now means idle: terminal and every run terminal. OpenAPI and the generated client regenerated. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… the last run's end Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The log follower stops once the execution has been terminal past AFTER_RUN_FOLLOW_MS even when complete stays false; the kill dialog for an ended execution says it stops the afterExecution tasks. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ed execution A flow published before D12 can hold Pause, Exit or LoopUntil/WaitFor in afterExecution:. Executions running at deploy and CREATE_NEW_EXECUTION replacements are planned with plan_stored, which skips the start-time refusal, and the phase guard was only a debug_assert: in release an Exit rewrote the ended execution, and a Pause/LoopUntil left an after-run PAUSED for ever. - spec::validate::after_execution_refusal: B2's predicate, extracted; one text for the start refusal and the stored plan. - Plan::after_execution_refused (no index moves; A1 fixture untouched). - after_execution::drive, layer 1: a refused run is FAILED before it starts; the worker writes the refusal as its message. - Layer 2, release-safe: a batch that would write the execution (SetExecutionState, PauseRun, ScheduleRetry) is dropped and the run it was about FAILED. - Deploy note in the EN spec and its VI mirror. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Layer 2 of the afterExecution guard kept a named run that was already terminal, so it could drop a batch with no replacement and stall the phase. It now keeps only runs that can still fail, falls back to the live leaves of the phase, and with no live phase run returns the batch with its execution writes stripped, so the phase moves on and terminated becomes true. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Package #2b of the Kestra-parity roadmap:
afterExecution:now runs, after the execution has ended (stacked on #759, which landedfinally:).advanceruns theafterExecution:list once the execution is terminal (and afterKILLED). The execution's own state, history andended_atnever change; no retry (D11), no pause (D12). Plan indexes are append-only (the frozen layout fixture is untouched).Pause,Exit,LoopUntil/WaitForinsideafterExecution:are refused at start (naming Phase 3b-1 — Query Engine (async execution, REST polling) #9 / feat(web): Box plot + Sankey + stack totals — finish chart clean-room (17 types) #20);retry:there is warned at save/publish (Phase 3c-3 + 3c-3.2 — Charts: Table, Pivot, and multi-dimensional Pivot #13). A flow stored before this deploy that carries one of those kinds cannot write the ended execution: its after-run is FAILED with the refusal text, and the phase never passes a command that writes the execution.SetVariables/Labels); aSubflowparent hears its child only once the child is terminated (after its after-tasks); flow-trigger events are still written once, at the terminal state.execution_terminated; while a rootWorkingDirectoryblock claim is in flight it is a retryable 409kill_not_applied. A parent's kill cascade also stops a terminal child's after-phase (recorded divergence from Kestra, which skips terminal descendants).workflow.execution_is_idle(id)(terminal and every run terminal) replaces "terminal" in the superseded-container liveness, the pruner /protected_set, andlog_status.complete. The_wdsweep keeps reading the block's own state. Additive, one function.executionLive), bounded to 30 minutes after the end; the Kill button and its copy handle a phase kill andkill_not_applied; the log follower is bounded without cutting a full page.after-execution-error,-finally,-flowable,flow-concurrency-queue-after-execution,flow-concurrency-queue-killed-after-execution,restart-with-after-execution,sleep-long,sleep-long-after-execution-flowable).valids/after-execution(flowoutputs:, Phase 2a — Iceberg REST Catalog (egret-storage, egret-iceberg-ext, egret-catalog) #6) andvalids/pause-errors-finally-after-execution(Phase 3b-1 — Query Engine (async execution, REST polling) #9) stay baselined.Known, measured, documented (not fixed here)
A non-KILLED
Exitbeside a flowable sibling leaves a main-list run non-terminal for ever (pre-existing;tests/exit_sibling_idle.rspins it). Effect: such an execution is never "idle", so the pruner keeps its family,protected_setpins its readable set, and its logcompletenever turns true. Nothing is lost; the UI polling is bounded. Recorded in spec §6 and roadmap row #20 with the recommended fix (a terminal execution keeps closing main-list runs).Deploy notes
CREATE OR REPLACE FUNCTION; apply before the new workers start. Already-published flows withPause/Exit/LoopUntilinsideafterExecution:are now refused at every new start (schedules included); in-flight or stored ones fail that after-run instead of running it.protected_setcosts ~4 s per 100k executions (was 0.12 s), hourly; debt row with the cheaper form is in the debt spec.Test plan
cargo test -p egret-workflow: 74 binaries ok; only the 7sql_starrocks_livetests fail (they need a StarRocks engine)cargo test -p egret-config -p egret-server(env allow-list, OpenAPI + generated-client drift)typecheck,lint, vitestworkflows(76 files, 1238 tests)Docs: EN/VI done in-repo;
egret-data/docsfollow-up (Kill button on an ended execution,kill_not_applied, refusals, 30-minute follow) tracked separately.🤖 Generated with Claude Code