Skip to content

Add throttling middleware for rate and in-flight limit.#4171

Open
genericaccount-de wants to merge 1 commit into
constructorfabric:mainfrom
genericaccount-de:feature/throttling-middleware-improvement
Open

Add throttling middleware for rate and in-flight limit.#4171
genericaccount-de wants to merge 1 commit into
constructorfabric:mainfrom
genericaccount-de:feature/throttling-middleware-improvement

Conversation

@genericaccount-de

@genericaccount-de genericaccount-de commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Adds throttling middleware for rate and in-flight limit. Throttling is defined by common zones, which are reused by separate API endpoints by means of OperationsBuilder.

Summary by CodeRabbit

  • New Features
    • Added zone-based request throttling with separate controls for steady-rate (RPS) and concurrent in-flight limits.
    • Enables per-route throttling via named zones, IP-keyed limiting, and optional “dry run” mode.
  • Bug Fixes
    • Throttling rejections now return consistent, canonical error details with Retry-After and rate-limit metadata headers.
  • Documentation
    • Updated quickstart and E2E/local configs to the zone-based throttling model; local config increases request body limit, and OpenAPI output no longer emits legacy rate-limit vendor extensions.
  • Tests
    • Added end-to-end tests for zone-based rate limiting and in-flight rejection; updated middleware-order and other gateway tests; removed legacy rate-limit test coverage.

@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

API gateway throttling now uses named rate-limit and in-flight limit zones instead of global rate_limit fields. The toolkit, gateway config, middleware, route wiring, deployment configs, and tests were updated to use the new ThrottlingSpec/zone model.

Changes

Zone-based throttling migration

Layer / File(s) Summary
Toolkit throttling contracts and OperationBuilder API
libs/toolkit/src/api/operation_builder.rs, libs/toolkit/src/api/mod.rs, libs/toolkit/src/api/openapi_registry.rs, gears/system/api-gateway/src/middleware/mime_validation.rs
OperationSpec now carries throttling, ThrottlingSpec and IdentityExtractor are added, require_rate_limit is replaced by with_throttling, and OpenAPI/test code is updated to the new field.
ApiGatewayConfig zone definitions and validation
Cargo.toml, gears/system/api-gateway/Cargo.toml, gears/system/api-gateway/src/config.rs, deny.toml
Workspace and crate dependencies are updated, gateway config now defines throttling zone maps and serde types, and throttling validation/tests are added.
Throttling middleware implementation and wiring
gears/system/api-gateway/src/middleware/mod.rs, gears/system/api-gateway/src/gear.rs, gears/system/api-gateway/src/middleware/throttling.rs
The middleware stack is split into pre-auth and post-auth throttling phases, and the new throttling middleware builds maps, enforces zones, and emits throttling responses and headers.
Mini-chat route throttling wiring and deployment configs
gears/mini-chat/mini-chat/src/api/rest/routes/chats.rs, config/*.yaml, gears/mini-chat/deploy/helm/mini-chat/templates/configmap.yaml, testing/e2e/gears/mini_chat/config/base.yaml
Mini-chat route registration now uses ThrottlingSpec, and the referenced throttling zones are defined in local, e2e, Helm, quickstart, and mini-chat configs.
Gateway integration tests for throttling changes
gears/system/api-gateway/tests/*.rs
API gateway tests switch to throttling, drop legacy rate-limit defaults, and add coverage for throttling responses, headers, ordering, and in-flight behavior.

Estimated code review effort: 5 (Critical) | ~120 minutes

🚥 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 summarizes the main change: adding throttling middleware for rate and in-flight limits.
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

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.

@genericaccount-de
genericaccount-de force-pushed the feature/throttling-middleware-improvement branch from 4646634 to 48abd86 Compare July 1, 2026 09:10

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

🧹 Nitpick comments (2)
gears/system/api-gateway/tests/throttling_tests.rs (1)

252-268: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Fixed-delay synchronization risks flaky CI failures.

Using sleep(20ms) to assume the first spawned request has acquired the in-flight permit before firing the second request is a timing assumption, not a guarantee. Under CI load this can intermittently let the second request land before the permit is taken, causing a spurious OK instead of TOO_MANY_REQUESTS.

Consider adding explicit synchronization — e.g., a tokio::sync::Notify/oneshot signaled from slow_handler right after it starts (before the internal sleep), and await that signal instead of a fixed delay.

♻️ Example fix sketch
-async fn slow_handler() -> Json<TestResponse> {
-    sleep(Duration::from_millis(200)).await;
+async fn slow_handler() -> Json<TestResponse> {
+    STARTED.notify_one(); // signal readiness before the slow work
+    sleep(Duration::from_millis(200)).await;
     Json(TestResponse {
         message: "slow".to_owned(),
     })
 }

Then await STARTED.notified() in the test instead of a fixed sleep.

🤖 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 `@gears/system/api-gateway/tests/throttling_tests.rs` around lines 252 - 268,
The throttling test currently relies on a fixed `tokio::time::sleep` to wait for
the first spawned request to acquire the in-flight permit, which is flaky under
load. Update the `throttling_tests.rs` flow around `first` and the
`slow_handler` setup to use explicit synchronization instead of timing
assumptions. Add a `tokio::sync::Notify` or oneshot signal from `slow_handler`
as soon as it starts holding the permit, then have the test await that signal
before sending the second request. This keeps the `TOO_MANY_REQUESTS` assertion
deterministic.
gears/system/api-gateway/src/middleware/throttling.rs (1)

91-107: 🚀 Performance & Scalability | 🔵 Trivial | ⚖️ Poor tradeoff

Soft max_keys eviction can't shrink below concurrently-active keys.

gate() only evicts entries with Arc::strong_count(v) <= 1; if all tracked keys are currently in-flight (strong_count > 1), the map can grow past max_keys indefinitely under sustained high-cardinality traffic. This is a reasonable "soft cap" tradeoff for a first implementation given KeyGates must stay alive while held, but worth a follow-up note/metric if unbounded key cardinality (e.g., unauthenticated IP keys) is a realistic threat model.

🤖 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 `@gears/system/api-gateway/src/middleware/throttling.rs` around lines 91 - 107,
The soft-cap behavior in InFlightZone::gate can leave the keys map growing
beyond max_keys when all entries are still in flight, so add follow-up
observability or a clear note around this tradeoff. Update gate() to surface
when retain() fails to reclaim entries (for example via a metric/log on the key
eviction path) and document that the cap is only best-effort while KeyGate
references are active.
🤖 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 `@gears/system/api-gateway/src/config.rs`:
- Around line 421-424: The validate_status helper currently accepts any valid
HTTP status, including 2xx/3xx values, for throttle responses. Update
validate_status in config.rs to keep using StatusCode::from_u16 for parsing,
then add an explicit check that only 4xx/5xx statuses are accepted for
limit-exceeded/throttle configuration and return an error otherwise. Make sure
the error message stays tied to zone and response_status_code so callers can
identify invalid non-error statuses.

In `@gears/system/api-gateway/src/middleware/throttling.rs`:
- Around line 12-14: Update the module-level docs in the throttling middleware
to match the current behavior: the comment near the top of throttling.rs is
stale because enforce() now performs live per-request rate-limit and in-flight
checks. Replace the reference to throttling_rules with the actual config
concepts used by this code path, such as rate_limit_zones and
in_flight_limit_zones, so the docs accurately describe what enforce() and the
surrounding middleware do.
- Around line 310-391: Rate-limit headers are being attached to the request in
enforce, so successful responses never expose them to clients. Update enforce
and the downstream success path around next.run(req) to add RateLimit-Policy,
RateLimit-Limit, RateLimit-Remaining, and legacy X-RateLimit-* onto the Response
before returning it, including the in-flight permit branch. Use the existing
symbols enforce, throttle_response, and the rate_zone/inflight_zone handling to
locate where the response headers should be applied.
- Around line 164-238: The throttling builder currently creates separate zone
state per auth partition, so the same named zone gets different limiter
instances before and after auth. Update the shared build path in build() so rate
zones and in-flight zones are constructed once and reused across both
partitions, instead of having independent HashMap/Arc state per require_ctx.
Reuse the same zone caches when populating ThrottlingInner routes so any
operation referencing the same zone name shares the same Arc<RateZone> or
Arc<InFlightZone> regardless of require_security_context.

In `@libs/toolkit/src/api/operation_builder.rs`:
- Around line 272-275: The ThrottlingSpec contract currently carries a key_type
value that is not enforced by runtime throttling, which can drift from the
actual gateway zone configuration. Update the operation builder/types around
ThrottlingSpec and the throttling map construction logic so key strategy is
sourced only from the referenced zone config, or validate any provided key_type
against the zone before accepting it. Use the ThrottlingSpec, key_type,
identity_extractor, and the throttling map builder code paths to remove the
stale field or enforce consistency.

---

Nitpick comments:
In `@gears/system/api-gateway/src/middleware/throttling.rs`:
- Around line 91-107: The soft-cap behavior in InFlightZone::gate can leave the
keys map growing beyond max_keys when all entries are still in flight, so add
follow-up observability or a clear note around this tradeoff. Update gate() to
surface when retain() fails to reclaim entries (for example via a metric/log on
the key eviction path) and document that the cap is only best-effort while
KeyGate references are active.

In `@gears/system/api-gateway/tests/throttling_tests.rs`:
- Around line 252-268: The throttling test currently relies on a fixed
`tokio::time::sleep` to wait for the first spawned request to acquire the
in-flight permit, which is flaky under load. Update the `throttling_tests.rs`
flow around `first` and the `slow_handler` setup to use explicit synchronization
instead of timing assumptions. Add a `tokio::sync::Notify` or oneshot signal
from `slow_handler` as soon as it starts holding the permit, then have the test
await that signal before sending the second request. This keeps the
`TOO_MANY_REQUESTS` assertion deterministic.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 974a0bd4-001e-42ea-a095-f1fb35faf48f

📥 Commits

Reviewing files that changed from the base of the PR and between 6ae9504 and 48abd86.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (29)
  • Cargo.toml
  • config/e2e-local.yaml
  • config/e2e-scope-enforcement.yaml
  • config/e2e-tr-authz.yaml
  • config/mini-chat.yaml
  • config/no-auth.yaml
  • config/quickstart.yaml
  • gears/mini-chat/deploy/helm/mini-chat/templates/configmap.yaml
  • gears/mini-chat/mini-chat/src/api/rest/routes/chats.rs
  • gears/system/api-gateway/Cargo.toml
  • gears/system/api-gateway/src/config.rs
  • gears/system/api-gateway/src/gear.rs
  • gears/system/api-gateway/src/gear.rs.orig
  • gears/system/api-gateway/src/middleware/mime_validation.rs
  • gears/system/api-gateway/src/middleware/mod.rs
  • gears/system/api-gateway/src/middleware/rate_limit.rs
  • gears/system/api-gateway/src/middleware/throttling.rs
  • gears/system/api-gateway/tests/access_log_tests.rs
  • gears/system/api-gateway/tests/body_limit_tests.rs
  • gears/system/api-gateway/tests/http_metrics_tests.rs
  • gears/system/api-gateway/tests/middleware_order.rs
  • gears/system/api-gateway/tests/middleware_order.rs.orig
  • gears/system/api-gateway/tests/mime_validation_integration.rs
  • gears/system/api-gateway/tests/rate_limit_tests.rs
  • gears/system/api-gateway/tests/throttling_tests.rs
  • libs/toolkit/src/api/mod.rs
  • libs/toolkit/src/api/openapi_registry.rs
  • libs/toolkit/src/api/operation_builder.rs
  • testing/e2e/gears/mini_chat/config/base.yaml
💤 Files with no reviewable changes (12)
  • gears/mini-chat/deploy/helm/mini-chat/templates/configmap.yaml
  • config/no-auth.yaml
  • gears/system/api-gateway/tests/access_log_tests.rs
  • config/e2e-scope-enforcement.yaml
  • config/mini-chat.yaml
  • config/e2e-local.yaml
  • gears/system/api-gateway/tests/middleware_order.rs.orig
  • gears/system/api-gateway/tests/rate_limit_tests.rs
  • config/e2e-tr-authz.yaml
  • testing/e2e/gears/mini_chat/config/base.yaml
  • gears/system/api-gateway/src/gear.rs.orig
  • gears/system/api-gateway/src/middleware/rate_limit.rs

Comment thread gears/system/api-gateway/src/config.rs Outdated
Comment thread gears/system/api-gateway/src/middleware/throttling.rs Outdated
Comment thread gears/system/api-gateway/src/middleware/throttling.rs
Comment thread gears/system/api-gateway/src/middleware/throttling.rs Outdated
Comment thread libs/toolkit/src/api/operation_builder.rs Outdated
@genericaccount-de
genericaccount-de force-pushed the feature/throttling-middleware-improvement branch 2 times, most recently from adc2cfd to d7ff18b Compare July 2, 2026 10:30

@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.

🧹 Nitpick comments (1)
gears/mini-chat/deploy/helm/mini-chat/templates/configmap.yaml (1)

65-85: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Throttling limits hardcoded in production Helm template; consider exposing via .Values.

Unlike bind_addr (line 54) which is parameterized through .Values.service.port, the rate/in-flight limits here (10/s, burst 10, in-flight 100, backlog_limit: 0) are hardcoded directly in the production chart and identical to the local dev config. Ops teams cannot tune throttling per-environment without editing the template, and these values (esp. zero backlog) may be too restrictive for production chat-creation traffic.

Consider surfacing rate_limit, burst_limit, in_flight_limit, and backlog_limit as chart values with these as defaults.

🤖 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 `@gears/mini-chat/deploy/helm/mini-chat/templates/configmap.yaml` around lines
65 - 85, The throttling settings in the mini-chat Helm ConfigMap are hardcoded
instead of being configurable per environment. Update the configmap template to
read the rate-limit, burst-limit, in-flight-limit, and backlog-limit values from
chart values with the current numbers as defaults, using the existing config
sections for rate_limit_zones and in_flight_limit_zones as the places to wire
them in. This will let ops tune the mini-chat chat-create throttling without
modifying the template while keeping the current behavior as the default.
🤖 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 `@gears/mini-chat/deploy/helm/mini-chat/templates/configmap.yaml`:
- Around line 65-85: The throttling settings in the mini-chat Helm ConfigMap are
hardcoded instead of being configurable per environment. Update the configmap
template to read the rate-limit, burst-limit, in-flight-limit, and backlog-limit
values from chart values with the current numbers as defaults, using the
existing config sections for rate_limit_zones and in_flight_limit_zones as the
places to wire them in. This will let ops tune the mini-chat chat-create
throttling without modifying the template while keeping the current behavior as
the default.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 697e6620-d8b4-419f-8f52-39742791e61d

📥 Commits

Reviewing files that changed from the base of the PR and between adc2cfd and d7ff18b.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (29)
  • Cargo.toml
  • config/e2e-local.yaml
  • config/e2e-scope-enforcement.yaml
  • config/e2e-tr-authz.yaml
  • config/mini-chat.yaml
  • config/no-auth.yaml
  • config/quickstart.yaml
  • gears/mini-chat/deploy/helm/mini-chat/templates/configmap.yaml
  • gears/mini-chat/mini-chat/src/api/rest/routes/chats.rs
  • gears/system/api-gateway/Cargo.toml
  • gears/system/api-gateway/src/config.rs
  • gears/system/api-gateway/src/gear.rs
  • gears/system/api-gateway/src/gear.rs.orig
  • gears/system/api-gateway/src/middleware/mime_validation.rs
  • gears/system/api-gateway/src/middleware/mod.rs
  • gears/system/api-gateway/src/middleware/rate_limit.rs
  • gears/system/api-gateway/src/middleware/throttling.rs
  • gears/system/api-gateway/tests/access_log_tests.rs
  • gears/system/api-gateway/tests/body_limit_tests.rs
  • gears/system/api-gateway/tests/http_metrics_tests.rs
  • gears/system/api-gateway/tests/middleware_order.rs
  • gears/system/api-gateway/tests/middleware_order.rs.orig
  • gears/system/api-gateway/tests/mime_validation_integration.rs
  • gears/system/api-gateway/tests/rate_limit_tests.rs
  • gears/system/api-gateway/tests/throttling_tests.rs
  • libs/toolkit/src/api/mod.rs
  • libs/toolkit/src/api/openapi_registry.rs
  • libs/toolkit/src/api/operation_builder.rs
  • testing/e2e/gears/mini_chat/config/base.yaml
💤 Files with no reviewable changes (9)
  • config/no-auth.yaml
  • gears/system/api-gateway/tests/middleware_order.rs.orig
  • config/e2e-scope-enforcement.yaml
  • config/e2e-local.yaml
  • gears/system/api-gateway/src/middleware/rate_limit.rs
  • config/e2e-tr-authz.yaml
  • gears/system/api-gateway/tests/access_log_tests.rs
  • gears/system/api-gateway/src/gear.rs.orig
  • gears/system/api-gateway/tests/rate_limit_tests.rs
✅ Files skipped from review due to trivial changes (2)
  • gears/system/api-gateway/src/middleware/mime_validation.rs
  • Cargo.toml
🚧 Files skipped from review as they are similar to previous changes (15)
  • gears/system/api-gateway/tests/body_limit_tests.rs
  • gears/mini-chat/mini-chat/src/api/rest/routes/chats.rs
  • gears/system/api-gateway/tests/mime_validation_integration.rs
  • gears/system/api-gateway/src/middleware/mod.rs
  • libs/toolkit/src/api/mod.rs
  • gears/system/api-gateway/tests/middleware_order.rs
  • config/quickstart.yaml
  • gears/system/api-gateway/Cargo.toml
  • gears/system/api-gateway/tests/http_metrics_tests.rs
  • gears/system/api-gateway/src/gear.rs
  • libs/toolkit/src/api/openapi_registry.rs
  • gears/system/api-gateway/tests/throttling_tests.rs
  • gears/system/api-gateway/src/middleware/throttling.rs
  • libs/toolkit/src/api/operation_builder.rs
  • gears/system/api-gateway/src/config.rs

@genericaccount-de
genericaccount-de force-pushed the feature/throttling-middleware-improvement branch 3 times, most recently from ecfbf77 to f080d81 Compare July 2, 2026 15:36
Comment thread deny.toml

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think generic part of throttling should not be in api gateway crate

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done, moved generic parts to the api-gateway.

@genericaccount-de
genericaccount-de force-pushed the feature/throttling-middleware-improvement branch 2 times, most recently from cb8a5db to a310889 Compare July 8, 2026 15:44
…ute.

Signed-off-by: ga <7kozlik+github@gmail.com>
@genericaccount-de
genericaccount-de force-pushed the feature/throttling-middleware-improvement branch from a310889 to ece9145 Compare July 16, 2026 09:50
/// peer address from `ConnectInfo`, else `"unknown"`.
fn client_ip(req: &Request) -> String {
let headers = req.headers();
if let Some(ip) = headers

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

CRITICAL

client_ip trusts the leftmost X-Forwarded-For (and X-Real-IP) value, which is fully client-controlled, as the throttling bucket key with no trusted-proxy validation.

Pre-auth IP-keyed zones are the only limit an unauthenticated caller hits; a spoofed or per-request-unique XFF both bypasses the limit entirely and lets an attacker attribute load to victim IPs.

Derive the client IP from ConnectInfo or from a configured trusted-proxy hop count (rightmost untrusted XFF entry), and ignore client-supplied X-Forwarded-For/X-Real-IP unless the immediate peer is a trusted proxy.

.ok_or_else(|| anyhow!("throttling: rate_limit zone '{name}' has rps = 0"))?;
let burst = NonZeroU32::new(cfg.burst_limit)
.ok_or_else(|| anyhow!("throttling: rate_limit zone '{name}' has burst_limit = 0"))?;
let limiter = RateLimiter::keyed(Quota::per_second(rps).allow_burst(burst))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

HIGH

RateLimitZone.max_keys is validated and documented as an LRU-eviction bound, but get_or_build_rate_zone never applies it to the governor keyed limiter, so its internal state store grows one entry per distinct key with no eviction.

For IP-keyed pre-auth zones the key is attacker-controlled via X-Forwarded-For, so unbounded distinct keys exhaust process memory and take down the gateway despite the configured bound.

Enforce max_keys on the rate limiter (periodic retain_recent/shrink prune bounded by a lifecycle CancellationToken, or a bounded keyed store) so the tracked-key count cannot exceed the configured limit.

}
// Soft `max_keys` cap: drop gates no longer referenced by in-flight requests.
if self.keys.len() as u64 >= self.cfg.max_keys {
self.keys.retain(|_, v| Arc::strong_count(v) > 1);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

HIGH

gate() runs DashMap::retain on the request hot path once len() >= max_keys; it write-locks and scans every shard, and when keys are actively in-flight (strong_count > 1) it evicts nothing, so the map keeps growing and the full O(n) scan repeats on each subsequent new-key request.

Under a flood of distinct keys (attacker-controlled IPs pre-auth) this becomes a per-request O(n) all-shard write-locking scan that serializes gate lookups and fails to bound memory — the cap becomes a DoS amplifier.

Replace the inline retain with a bounded eviction that does not write-lock all shards per request (segmented LRU with a size cap, or a periodic background sweep gated by a CancellationToken), and skip the scan when it cannot free entries.

#[derive(Clone, Default)]
pub struct ThrottlingSpec {
/// Name of the rate-limit zone this operation participates in.
pub rate_limit_zone: String,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

MEDIUM

ThrottlingSpec models zone references as bare String, with the empty string overloaded as a sentinel for "no zone in this category" (see if thr.rate_limit_zone.is_empty() in the gateway).

The absent-zone state is representable only by convention, so every consumer must remember the sentinel rule and an accidental empty string is a silently-valid "no throttling" — a type the compiler could have rejected.

Change both fields to Option<String> (or a ZoneName newtype) so "no zone" is expressed as None and callers cannot pass an ambiguous empty string.

}

fn log_throttled(key: &ThrottleKey, kind: &str, id: &str) {
tracing::debug!(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

MEDIUM

Enforced throttling rejections (real 429s) are logged at tracing::debug! and emit no metric, while non-enforced dry-run would-be-rejections are logged at tracing::warn!.

At the default info level operators get zero visibility into actual client throttling, and with no rejection counter, capacity/tuning problems on this critical path are undiagnosable in production.

Emit a rejection metric (counter keyed by zone/kind) and raise the enforced-rejection log to at least info, keeping the high-cardinality key value out of the message body.

});

// Let the first request acquire the permit before firing the second.
tokio::time::sleep(Duration::from_millis(20)).await;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LOW

The in-flight concurrency test coordinates the two requests with a fixed 20ms sleep, so the required ordering (first request holds the sole permit before the second arrives) is not guaranteed.

On a loaded runner the spawned first request may not acquire the permit within 20ms, causing spurious failures or masking a real regression instead of deterministically exercising the gate.

Replace the sleep with an explicit synchronization signal (a tokio::sync::Notify or Barrier the slow handler fires on entry) so the second request is issued only after the permit is confirmed held.

});

let throttling = builder.spec.throttling.as_ref().expect("throttling set");
assert_eq!(throttling.rate_limit_zone, "rl_identity");

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LOW

builder_with_throttling_sets_spec_and_extractor asserts the zone-name fields equal the exact strings just passed to with_throttling, which only confirms the setter stored its input.

with_throttling merely assigns the spec, so echoing the same names back exercises no logic and catches no regression; only the extractor-invocation and Clone/Debug assertions carry value.

Drop the two zone-name echo assertions and keep/extend the ones that verify behavior — that the stored closure is invokable and the Arc<dyn Fn> field survives Clone/Debug.

@MikeFalcon77

Copy link
Copy Markdown
Collaborator

Design-level comments:

This is separate from the code-level findings. It concerns the throttling model itself.

  1. Configuration ergonomics
    The code/config split is drawn in the wrong place. Numeric limits (rps, burst, status, retry-after, max_keys) live in YAML zones — good, operators tune them without a rebuild. But the operation->zone binding, dry_run, and require_security_context all live in Rust code. dry_run is exactly what an operator wants to flip at runtime when rolling out enforcement gradually; today that requires editing the binary. This contradicts the NGINX-style model the code advertises, where the whole policy is config.

  2. 3rd-party API gateway target
    The model will not port to an external gateway (Kong/Envoy/AWS API GW) as-is. Enforcement lives entirely in the in-process axum middleware; ThrottlingSpec is only metadata that the local middleware executes. Hard blockers:

IdentityKeyFn = Arc<dyn Fn(&axum::extract::Request) -> String> is a Rust closure — not serializable, not callable by an external gateway. Identity keying does not translate at all.

IdentityKeyFn pulls axum::extract::Request into the toolkit contract type (operation_builder.rs:261). That leaks the runtime into the contract layer — a direct problem on feature/toolkit_contracts, where contracts are meant to be portable.

Zone definitions live in ApiGatewayConfig, not in the contract. require_security_context / dry_run are concepts of the local middleware pipeline only.

For an external target the key must be declarative (enum Key { Ip, Header(String), JwtClaim(String), … }) and the zone model must live in the contract, not the gateway gear.

  1. Rust idioms (Go port)
    Mostly idiomatic; two Go-isms stand out:

Empty string as a "no zone" sentinel (if thr.rate_limit_zone.is_empty()) instead of Option.
axum in a public contract type (see §2).
Good/idiomatic: RateSpec newtype with custom Deserialize, KeyType/RetryAfter enums, the manual Debug, governor + tokio::Semaphore. The with_throttling(mut self) -> Self builder is consistent with the rest of OperationBuilder.

  1. Group-level throttling setup
    Not supported. with_throttling is per-operation and the full 5-field spec is repeated on every route; there is no router/group API and no default. Two consequences:

Numeric limits are not duplicated (defined once per zone), only the binding boilerplate is.
Sharing semantics: build_maps makes every operation on a zone share one token bucket / gate. "One zone on 10 routes" means 10 rps across all ten combined, not 10 rps each. Same-limit-but-independent per route is not expressible without separate zones.

  1. Is this how Rust does it?
    Building blocks are OK (governor, Semaphore, axum from_fn). Architecture - no. The per-operation-metadata → central resolver → config zones shape is more Go-framework pattern. The common Rust approach is a tower::Layer (tower_governor::GovernorLayer, GlobalConcurrencyLimitLayer) applied to a router or route group via .route_layer(), which is declarative at the composition site and gives grouping for free. Need to design config-based throttling setup with tower layers approach.

Suggestion:
Let's design it via toolkit ADR/desing document
It should decide:

  1. The config/code boundary — specifically whether dry_run and the operation->zone binding move to config (route-policies already reserve rate_limit as a "future field").
  2. A declarative key model (enum Key) that replaces the axum closure, so the contract stays runtime-agnostic and an external-gateway backend is possible.
  3. Whether zone binding is expressible at the route-group level, and the intended shared-vs-per-route bucket semantics.

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.

2 participants