Skip to content

🐛 fix(sink): keep one inventory's ownership conflicts out of the shared breaker - #532

Merged
konih merged 13 commits into
mainfrom
fix/sink-breaker-terminal-isolation
Nov 5, 2026
Merged

konih merged 13 commits into
mainfrom
fix/sink-breaker-terminal-isolation

Conversation

@konih

@konih konih commented Nov 5, 2026

Copy link
Copy Markdown
Collaborator

TL;DR

  • What: a Git ownership conflict that belongs to one inventory no longer counts toward the
    shared per-sink circuit breaker. Every other export failure still counts, including sink-wide
    Terminal failures such as a revoked Git token or a protected branch.
  • Why: five ownership rejections from one misconfigured inventory opened the breaker for every
    healthy inventory on the same sink for 30 s. The same shared breaker also made four reconciler
    tests flaky under -race -count=3.
  • Risk: low. The change is a narrow exclusion (a marker on 7 raise sites). The breaker still
    sheds load for every failure that is not marked, and a site nobody marked keeps today's
    behaviour.
  • Reviewer action: read internal/sink/circuit_breaker.go and the marked sites in
    internal/sink/git/prune_owned.go and prune_target.go.

What changes

  • Marker. kollecterrors.InventoryScoped(err) makes an error Terminal, keeps its text and
    chain, and marks it with ErrInventoryScoped. IsInventoryScoped tests for it.
  • Marked sites. The ownership-conflict rejections in the Git prune and merge-target claim
    checks (7 sites) go through a new ownershipConflict helper. Rejections caused by shared state
    (prune metadata, repository contents) stay unmarked and keep counting.
  • Breaker. IsExcluded: kollecterrors.IsInventoryScoped: a marked result counts neither as a
    failure nor as a success, does not reset the failure streak, and on a half-open trial frees the
    slot without closing or re-opening the breaker. Settings, key, threshold and timeout are
    unchanged; newBreaker(key, timeout) is extracted for tests.
  • Tests. Breaker unit and boundary tests (incl. half-open and a concurrent trial), a
    real-Git-backend boundary test, a per-site table that fails if any marked site loses its
    marker, a go-git engine case. Reconciler tests that shared the postgres-demo breaker now use
    a per-invocation sink name; one parallel test that read a global metric delta is now serial.
  • Docs. Troubleshooting row and an ADR-0602 note describe what counts and how half-open works.

Risk

  • A rejection marked inventory-scoped that is really sink-wide would stop the breaker from
    shedding load for that fault. Each of the 7 sites was classified in the probe and checked by
    two reviewers; ambiguous sites were left unmarked.
  • An inventory that keeps failing its ownership check can keep a recovering sink half-open (one
    trial at a time). Each excluded trial frees the slot, so other inventories are not locked out.

Files to check first

  1. internal/sink/circuit_breaker.go
  2. internal/errors/errors.go (InventoryScoped)
  3. internal/sink/git/prune_owned.go, internal/sink/git/prune_target.go
  4. internal/sink/circuit_breaker_test.go, internal/sink/git/prune_scoped_marker_test.go

Review commit by commit; each commit builds and passes the packages it touches.

Testing

  • go test -race -count=3 ./internal/errors/ ./internal/sink/... and
    go test -race -count=3 -skip TestControllers ./internal/controller/ (ten consecutive green runs
    of the latter; 3 of 10 failed on the base before the isolation).
  • Mutations: dropping the exclusion, swapping in IsSuccessful, excluding every Terminal,
    excluding every error, and reverting each marked site one at a time are each caught by a named
    test.
  • task verify, task lint, task spec:validate, scrub and gitleaks green on the head.

Release note (fixes): a single inventory's Git ownership conflict no longer opens the sink circuit
breaker for the other inventories on that sink.


Optional: how this was decided

The first design excluded every Terminal error. The spec-set review showed that Git auth and
protected-branch rejections reach the breaker already Terminal, and that inventories re-export on
every resource event, so that design would have stopped the breaker shedding load for a revoked
token. It was re-decided to mark only inventory-scoped results. OpenSpec change
openspec/changes/sink-breaker-terminal-isolation/ holds the spec, the per-task evidence and the
review records.

konih added 13 commits November 5, 2026 12:15
Terminal export errors trip the shared per-sink circuit breaker, so one
misconfigured inventory opens it for every inventory on the sink. Spec set
for excluding them (gobreaker IsExcluded), with test-first tasks.
Test isolation for the process-wide breaker registry, a constructor-only
timeout seam, the before/at/after-breaker classification boundary,
Forbidden and inferred-Terminal controls, half-open state assertions and a
concurrent-trial case, and a corrected flake mechanism.
Spec-set review round 2 showed that Git auth and protected-branch
rejections reach the breaker already Terminal and that Terminal exports
repeat on every event, so excluding all Terminal results would drop load
shedding for sink-wide failures. Re-decided: mark ownership rejections as
inventory-scoped and exclude only those.
Verifies the spec set's premises on base: breaker settings, gobreaker v2.4.0 exclusion semantics, the three zones, Git sink-wide Terminal at the breaker, re-export on the next reconcile. Classifies every Terminal raised inside an export plan (25 invalidPrune call sites plus backend classifiers) and every ResetBreakersForTest caller, names the SCB-1e fixture, and measures the controller race flake on base (2 of 10 runs red).
An ownership conflict is about one inventory, yet it counted toward the
per-sink circuit breaker, so five rejections of one inventory blocked
every other inventory on the same sink for 30 s.

Add an InventoryScoped marker in internal/errors (Terminal, text and
chain unchanged), route the five inventory-scoped prune sites through
ownershipConflict, and set IsExcluded on the breaker. Sink-wide
failures (Git auth, shared prune metadata) still count. Extract
newBreaker(key, timeout) so half-open tests can use a 1 s breaker.

Tasks 1.1 + 1.2 of openspec change sink-breaker-terminal-isolation.
Controller tests shared the fixed sink name postgres-demo, so parallel
siblings and -count reruns fed one process-wide circuit breaker key and
could open it for a test that expects an export (3 of 10 race runs red on
the previous commit). Each test now derives a per-invocation sink name
from uniqueSinkName and builds its expected status name from it.

After the change the race command shows no breaker failure in ten runs;
one run failed on the unrelated debounce-metric delta test, so task 2.2
stays open on its ten-green clause pending a decision.
TestClusterInventory_DebouncedExportIncrementsMetric read the
process-global kollect_export_debounced_total counter as a delta while
running in parallel, so a concurrent debounced cluster export made the
delta 2 (about 1 in 10 race runs). Run it non-parallel, as
terminalCleanupIncrementsMetric already does. With the per-invocation
sink names, ten race -count=3 runs of the controller package are green.
The troubleshooting row said the breaker opens on 5 transient failures and self-closes after the timeout. It counts every backend write failure except an inventory-scoped one (a Git ownership conflict), and after 30 s admits one trial that closes, re-opens or stays half-open. ADR-0602 gets a dated note; the decision is unchanged.
Branch review round 1 found that only one ownershipConflict raise site was
pinned by a test. A table in internal/sink/git now drives every routed site
through the function that raises it, plus the go-git engine through
ClassifyExportError; each site reverted to invalidPrune fails its row.

The InventoryScoped doc comment now says it always yields Terminal, pinned
by a test, and T0.1's cleanup statement is narrowed: relational cleanup goes
through RunExportEnvelope.
Round 2 approve/clean, per-commit gate green on all commits, and the
troubleshooting wording for the breaker counting interval.
@codecov

codecov Bot commented Nov 5, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@sonarqubecloud

sonarqubecloud Bot commented Nov 5, 2026

Copy link
Copy Markdown

@konih
konih merged commit 850df53 into main Nov 5, 2026
46 checks passed
@konih
konih deleted the fix/sink-breaker-terminal-isolation branch November 5, 2026 12:48
konih added a commit that referenced this pull request Nov 5, 2026
Post-merge evidence for #532 and the living spec sink-circuit-breaker
(SCB-1..SCB-3).
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.

1 participant