Skip to content

feat(reliability): retry, circuit breaker, graceful degradation, single-flight (LAB-518) - #43

Merged
27Bslash6 merged 5 commits into
mainfrom
lab-518-reliability-tier
Jul 24, 2026
Merged

feat(reliability): retry, circuit breaker, graceful degradation, single-flight (LAB-518)#43
27Bslash6 merged 5 commits into
mainfrom
lab-518-reliability-tier

Conversation

@27Bslash6

@27Bslash6 27Bslash6 commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Closes LAB-518 — reliability-tier parity for cachekit-rs (rank #3 of the LAB-275 cross-SDK feature-gap audit).

What

A Rust user behind a flaky backend now gets the same posture as py/ts:

Layer Implementation
Retry Truncated exponential backoff + jitter around every backend data op, driven by the previously-dead BackendErrorKind::is_retryable() classification. Permanent/auth errors propagate on first failure. Defaults: 3 attempts, 100 ms base, 5 s cap, jitter ×[0.5, 1.5).
Circuit breaker closed/open/half-open with rolling failure window (5 failures / 60 s → open 5 s → 3 probes, close after 3 successes — the ts production numbers). New BackendErrorKind::CircuitOpen for fail-fast errors. Only retryable-kind failures count toward opening — a burst of 400s can't cut off healthy traffic (deliberate improvement over ts's count-everything breaker; rs has the classification, ts doesn't).
Graceful degradation #[cachekit] fails open on backend failure — the wrapped function runs uncached (cachekit-py posture). The secure path stays fail-closed: backend + decryption errors propagate; encrypted workloads never silently degrade. Config errors always propagate (cross-SDK key-divergence contract unchanged).
Single-flight CacheKit::single_flight() (always available, tokio-sync only): per-key in-process async mutex collapses concurrent fills; followers re-check the cache, leaders never do (a leader re-check = a second billable miss under metered-misses). With reliability + a lock-capable backend (CachekitIO, Redis — both already implement LockableBackend), a distributed fill lock suppresses fills cross-process; contested processes poll for the remote fill (100 ms cadence, 5 s budget) then compute anyway (fail-open). The macro wires all of this automatically.

Feature/preset story

  • New reliability feature, in defaults, native-only (compile_error! with workers, mirroring the l1 guard). Composition: breaker(retry(op)) as a Backend decorator applied at build() — zero changes to client call sites, covers plain/secure/interop/macro paths uniformly.
  • Presets: on in production/encrypted/io, off in minimal (ts preset posture). Builder: .reliability(config) / .no_reliability().
  • Backend::as_lockable() default hook (returns None; overridden by CachekitIO + Redis) so dyn Backend can expose its lock capability — trait objects can't cross-cast.

Tests

21 new tests (11 reliability_tests.rs, 6 breaker/retry unit tests, 4 macro tests): retry recovery/exhaustion/permanent-passthrough, breaker open/fail-fast/half-open-recovery/probe-slot-release, one-breaker-failure-per-retry-sequence, macro fail-open + secure fail-closed, 5-way concurrent-miss dedup, distributed-lock acquire/release + contested-poll paths.

Gates: full suite green (cachekitio,redis,encryption,l1,macros + defaults), clippy -D warnings, fmt, MSRV 1.85 check, redis-only and wasm32 feature combos compile (warnings unchanged vs base), cargo-deny licenses.

Non-goals

Backpressure (matrix row stays ❌, not in LAB-518 acceptance criteria), SWR (stretch — should split to its own ticket), observability (LAB-101).

Crypto/protocol gate

No encryption, AAD, key-derivation, cache-key, or wire-format code is touched — the decorator passes bytes through untransformed and the macro's key construction is unchanged (macro_key_delegates_to_interop_key still green). Expert-panel review not triggered.

Companion docs PRs: cachekit-io/protocol (matrix rows), cachekit-io/docs (rust quickstart/builder/macros pages).

Summary by CodeRabbit

  • New Features

    • Added a reliability tier with retries, backoff, circuit breaking and graceful degradation.
    • Added single-flight request coordination to prevent duplicate cache fills during concurrent misses.
    • Added configurable reliability settings and backend locking support.
    • Added documentation and examples covering reliability behaviour and feature compatibility.
  • Bug Fixes

    • Improved handling of temporary backend outages while preserving fail-closed behaviour for encrypted operations.
    • Prevented cancelled recovery probes from leaving circuit breakers stuck.

…-miss single-flight (LAB-518)

Brings cachekit-rs to reliability-tier parity with the Python and
TypeScript SDKs:

- New `reliability` feature (default, native-only): retry with truncated
  exponential backoff + jitter driven by the previously-dead
  BackendErrorKind::is_retryable() classification, and a
  closed/open/half-open circuit breaker with a rolling failure window.
  Composed as breaker(retry(op)) around every backend data operation,
  mirroring cachekit-ts's ReliabilityExecutor. Only retryable-kind
  errors count toward opening the circuit — permanent/auth errors are
  request-specific and must not cut off healthy traffic.
- Presets: on by default in production/encrypted/io, off in minimal
  (ts preset posture). Builder: .reliability() / .no_reliability().
- New BackendErrorKind::CircuitOpen for fail-fast errors.
- Graceful degradation in #[cachekit]: backend failures fail OPEN on the
  plain path (function runs uncached, matching cachekit-py); the secure
  path stays fail-CLOSED — encrypted workloads never silently degrade.
- Cold-miss single-flight (CacheKit::single_flight, always available):
  per-key in-process async mutex dedups concurrent fills; with
  `reliability`, lock-capable backends (CachekitIO, Redis via the
  existing LockableBackend) additionally suppress fills cross-process
  through a distributed fill lock with poll-for-fill on contention.
  Leaders never re-check the cache — a re-check would be a second
  billable miss under metered-misses pricing.
- Backend::as_lockable() default hook (None) overridden by CachekitIO
  and RedisBackend so dyn Backend can expose its lock capability.

Co-authored-by: multica-agent <github@multica.ai>
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 39 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4bd5eaaf-c4ea-4d89-85bd-1778cef7f297

📥 Commits

Reviewing files that changed from the base of the PR and between 3874425 and dde95ab.

📒 Files selected for processing (6)
  • README.md
  • crates/cachekit/Cargo.toml
  • crates/cachekit/src/backend/mod.rs
  • crates/cachekit/src/flight.rs
  • crates/cachekit/src/lib.rs
  • crates/cachekit/tests/macro_tests.rs

Walkthrough

The PR adds a native-only reliability feature with retries, circuit breaking, fail-open handling, and distributed single-flight cache fills. It extends backend capability discovery, builder presets, macro miss handling, public exports, documentation, and integration coverage.

Changes

Reliability contracts and builder wiring

Layer / File(s) Summary
Reliability contracts and builder wiring
crates/cachekit/Cargo.toml, crates/cachekit/src/lib.rs, crates/cachekit/src/error.rs, crates/cachekit/src/client.rs, crates/cachekit/src/intents.rs, README.md
Adds the native-only reliability feature, public configuration exports, circuit-open errors, builder configuration, default intent wiring, feature incompatibility checks, and reliability documentation.

Retry and circuit-breaker engine

Layer / File(s) Summary
Retry and circuit-breaker engine
crates/cachekit/src/reliability.rs, crates/cachekit/tests/reliability_tests.rs
Implements retry backoff and jitter, circuit states and half-open probes, guarded backend operations, cancellation-safe permits, shared wrappers, and tests for retry and breaker behaviour.

Local and distributed single-flight

Layer / File(s) Summary
Local and distributed single-flight
crates/cachekit/src/flight.rs, crates/cachekit/src/client.rs, crates/cachekit/src/backend/*, crates/cachekit/tests/reliability_tests.rs
Adds per-key in-process coordination, optional distributed fill locks, contested-lock polling, lock release, backend lock capability exposure, and single-flight integration tests.

Macro failure and fill handling

Layer / File(s) Summary
Macro failure and fill handling
crates/cachekit-macros/src/lib.rs, crates/cachekit/tests/macro_tests.rs
Updates generated cache-miss control flow for single-flight rechecks, plain-path fail-open behaviour, secure-path fail-closed behaviour, permanent-error propagation, and guard release.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CacheKitBuilder
  participant ReliableBackend
  participant CircuitBreaker
  participant RetryPolicy
  participant Backend
  CacheKitBuilder->>ReliableBackend: wrap backend with reliability configuration
  ReliableBackend->>CircuitBreaker: guard operation
  CircuitBreaker->>RetryPolicy: execute retry policy
  RetryPolicy->>Backend: perform backend operation
  Backend-->>RetryPolicy: return result or error
  RetryPolicy-->>CircuitBreaker: report outcome
  CircuitBreaker-->>ReliableBackend: return result or circuit-open error
Loading
sequenceDiagram
  participant CacheMacro
  participant CacheKit
  participant FlightMap
  participant LockableBackend
  participant CacheBackend
  CacheMacro->>CacheKit: acquire single-flight guard
  CacheKit->>FlightMap: lock computed key
  FlightMap->>LockableBackend: acquire distributed fill lock
  LockableBackend-->>CacheKit: leader or contested result
  CacheKit->>CacheBackend: re-check or write filled value
  CacheKit->>LockableBackend: release distributed lock
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarises the main reliability-tier changes: retry, circuit breaker, graceful degradation, and single-flight.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch lab-518-reliability-tier

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

@27Bslash6

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

27Bslash6 and others added 2 commits July 24, 2026 01:04
… (LAB-518)

The circuit breaker's half_open_calls counter caps in-flight recovery
probes, but a Success that had not yet crossed success_threshold never
released its slot — only Neutral did. Any config with
success_threshold > half_open_max_calls (all fields are pub, unvalidated)
therefore wedged the breaker half-open permanently: slots fill, successes
stall below threshold, and every call fails fast with CircuitOpen even
against a recovered backend. The default 3/3 preset escaped only because
threshold == cap. Mirror the Neutral arm and release the slot on a
non-closing success; add a regression test with threshold(3) > cap(1).

Surfaced by the crypto/protocol expert-panel review.

Co-authored-by: multica-agent <github@multica.ai>
…fail-open, API cuts (LAB-518)

Expert-panel review findings on PR #43:

- CRIT: half-open probe slots leaked when a guarded future was cancelled
  (caller timeout/select!) or panicked before recording an outcome —
  leak half_open_max_calls of them and the breaker wedges half-open
  forever, CircuitOpen on every call even against a recovered backend.
  try_acquire now returns an RAII ProbePermit: complete() hands
  accounting to the outcome arms; Drop releases the slot (like Neutral,
  no transition) on every other exit path. The manual counter pairs
  that leaked twice are gone. Unit tests cover dropped-permit release
  and closed-state permits not touching half-open accounting; an
  integration test cancels a hanging probe via tokio::time::timeout
  and proves the breaker still recovers.
- MAJ: the plain-path fail-open arm swallowed permanent/auth backend
  errors — a wrong API key silently ran uncached forever while looking
  healthy. Fail-open is now scoped to outage-class errors (retryable +
  CircuitOpen); permanent/auth propagate. secure unchanged
  (fail-closed on everything).
- MIN: SingleFlight::awaiting_fill → wait_for_fill — it sleeps and
  mutates; the old name read as a pure predicate.
- Cuts: no_reliability() deleted (opt out by passing a config with both
  layers None — build() skips the no-op wrap); CircuitState demoted to
  cfg(test) until LAB-101 consumes it at runtime.

Co-authored-by: multica-agent <github@multica.ai>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/cachekit/src/flight.rs`:
- Around line 45-58: Document the distributed fill suppression ceiling
represented by FILL_LOCK_TIMEOUT_MS and FILL_POLL_BUDGET, explicitly noting that
fills exceeding the lock TTL may be recomputed concurrently after auto-expiry.
Keep the existing fail-open and owner-checked release behavior unchanged; only
add operational guidance or, if supported by the surrounding design, outline a
lock-renewal/heartbeat strategy.

In `@crates/cachekit/tests/macro_tests.rs`:
- Around line 359-379: Add a macro test alongside
macro_fails_open_when_backend_down that configures a low reliability failure
threshold, drives the backend through enough failures to open its circuit, and
then verifies the generated fail_open_arm plain path still executes uncached
successfully. Assert the operation result and run count after CircuitOpen is
reached, using the existing fail-open helpers and backend fixtures.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: a574450d-1233-4af2-b5cc-5fd5a361d116

📥 Commits

Reviewing files that changed from the base of the PR and between f33e646 and 3874425.

📒 Files selected for processing (14)
  • README.md
  • crates/cachekit-macros/src/lib.rs
  • crates/cachekit/Cargo.toml
  • crates/cachekit/src/backend/cachekitio.rs
  • crates/cachekit/src/backend/mod.rs
  • crates/cachekit/src/backend/redis.rs
  • crates/cachekit/src/client.rs
  • crates/cachekit/src/error.rs
  • crates/cachekit/src/flight.rs
  • crates/cachekit/src/intents.rs
  • crates/cachekit/src/lib.rs
  • crates/cachekit/src/reliability.rs
  • crates/cachekit/tests/macro_tests.rs
  • crates/cachekit/tests/reliability_tests.rs

Comment thread crates/cachekit/src/flight.rs
Comment thread crates/cachekit/tests/macro_tests.rs
…he 5s fill-lock ceiling (LAB-518)

CodeRabbit round 2: the macro's fail-open guard special-cases
BackendErrorKind::CircuitOpen but no test tripped a real breaker on the
plain path — added one (threshold 1: transient fall-open opens the
circuit, second call falls open on CircuitOpen). Also documented that
FILL_LOCK_TIMEOUT_MS bounds cross-process suppression: fills slower
than 5 s lose the lock mid-compute and may recompute concurrently
(fail-open by design; owner-checked release prevents wrongful
deletion).

Co-authored-by: multica-agent <github@multica.ai>
@27Bslash6

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 24, 2026
@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

…liability-tier

Conflict resolution notes:
- Cargo.toml: main made tokio an optional dep for memcached/file
  spawn_blocking; this branch needs tokio-sync unconditionally for
  cold-miss single-flight. Kept ONE non-optional tokio (sync only) and
  dropped dep:tokio from the memcached/file feature lists — they still
  stack tokio/rt + tokio/time on top, same build result.
- lib.rs / README: kept both sides' workers mutual-exclusion guards
  (reliability + memcached + file).
- New File/Memcached backends implement no LockableBackend, so the
  default as_lockable() = None is correct — no overrides needed.

Co-authored-by: multica-agent <github@multica.ai>
@27Bslash6
27Bslash6 merged commit e9b9a1e into main Jul 24, 2026
8 checks passed
@27Bslash6
27Bslash6 deleted the lab-518-reliability-tier branch July 24, 2026 16:08
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