feat(net): RateKey + RateLimitConfig + boot-time coverage (Slice 0 PR 4)#72
Conversation
The exhaustive match, not the length assertion, is the actual drift tripwire.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
✅ Files skipped from review due to trivial changes (1)
📝 WalkthroughWalkthroughAdds a new ChangesRate limit coverage validation
Estimated code review effort: 3 (Moderate) | ~20 minutes Possibly related issues
Possibly related PRs
Suggested labels: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
crates/adapter/net/http/api/src/rate.rs (1)
137-150: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win
validate_coveragedoesn't flag stale keys not inK::all().The loop only checks that every
K::all()variant is present incfg.local; it never rejects extra entries inlocalthat no longer correspond to a current variant (e.g., after aRateKeyvariant is removed/renamed, an orphaned entry silently survives). Since the module's contract is "total overK::all()," consider also assertingcfg.local.len() == K::all().len()(or diffing keys) to catch drift in both directions.♻️ Optional strengthening of the total-coverage check
pub fn validate_coverage<K>(cfg: &RateLimitConfig<K>) -> Result<(), BuildError> where K: RateKey + fmt::Debug, { cfg.global.validate()?; + if cfg.local.len() != K::all().len() { + // an entry in `local` does not correspond to any current `K::all()` variant + } for key in K::all() { match cfg.local.get(key) { None => return Err(BuildError::UndeclaredKey(format!("{key:?}"))), Some(LimitDecl::Policy(policy)) => policy.validate()?, Some(LimitDecl::GlobalOnly) => {}, } } Ok(()) }🤖 Prompt for 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. In `@crates/adapter/net/http/api/src/rate.rs` around lines 137 - 150, `validate_coverage` only verifies that every `K::all()` key exists in `RateLimitConfig::local`, but it does not reject stale or orphaned entries that are no longer part of the current `RateKey` set. Update `validate_coverage` to check coverage in both directions by comparing the configured local keys against `K::all()` (for example by validating the lengths or diffing the key sets) so removed or renamed variants are flagged as `BuildError` instead of silently surviving.
🤖 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.
Nitpick comments:
In `@crates/adapter/net/http/api/src/rate.rs`:
- Around line 137-150: `validate_coverage` only verifies that every `K::all()`
key exists in `RateLimitConfig::local`, but it does not reject stale or orphaned
entries that are no longer part of the current `RateKey` set. Update
`validate_coverage` to check coverage in both directions by comparing the
configured local keys against `K::all()` (for example by validating the lengths
or diffing the key sets) so removed or renamed variants are flagged as
`BuildError` instead of silently surviving.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: bedb1658-563d-41d0-91aa-b5d03304cdfa
📒 Files selected for processing (3)
CHANGELOG.mdcrates/adapter/net/http/api/src/lib.rscrates/adapter/net/http/api/src/rate.rs
|
Re: CodeRabbit nitpick on
No code change. |
…verage # Conflicts: # CHANGELOG.md
* docs(net): Slice 2 stack()/HttpConfig design + Slice 0 PR4 plan Adds the runtime-free assembly design (stack() + HttpConfig) derived from the construction-surface spec and ADR-0031, and commits the previously-untracked Slice 0 PR4 (rate-coverage) implementation plan that shipped as #72. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(net): Slice 2 implementation plan + spec refinements Adds the task-by-task stack()/HttpConfig plan and folds in the design refinements surfaced while planning against the shipped layer APIs: HttpConfig gains rate_limit_max_wait (RateLimitLayer::new's 3rd arg), validation is delegated to RateLimitLayer::new (no separate call), and tests use an inline recording leaf (not MockClient, a dev-dep cycle). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(net): HttpConfig + stack() - canonical resilience assembly Add the non-generic HttpConfig aggregate and the stack() function that validates pacing coverage (RateLimitLayer::new) before composing the ADR-0031 canonical resilience order over any leaf: Tracing(CircuitBreaker(Retry(RateLimit(Timeout(SetHeaders(Auth(leaf))))))). Includes a transparency smoke test (request flows through all layers, body untouched) and a boot-coverage test proving a non-total RateLimitConfig fails closed as a BuildError before anything is built. Deviations from the brief, needed to compile/lint clean: - Added `S::Body: Send` to stack()'s where clause: RateLimit's Service impl requires the body to be Send, which the bare HttpClient bound does not imply. - Test module imports `oath_adapter_net_api::Timer` (needed for ScriptLeaf's Step::Hang arm to call `timer.sleep`). - `#[allow(clippy::needless_pass_by_value)]` on stack(): rate_limits is only borrowed internally, but the public signature intentionally takes ownership to mirror HttpConfig's "consumed once at boot" shape. - Second test uses `let-else` instead of `.unwrap_err()`, which would require Debug on the opaque `impl HttpClient + ...` Ok type. - `#[allow(dead_code)]` on Step::Err/Hang, ScriptLeaf::calls/seen_auth, and CounterAuth: scaffolding consumed by Task 2's full-stack tests landing later in this same file. Closes #87 * test(net): stack ordering, fail-closed, and per-attempt Auth tests * docs(net): record stack()/HttpConfig amendment (ADR-0034) + changelog * docs(net): note S::Body: Send bound in ADR-0034 amendment #11 Final whole-branch review flagged the amendment enumerated the bound refinements but omitted S::Body: Send (transitively required by Retry/RateLimit's B: Send). Records it so the ADR matches the shipped stack() signature. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(net): sync stack() signature bounds in Slice 2 spec CodeRabbit (PR #88) flagged the spec's stack() where-clause still showed the pre-implementation sketch bounds. Sync it to the shipped signature: S::Body: Send, T/A + 'static, K + Debug (already in ADR-0034 #11). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Closes #69
Slice 0 PR 4 of the net-http construction surface (spec: docs/superpowers/specs/2026-06-30-net-http-construction-surface-design.md) — closes Slice 0.
RateKey— the adapter's endpoint key with a finite universe (fn all() -> &'static [Self]), kept generic so the coverage check can iterate every variant.LimitPolicy(TokenBucket { rate, burst }/Concurrency { max }) +LimitDecl(Policy/GlobalOnly) — explicit per-endpoint classification; there is no "absent" arm.RateLimitConfig<K>— a total map (requiredglobal+localoverK::all()).BuildError+validate_coverage— the pure construction-time check: an unclassified key →UndeclaredKey; an out-of-rangerate/burst/max→InvalidPolicy. A missing bucket is a boot failure, not a 15-minute IBKR penalty box (ADR-0034 §3).No new dependency; no runtime; no layer. Slice 2's
stack()/build()will callvalidate_coveragebefore assembling the stack.Next: Slice 1 — the resilience layers (
Retry,RateLimit(consumes this config + constructsGuarded),CircuitBreaker,Tracing), needingMockTimer-driven timing tests and the per-requestScope/RateLimit<K>extension.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes
Tests