Skip to content

Recover stuck graphs by continuing queued next_nodes when no interrupt exists - #12

Open
MikaAK wants to merge 1 commit into
freshaengineering:mainfrom
MikaAK:fix/resume-stuck-at-next-nodes
Open

Recover stuck graphs by continuing queued next_nodes when no interrupt exists#12
MikaAK wants to merge 1 commit into
freshaengineering:mainfrom
MikaAK:fix/resume-stuck-at-next-nodes

Conversation

@MikaAK

@MikaAK MikaAK commented Apr 27, 2026

Copy link
Copy Markdown

Summary

When a graph stops mid-execution at a non-interrupt node — for example, the calling process crashes after a checkpoint is written but before the queued node finishes — the latest checkpoint has next_nodes populated but pending_interrupts: nil. Subsequent calls to LangEx.invoke(graph, %Command{resume: _}, ...) previously returned {:error, :no_pending_interrupt} and left the graph permanently stuck with no automatic recovery path.

This PR adds a fallback in resume_from_checkpoint: when no pending interrupt is found but the latest checkpoint has runnable next_nodes (excluding :__end__), continue execution from those nodes. The resume value is discarded since there is no interrupt to feed it to — callers using a dummy resume_val as a "nudge" trigger (e.g. cron-driven re-observation of external state) get automatic recovery without having to detect the stuck state themselves.

Pregel.run_graph/3 gains a new clause that handles next_nodes-driven start as a peer to the existing :__start__ and resume paths.

The change is strictly additive: previously-erroring no-pending-interrupt cases either now recover (when next_nodes is non-empty) or still error.

How this stuck state happens in practice

Real-world reproduction: a Pregel super-step writes a checkpoint with next_nodes: [:create_pr], then the worker process is killed (crash, code reload, deploy) before :create_pr can execute. The next cron-driven nudge call sees no pending interrupt anywhere in the last N checkpoints and bails out. The graph is dead.

After this PR: the same nudge call notices the queued non-interrupt nodes and resumes execution from them.

Test plan

  • New test file test/lang_ex/checkpoint/resume_stuck_at_next_nodes_test.exs with three cases:
    • resume continues from queued next_nodes when no pending interrupt exists anywhere (was failing, now passing)
    • still returns {:error, :no_pending_interrupt} when next_nodes is empty
    • still returns {:error, :no_pending_interrupt} when next_nodes only has :__end__
  • Full suite green: 146 tests, 0 failures
  • mix format --check-formatted clean

…t exists

When a graph stops mid-execution at a non-interrupt node (e.g. the
calling process crashes after a checkpoint is written but before the
queued node finishes), the latest checkpoint has next_nodes populated
but pending_interrupts: nil. Subsequent calls to LangEx.invoke with
%Command{resume: _} previously returned {:error, :no_pending_interrupt}
and left the graph permanently stuck.

This change adds a fallback in resume_from_checkpoint: when no pending
interrupt is found anywhere but the latest checkpoint has runnable
next_nodes (excluding :__end__), continue execution from those nodes.
The resume value is discarded since there is no interrupt to feed it
to — callers using a dummy resume_val purely as a "nudge" trigger
(e.g. cron-driven re-observation of external state) get automatic
recovery without needing to detect the stuck state themselves.

Pregel.run_graph/3 gains a new clause that handles next_nodes-driven
start as a peer to the existing :__start__ and resume paths.
Strictly additive: previously-erroring no-pending-interrupt cases
either now recover (when next_nodes is non-empty) or still error.
@twist900

twist900 commented Jun 4, 2026

Copy link
Copy Markdown
Collaborator

@claude review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants