Skip to content

feat(workflow-engine): add namespace throttle sweep state machine - #19964

Draft
danielskovli wants to merge 2 commits into
feat/workflow-engine-throttle-schemafrom
feat/workflow-engine-throttle-sweep
Draft

feat(workflow-engine): add namespace throttle sweep state machine#19964
danielskovli wants to merge 2 commits into
feat/workflow-engine-throttle-schemafrom
feat/workflow-engine-throttle-sweep

Conversation

@danielskovli

Copy link
Copy Markdown
Contributor

Description

The core of the namespace circuit breaker (#18481, ADR in #19954): a periodic sweep (NamespaceThrottleService) running detect → throttle → probe → release under a Postgres advisory lock — single writer across replicas; a held lock skips the cycle rather than queueing. With Throttling.Enabled=false the loop never starts.

  • Detect: one GROUP BY namespace over the new partial index; trip = absolute floor AND ratio, both from EngineSettings.Throttling.
  • Park: bulk-stamps throttled_until = now + window (±20% jitter per row), clamped per-stamp to each workflow's current step's retry deadline — the anchor rule is extracted from WorkflowHandler into a shared StepExtensions.ResolveRetryAnchor (behavior-neutral) and combined with the existing GetDeadline; a row at/past its deadline is left unparked so the final attempt is never delayed. Stamps are guarded WHERE status = Requeued and never touch updated_at.
  • Canaries: K earliest-backoff_until Requeued workflows, unparked, judged purely by requeue-count comparison against counts recorded at selection (race-free vs. mid-attempt; a vanished canary counts as progressed). Unanimous failure → window ×2 (capped) + canary rotation; any progress → HalfOpen (quorum of one), first cohort releasing in the same sweep.
  • Release: oldest-first cohorts doubling each sweep, stamped now + random()·SweepInterval (jittered smear, not a NULL-clear, so a big cohort doesn't hit one poll cycle). Re-trip during recovery is evaluated over the unparked population only (parked rows are still Requeued — raw counts would re-trip instantly) and returns to Open keeping the grown window.
  • Closed: the row lingers for a grace period (5 × SweepInterval, named constant) during which stragglers' throttled_until is cleared, then is deleted. A re-trip from Closed starts over at the initial window — the grace period judged the incident over.
  • Publication: a read-only IThrottleStateView singleton (refreshed by every replica each cycle, lock holder or not) exposing Open breakers only — HalfOpen is deliberately excluded so cooperative parking (next PR) cannot hide the failures the re-trip signal is judged on.
  • Observability: engine.throttle.tripped/extended/released/closed counters tagged namespace, an open-breakers gauge, per-cycle activity, and structured logs for every transition.
  • Release cohort size is in-memory by design: losing it on restart/failover restarts cohorts at canary scale and re-doubles — a few extra-cautious sweeps, never incorrect.

Stacked on #19962.

Verification

  • Full engine suite: 701 passed, 0 failed (Resilience 57, Models 63, Data 70, Core 197, Repository 166, Integration 148 — real Postgres/Testcontainers).
  • 16 new sweep tests drive cycles with explicit clock values (zero sleeps): trip gating, jitter bounds, deadline clamp (near-deadline exact, past-deadline no-stamp), canary judgments, rotation + window cap, quorum-of-one, cohort order/doubling/smear, re-trip window retention, grace cleanup + row deletion, advisory-lock skip, disabled no-op.
  • No snapshot or wire-contract changes. The hosted-services registration-order snapshot lives in workflow-engine-app and will legitimately change when its CI runs against this chain.

🤖 Generated with Claude Code

@github-actions github-actions Bot added the skip-releasenotes Issues that do not make sense to list in our release notes label Aug 13, 2026
@coderabbitai

coderabbitai Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 0f1cff30-a50a-4f14-816b-1246c1d75a27

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@danielskovli
danielskovli force-pushed the feat/workflow-engine-throttle-sweep branch from 2207cd8 to 8fa7487 Compare August 19, 2026 12:45
@danielskovli
danielskovli force-pushed the feat/workflow-engine-throttle-sweep branch from 8fa7487 to 61d785b Compare August 20, 2026 06:47
@danielskovli
danielskovli force-pushed the feat/workflow-engine-throttle-sweep branch from 61d785b to 29efbff Compare August 20, 2026 09:17
@danielskovli
danielskovli force-pushed the feat/workflow-engine-throttle-sweep branch from 29efbff to 1aed5f3 Compare August 20, 2026 09:32
@danielskovli
danielskovli force-pushed the feat/workflow-engine-throttle-sweep branch from 1aed5f3 to dd3039e Compare August 20, 2026 09:54
@danielskovli
danielskovli force-pushed the feat/workflow-engine-throttle-sweep branch from dd3039e to 8796756 Compare August 20, 2026 11:49
@danielskovli
danielskovli force-pushed the feat/workflow-engine-throttle-sweep branch from 8796756 to 3761082 Compare August 20, 2026 12:05
danielskovli and others added 2 commits August 20, 2026 14:06
NamespaceThrottleService runs the failure-storm circuit breaker's full
detect → throttle → probe → release cycle under an advisory lock (single
writer across replicas), parking Requeued populations behind jittered,
retry-deadline-clamped throttled_until stamps while rotating canaries
probe recovery, then releasing oldest-first in doubling cohorts. Each
replica publishes an in-memory open-breaker snapshot (IThrottleStateView)
per cycle for the upcoming handler cooperation; with Throttling.Enabled
off the service exits before entering its loop.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…xpire the breaker snapshot fail-open

Two ADR review findings (PR #19954): a canary observed mid-attempt no
longer counts as progressed — being leased proves nothing about the
target, and in a hang-until-timeout storm counting it as progress would
release cohorts into a hanging target that cannot feed the re-trip
signal until its attempts time out. And the handler-facing open-breaker
snapshot now carries its publication time and reads as empty once older
than three sweep intervals, so a replica whose sweep loop has died
loses its power to park instead of stamping workflows into long-closed
namespaces with nothing left to clear them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@danielskovli danielskovli reopened this Aug 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip-releasenotes Issues that do not make sense to list in our release notes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant