fix(branch): converge merges promptly via drain-ETA cadence and never-started-count rekick#12
Merged
Merged
Conversation
…oughput gauge updates in place
…ETA); keep sub-1/s rates visible (round(3))
A wait/wait_until child completing drops total pending without any never-started child being consumed, which the old pending-delta gate mistook for 'draining' and used to suppress rekick of a genuinely-dropped child behind staggered waits (deferring recovery). Gate on the dispatched (idle, started_at nil) count delta instead — the true 'workers are pulling this branch's queue' signal. Persists a 'dispatched' count in poll metadata; reused for the cadence :dispatched motion.
The poller (BranchMergeJob) is our code, and its queue placement is load-bearing:
on a queue saturated by a fan-out's own children it starves, so convergence lags
and no throughput is recorded. Previously the only lever was monkey-patching
BranchMergeJob.queue_as in the host app — fragile (a dev code-reload can reset it,
which bit us live-driving). Add ChronoForge.configure { |c| c.branch_merge_queue },
read per-enqueue via a queue_as block. Defaults to :default.
… region Auto-refresh was per-page opt-in, so pages that never wrapped their content (analytics, repetitions, wait_states, and — until just now — the workflow detail) silently didn't refresh. There's no reason for the split: the poller preserves filter text, focus, and scroll across the swap, and the nav/refresh controls live in <header> outside it. Hoist the region onto <main> so every page refreshes structurally, and drop the now-redundant per-page markers.
…e throughput/universal auto-refresh; note as-shipped divergences in the plan
…y stats on the branch detail page
… capped, like its sibling counts)
… (full numbers, no new query)
…eenshots (exact counts, branch stats)
5532173 to
7079dfc
Compare
…tarted' Kills the name collision: BranchProbe.dispatched / poll metadata "dispatched" meant never-started (idle & started_at nil), while the dashboard's "Dispatched" column means the TOTAL children spawned. Renames the poller-side concept (BranchProbe#never_started(?), the :never_started cadence motion, the never_started_by_branch vars, and the poll key) to never_started; the dashboard's total "Dispatched" column is left as-is. Old metadata falls back to the capped count until re-polled.
It is the total children spawned (spawn_each -> spawned_workflows); "Spawned" names the source directly and removes the last "dispatched" ambiguity now that the poller-side never-started count no longer shares the word.
A sealed branch dispatch total is immutable, so the poller counts it exactly ONCE (first poll after seal) and caches it on the branch-log metadata as \"spawned\"; later polls reuse it. The dashboard then shows the real Spawned total (panel column + branch-detail stat) with zero per-render cost — no cap, no COUNT(*) over 500k on every refresh. Falls back to the capped live count until the branch seals.
…counts (100k drive)
…ached counts + branch-detail stats
… selectable options)
6 tasks
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.
What & why
BranchMergeJob(the poller that drives a fan-out's merge to completion) had two defects, surfaced by the fan-out scale test:max_interval, ~5 min) exactly when a fast-draining backlog was about to finish, so a parent could hangidlefor up to 5 min after its last child completed.wait_untilchild resuming would drop without any never-started child actually moving.The poller holds no lock and never replays the (heavy) parent; each pass is fenced by a per-pass
poll_tokenso a superseded chain stops quietly.The two fixes
1. ETA-driven cadence (
reschedule_delay). Poll at(pending / rate) * ETA_FRACTION, clamped[min_interval, max_interval]. Because each pass re-estimates against the shrinking remainder, cadence converges geometrically: a 500k fan-out polls flatmax_intervalthrough the long middle, then tightens tomin_intervalfor the last few thousand — the parent is woken within ~min_intervalof the last child, not up to amax_intervallate. When nothing completes in an interval the fallback is motion-aware::runningholds the responsive floor (a slow/low-fan-out child is never woken late),:never_startedbacks off exponentially (a queued-but-unpicked straggler),:nonedecays tomax_interval. Motion probes are computed lazily, only off the drain path.2. Never-started-count rekick gate. Rekick is gated on the never-started count (
idle & started_at nil) delta, not total pending: if that count fell since the last poll, workers are consuming the queue, so deep-queued-but-healthy children are left alone. Only a branch whose never-started count has gone flat has its stale children rekicked, and atouchper rekick debounces it to at most once perREKICK_AFTER. Blocked (failed/stalled) and waiting (idlewithstarted_atset) children are never touched.Supporting work
ChronoForge.config.branch_merge_queue(default:default).merge_branchesenqueues the poller after dispatching children, so it must not share a large fan-out's queue or it starves behind the backlog (polls once atpending≈0, no ETA, dashboard gauge never renders). Documented as a trap in the scale-test doc.last/next_poll_at,interval,pending,never_started,spawned,rate,eta_seconds,polls, rekick counts) under a row lock + token recheck. Purely observational; replay/correctness never read it.round(3)); a branch-detail stats header; universal auto-refresh (the whole<main>is adata-poll-region, so every page — including detail — refreshes in place, preserving filter text/focus/scroll).pendingandnever_startedrender exact straight from the poll metadata the poller already computes; the totalspawnedis immutable once a branch seals, so it's counted once and cached on the metadata (sticky), then shown exact with zero per-render cost. All fall back to the capped live count until the branch is sealed+polled. The mutable per-state chips stay capped (5000+).dispatched → never_startedto kill a collision with the dashboard's total column, which was in turn renamed "Dispatched" → "Spawned" (it maps tospawn_each→spawned_workflows).Validation
🤖 Generated with Claude Code