Skip to content

fix(executor): let a branch loop back and re-run an earlier node - #2184

Open
joelorzet wants to merge 1 commit into
stagingfrom
fix/workflow-loop-back-edges
Open

fix(executor): let a branch loop back and re-run an earlier node#2184
joelorzet wants to merge 1 commit into
stagingfrom
fix/workflow-loop-back-edges

Conversation

@joelorzet

Copy link
Copy Markdown
Contributor

Problem

Drawing an edge from a downstream node back to an earlier one produced a workflow that stopped at the node before the loop entry. The engine treats any node with more than one incoming edge as a fan-in join and holds it until every branch arrives; a back edge counts as one of those branches, and it can only arrive after the node it points at has run. The join waits forever.

Auto-layout had the mirror of the same bug: the loop entry never cleared its in-degree during the column sweep, so it and everything downstream were placed as disconnected nodes stacked below the graph.

What changes

Back edges are classified once per run by a depth-first pass and kept apart from the forward DAG. Everything that reasons about what must happen before a node runs (convergence barrier, orphan detection, condition skip propagation, For Each body identification, layout columns) reads the forward edges only.

Routing keeps the back edges, so reaching one re-enters the loop entry: the loop body's traversal state is cleared and the entry plus every node below it runs again. Outputs are overwritten per pass, so templates and the run panel read the newest one.

Guardrails

  • 100 passes per loop, 1000 loop traversals per run. Either cap fails the run with a message naming the loop entry, so a loop that never exits stops loudly instead of returning a half-finished pass.
  • A pass after the first skips the spurious-max-retries recovery: that recovery is keyed by node, and on a second pass it would hand back the previous pass's output as if it were this one's.
  • A back edge that crosses a For Each body is refused before the first step runs; the body runner's dispatcher would drop it in silence.

Coverage

33 new unit tests over back-edge classification, the caps, body-state reset, layout, and a harness that drives the executor's dispatch shape end to end. Full unit suite passes (21,719 tests).

A workflow could already be drawn with an edge pointing back at an
earlier node, but nothing ran past it. The back edge raised that node's
incoming-edge count, so the convergence barrier held it waiting for an
arrival only its own execution could produce, and the branch stalled in
front of the loop entry. Auto-layout broke the same way from the other
side: the extra in-degree kept the loop entry out of the column sweep,
so it and everything behind it landed in the disconnected pile.

Classify back edges once per run with a depth-first pass and keep them
out of every map that answers "what has to happen before this node
runs": the convergence barrier, orphan detection, skip propagation and
the For Each body scan all read the forward DAG now. Routing still sees
them, so a node wired back to an ancestor re-enters it, clears the loop
body's traversal state, and runs the entry plus everything below it
again.

Nothing in a graph bounds how often that happens, so two caps do: 100
passes per loop, 1000 loop traversals per run. Hitting either fails the
run with a message naming the loop, rather than returning whichever pass
happened to be last. A back edge crossing a For Each body is refused up
front, since the body runner's dispatcher would ignore it in silence.
@github-actions

Copy link
Copy Markdown
Contributor

PR Environment Deployed

Your PR environment has been deployed!

Environment Details:

Components:

  • Keeperhub Application
  • PostgreSQL Database (isolated instance)
  • LocalStack (SQS emulation)
  • Redis (isolated instance)
  • Schedule Dispatcher (staging image)
  • Block Dispatcher (staging image)
  • Event Tracker (staging image)

The environment will be automatically cleaned up when this PR is closed or merged.

@OleksandrUA

Copy link
Copy Markdown
Contributor

Heads up, I broke this PR environment and I am rebuilding it now.

KEEP-1258 repaired the ECR lifecycle policies, which had never expired anything. The staging
policy now keeps the newest 30 images per tag family. I sized that from a 30 day average of
about 9 app- pushes a day, which was wrong: PR environment builds push 57 to 64 a day into the
same keeperhub-staging app-* family, so 30 images is roughly 20 hours, not 3 days.

keeperhub-staging:app-6320127, which this environment pins, was swept about 32 hours ago.
Three CronJob pods here have been in ImagePullBackOff since. The long running Deployments kept
working only because the image was already cached on the node.

I am removing and re-adding deploy-pr-environment to rebuild the environment from the current
head of this branch. That recreates the namespace, so any data you had seeded in it goes.

The real fix is to stop PR builds from sharing a tag family with the staging branch builds, so PR
churn cannot evict images. That is a separate change to deploy-pr-environment.yaml and it is
tracked on KEEP-1258.

Sorry for the noise.

@OleksandrUA

Copy link
Copy Markdown
Contributor

Correction to my note above. I could not rebuild the environment, and nothing was deleted.

This branch conflicts with staging right now, so GitHub cannot build the merge ref for it
(mergeable: false, merge_commit_sha: null). Every workflow here that triggers on
pull_request, which includes both deploy-pr-environment and cleanup-pr-environment, needs
that ref, so none of them run. Only the pull_request_target workflows fire, which is why you
see PR Issue Link running and nothing else.

So, concretely:

  • I removed and re-added the deploy-pr-environment label. Neither action started a workflow.
    The label is back in place now.
  • The pr-2184 namespace, its database and its data are untouched. My earlier note said a
    rebuild would recreate the namespace. That did not happen.
  • The three CronJob pods stay in ImagePullBackOff, because
    keeperhub-staging:app-6320127, built from this branch's last commit on 2026-08-28, was swept
    by the KEEP-1258 lifecycle change. The long running Deployments keep serving from the image
    already cached on their node.

Rebase or merge staging into this branch when you get a chance. That resolves the conflict,
the merge ref becomes computable, and the label already on the PR redeploys the environment with
a fresh image. There is nothing for you to do on the infrastructure side.

The underlying problem is mine: PR builds share the app-* tag family in keeperhub-staging
with staging branch builds, at 57 to 64 pushes a day, so a 30 image window is about 20 hours.
Splitting PR images into their own tag family is tracked on KEEP-1258.

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

Labels

deploy-pr-environment no-issue-required PR exempt from the issue-first gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants