Skip to content

feat(reliability): backpressure — bound backend-op concurrency (LAB-729) - #49

Merged
27Bslash6 merged 3 commits into
mainfrom
lab-729-backpressure
Jul 28, 2026
Merged

feat(reliability): backpressure — bound backend-op concurrency (LAB-729)#49
27Bslash6 merged 3 commits into
mainfrom
lab-729-backpressure

Conversation

@27Bslash6

@27Bslash6 27Bslash6 commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Closes LAB-729.

Decision record (LAB-729 gate): build, not rs-specific N/A

  • This is parity, not net-new. cachekit-py ships backpressure: src/cachekit/reliability/load_control.py (threading.Semaphore-based BackpressureController), BackpressureConfig (enabled by default, max_concurrent_requests: 100, queue_size: 1000, timeout: 0.1s), surfaced through the decorator orchestrator with dedicated tests. A fleet-wide N/A was therefore never on the table.
  • The cachekit-ts rationale (LAB-519 matrix footnote) does not transfer. ts declined a global miss semaphore because on Node's single-threaded event loop the herd shapes it would bound are already handled (single-flight, backend timeouts, breaker). cachekit-rs has the failure mode ts argued it lacked: a tokio runtime (multi-threaded or single-threaded unsync alike) holds unbounded concurrent backend ops in flight — fred's single multiplexed Redis connection buffers unbounded in-flight commands (caller memory), and the reqwest SaaS client grows per-host connections without cap (socket/FD exhaustion). LAB-518's retry loop and LAB-728's SWR refreshes amplify exactly that fan-out under a backend slowdown.

Design

ConcurrencyLimiter (tokio::sync::Semaphore + bounded waiting queue) as the outermost layer of the existing ReliableBackend decorator: backpressure(breaker(retry(op))).

  • One permit per logical operation, held across the whole retry sequence. Bounds in-flight work including retry amplification: K callers mid-backoff are still K permits. Per-attempt permits would admit new callers onto a struggling backend while others back off, defeating the bound.
  • Shed calls never touch breaker state. A shed returns before CircuitBreaker::try_acquire, so breaker counters and half-open probe slots keep measuring backend health, not caller-side overload (tested with failure_threshold: 1).
  • No deadlock against retry backoff: a permit holder never re-enters the limiter (backend ops don't nest; locks bypass via as_lockable; health is unguarded). Tested with cap 1 + concurrent retrying callers.
  • Over-limit contract (typed, bounded): a saturated limiter admits up to max_queue waiters for at most acquire_timeout each; queue-full and wait-timeout both shed with the new non-retryable BackendErrorKind::Backpressure. Never an unbounded silent queue. Queue accounting is cancel-safe via an RAII drop guard (the ProbePermit lesson applied).
  • Macro layer: sheds are outage-class. The #[cachekit] plain path fails open on Backpressure exactly like CircuitOpen — the function runs uncached; secure paths fail closed. (Review-panel catch: the initial diff left sheds hard-failing wrapped functions while the sicker breaker-open state failed open.)

Divergences from cachekit-py (stated + justified)

cachekit-py cachekit-rs Why
Rejection classification TRANSIENT (queue full) / TIMEOUT (permit timeout) — both retryable there single non-retryable Backpressure kind rs retry sits inside the limiter and can never see a shed; for callers, immediately retrying a shed re-amplifies the overload being shed (CircuitOpen precedent: "not retryable now")
Order vs breaker breaker state pre-check → acquire → op limiter → breaker(retry) py's pre-check takes no slot, so order is immaterial there; in rs, limiter-outermost keeps shed load out of half-open probe-slot accounting
Defaults 100 / 1000 / 0.1 s identical parity

Preset story (mirrors LAB-518 posture)

ReliabilityConfig::default() now includes backpressure: Some(BackpressureConfig::default())production / encrypted / io get it; minimal stays unbounded/off. Builder gating also wraps on a backpressure-only config. Semver: new pub field on ReliabilityConfig breaks exhaustive struct literals; release-please bump-minor-pre-major lands this as 0.6.0, which cargo treats as incompatible with ^0.5 — no silent behaviour change for existing pins. BackendErrorKind is #[non_exhaustive].

Tests

  • Cap invariant: max_seen ≤ K under a 32-way burst with cap 4 (plus all 32 complete).
  • Immediate shed (max_queue: 0), timed shed (waits out acquire_timeout), nonzero queue-full boundary.
  • Sheds don't open the breaker; permit released on error; queue slot released on cancelled wait; composes with retry without deadlock (exact backend-call counts).
  • Macro fail-open on shed (macro_fails_open_when_backpressure_sheds).
  • cargo fmt --check, clippy --all-targets -D warnings, full suite 242 passed / 0 failed, MSRV 1.85 check green.

Docs (gate)

README (stack table, composition, degradation row, feature table, opt-out example), rustdoc (module docs, BackpressureConfig, presets, builder, error kind). Companion PRs: protocol matrix flip + decision footnote (cachekit-io/protocol, branch lab-729-matrix-rs-backpressure), docs.cachekit.io corrections (cachekit-io/docs, branch lab-729-docs-backpressure — the concepts page had backpressure throttling inverted, and the rust FAQ still claimed no reliability tier exists).

Reviewed pre-push by a 3-agent adversarial panel (bug-hunter / code-craftsman / red-team); the surviving findings (macro fail-closed CRIT, matrix footnote mechanism claims, docs-site contradiction, two minors) are all addressed in this PR set.

Summary by CodeRabbit

  • New Features
    • Added backpressure controls to cap concurrent backend work and manage a bounded waiting queue.
    • Configurable limits and timeouts, with shed requests surfacing as backpressure outcomes.
    • Plain cached calls can fall back to uncached execution when backpressure sheds; secure paths remain fail-closed.
  • Bug Fixes
    • Backpressure-only configuration is now correctly applied as part of the reliability stack.
  • Documentation
    • Updated reliability feature guidance, preset behaviour, and execution order to include backpressure.
  • Tests
    • Added and expanded coverage for backpressure shedding, queuing behaviour, and interactions with retry and circuit breaking.

…B-729)

Adds a ConcurrencyLimiter (tokio::sync::Semaphore + bounded waiting queue)
as the outermost layer of the ReliableBackend stack:
backpressure(breaker(retry(op))). One permit per logical operation, held
across the whole retry sequence — bounds retry amplification, and shed
calls never touch breaker counters, so the breaker keeps measuring backend
health rather than caller-side overload.

Over-limit behaviour is typed and bounded: a saturated limiter admits up
to max_queue waiters for at most acquire_timeout each; everyone else is
shed with the new non-retryable BackendErrorKind::Backpressure. Defaults
mirror cachekit-py's BackpressureController (100 concurrent / 1000 queued
/ 100 ms), and the production/encrypted/io presets enable it via
ReliabilityConfig::default() while minimal stays unbounded — the same
preset posture as the LAB-518 retry/breaker layers.

Builder gating now also wraps on a backpressure-only config.
…cuitOpen (LAB-729)

Review-panel CRIT: the graceful-degradation arm matched is_retryable() ||
CircuitOpen only, so a Backpressure shed hard-failed #[cachekit]-wrapped
functions while the sicker breaker-open state failed open — an availability
inversion, and a silent divergence from cachekit-py (whose decorator runs
the function uncached on a backpressure rejection).

A shed is the same class as CircuitOpen: the call never reached the
backend. Failing open adds no work a cold-miss storm doesn't already
create — the limiter bounds backend cache ops, not origin executions, and
single-flight still dedupes those. secure paths stay fail-closed.

Also from the panel: pin the nonzero max_queue admission boundary with a
unit test, drop the stray Eq derive on BackpressureConfig (sibling configs
are PartialEq only), and document the macro-layer shed behaviour on
BackpressureConfig, the macro rustdoc, and the README degradation row.
@kodus-27b

This comment has been minimized.

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

No new commits to review since the last review.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: f17412a8-de58-4c16-b33a-b5a210cfba92

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

Walkthrough

The reliability stack adds default bounded concurrency with queueing and shedding. Backpressure errors are exposed publicly, integrated outside breaker and retry handling, and treated as fail-open conditions by the plain macro path. Tests and documentation cover configuration, composition, cancellation, and shedding.

Changes

Backpressure reliability

Layer / File(s) Summary
Reliability contracts and configuration
crates/cachekit/src/error.rs, crates/cachekit/src/reliability.rs, crates/cachekit/src/client.rs, crates/cachekit/src/lib.rs
Adds BackpressureConfig, a ReliabilityConfig.backpressure field, default configuration, a typed Backpressure error, public re-exports, and backpressure-only wrapper activation.
Concurrency limiter integration
crates/cachekit/src/reliability.rs
Adds a bounded semaphore and queue with cancellation-safe slot release, acquiring permits before breaker admission and retry execution.
Macro fail-open handling
crates/cachekit-macros/src/lib.rs
Treats backpressure shedding as a plain-path fail-open condition while preserving secure-path fail-closed behaviour.
Behaviour validation and documentation
crates/cachekit/tests/*, README.md, crates/cachekit/src/intents.rs
Adds limiter, composition, cancellation, and macro integration tests, and documents defaults, stack ordering, and opt-out configuration.

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

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant CacheMacro
  participant ReliableBackend
  participant Backpressure
  participant BreakerRetry
  participant Backend
  Caller->>CacheMacro: invoke cached operation
  CacheMacro->>ReliableBackend: request backend access
  ReliableBackend->>Backpressure: acquire permit
  Backpressure-->>ReliableBackend: permit or shed error
  ReliableBackend->>BreakerRetry: run admitted operation
  BreakerRetry->>Backend: execute backend call
  Backend-->>BreakerRetry: response
  BreakerRetry-->>ReliableBackend: result
  ReliableBackend-->>CacheMacro: result or backpressure error
  CacheMacro-->>Caller: cached result or uncached fail-open result
Loading

Possibly related PRs

  • cachekit-io/cachekit-rs#43: Introduced the reliability behaviour that this change extends with backpressure errors and fail-open handling.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 76.36% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarises the main change: adding backpressure to bound backend-operation concurrency.
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-729-backpressure

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

@27Bslash6 27Bslash6 changed the title LAB-729: feat(reliability): backpressure — bound backend-op concurrency feat(reliability): backpressure — bound backend-op concurrency (LAB-729) Jul 27, 2026
Comment thread crates/cachekit/tests/macro_tests.rs Outdated

@kodus-27b kodus-27b 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.

@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: 1

🤖 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/reliability.rs`:
- Around line 454-528: Update the three Backpressure error messages in
ConcurrencyLimiter::acquire to include the relevant configured limits: max_queue
for queue-full and acquire_timeout for timeout; include max_concurrent as well
if needed to identify the limiter capacity. Preserve the existing shed behavior
and distinguish queue-full, unavailable, and timeout cases.
🪄 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: 549bd568-b7f3-4564-995e-c77845c7050e

📥 Commits

Reviewing files that changed from the base of the PR and between e31109b and 0891fa0.

📒 Files selected for processing (9)
  • README.md
  • crates/cachekit-macros/src/lib.rs
  • crates/cachekit/src/client.rs
  • crates/cachekit/src/error.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/reliability.rs
…ssages, bounded test polls (LAB-729)

CodeRabbit: the queue-full and wait-timeout shed messages now carry the
configured max_queue / acquire_timeout, so a production incident can tell
from the error alone which limit was hit. The three branches stay
distinguishable; shed behaviour unchanged.

Kody (critical): the saturate-the-limiter tests polled with an
equality-terminated spin (== 0), which hangs CI forever if the condition
is ever skipped. All four loops (the flagged macro_tests one plus the
three same-pattern loops in reliability_tests) now use a relational
condition with a 5 s deadline that fails the test fast instead.
@kodus-27b

kodus-27b Bot commented Jul 28, 2026

Copy link
Copy Markdown

Kody Review Complete

Great news! 🎉
No issues were found that match your current review configurations.

Keep up the excellent work! 🚀

Kody Guide: Usage and Configuration
Interacting with Kody
  • Request a Review: Ask Kody to review your PR manually by adding a comment with the @kody start-review command at the root of your PR.

  • Validate Business Logic: Ask Kody to validate your code against business rules by adding a comment with the @kody -v business-logic command.

  • Provide Feedback: Help Kody learn and improve by reacting to its comments with a 👍 for helpful suggestions or a 👎 if improvements are needed.

Current Kody Configuration
Review Options

The following review options are enabled or disabled:

Options Enabled
Bug
Performance
Security
Business Logic

Access your configuration settings here.

@27Bslash6

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@27Bslash6

Copy link
Copy Markdown
Contributor Author

@kody start-review

@coderabbitai

coderabbitai Bot commented Jul 28, 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

Copy link
Copy Markdown
Contributor Author

@27Bslash6 — LAB-729 is through the automated review gates and is ready for your signoff / merge.

Automated review is clean on the current head (5f60c371):

  • CodeRabbit: APPROVED (2026-07-28T00:13:28Z) — opened CHANGES_REQUESTED at 23:03Z, flipped after remediation
  • Kody: APPROVED (2026-07-28T00:14:41Z) — same, opened CHANGES_REQUESTED at 23:00Z
  • Review threads: 2 total, 0 unresolved
  • CI: all green — stable, beta, 1.85, wasm32 compilation check, redis lock integration, memcached integration, title lint
  • MERGEABLE / mergeStateStatus: CLEAN

Every check run is against the current head SHA, not a stale base.

Expert panel not run — the crypto/protocol gate was assessed and does not fire. The diff changes no encryption, AAD construction, key derivation, cache-key format, or ByteStorage wire format. The one security-adjacent surface was verified by hand: the macro emits the fail-open arm only in the else branch of if args.secure { quote!{} }, so the new BackendErrorKind::Backpressure cannot fall open on an encrypted path — secure workloads still fail closed on a shed. Happy to run the panel anyway if you'd rather have it on the record.

Companion PRs under the same ticket:

  • cachekit-io/protocol#45 — matrix row + rs-specific decision footnote — already merged
  • cachekit-io/docs#23 — public docs — APPROVED, CLEAN, 0 unresolved; still open, so it needs merging alongside this one or the public rust reliability FAQ stays stale

Not merging — that's yours.

@27Bslash6
27Bslash6 merged commit 8173e91 into main Jul 28, 2026
10 checks passed
@27Bslash6
27Bslash6 deleted the lab-729-backpressure branch July 28, 2026 12:11
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