Skip to content

[ISSUE #8442] Honour requestCount in the sliding window rate limiter script - #8456

Open
konlue wants to merge 1 commit into
apache:masterfrom
konlue:fix/8442-sliding-window-request-count
Open

konlue wants to merge 1 commit into
apache:masterfrom
konlue:fix/8442-sliding-window-request-count

Conversation

@konlue

@konlue konlue commented Nov 12, 2026

Copy link
Copy Markdown
Contributor

Fixes #8442.

Problem

RedisRateLimiter forwards requestCount to every algorithm as the fourth script argument, but sliding_window_request_rate_limiter.lua never reads ARGV[4]: it admits while the number of recorded entries is below capacity and records exactly one entry per request. A rule with algorithmName=slidingWindow and requestCount=5 therefore admits five times the traffic of the same handle under tokenBucket, and remain_request is computed before the current request is recorded, so it always over-reports by one.

Approach

The script now honours requestCount: it admits only when last_requested + requested <= capacity, records requested units in the existing sorted set (members KEYS[2] .. ':' .. i, the per-call unique key already supplied by SlidingWindowRateLimiterAlgorithm.getKeys), and returns capacity - last_requested - requested after recording, clamped at zero on rejection. No new keys, no key-schema change and no migration: zcard remains the usage counter, total members stay bounded by capacity because admission requires last_requested + requested <= capacity, and the TTL is unchanged.

requestCount is a request count, so non-positive or missing values normalize to one request (fail-safe, never free traffic) and fractions round up to whole units, so a fractional count never admits more traffic than configured. No Java change is needed: RedisRateLimiter already passes the value, and RateLimiterPlugin only consumes the allowed flag, so the remainder is not client-visible. The token/leaky requestCount=0 quirk and the missing Admin requestCount field are pre-existing and out of scope.

This intentionally makes existing slidingWindow rules with requestCount > 1 enforce the configured weight instead of one unit per request.

Testing

Nine new deterministic cases run the real script against embedded Redis with fixed epoch arguments and windows of several seconds, so no sleeping is involved and a slow CI cannot expire a key mid-test. They cover the issue's 10-requests-at-requestCount-5 scenario (two admitted, matching tokenBucket), the exact recorded unit count, per-call member uniqueness, non-negative remainders, zero/negative normalization, fractional rounding, and whole-unit expiry. The two existing sliding-window expectations are updated from the pre-fix remainder values (100 -> 99, 1 -> 0); they now also pass the algorithm's real two-key list.

shenyu-plugin-ratelimiter 56 tests (7 new) green; Checkstyle and Apache RAT clean.

@Aias00 Aias00 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Stream Agent analysis

Sliding window Lua fix honors requestCount via a zadd loop but records non-unique ZSET members, so usage is under-counted and the limiter over-admits. The PR's own added tests assert ZCARD values the implementation cannot produce.

Evidence

  • sliding_window_request_rate_limiter.lua RIGHT line 49: redis.call('zadd', tokens_key, now, timestamp_key .. ':' .. i) builds the member only from the constant per-handle timestamp_key (KEYS[2]) and the loop index; successive calls to the same handle re-add identical members, so ZCARD never exceeds requested.
  • PR tests contradict the code: with FIXED_NOW and identical keys, slidingWindowHonoursRequestedCount asserts ZCARD=10 and slidingWindowUnitsAreUniquePerCall asserts ZCARD=4, but member collisions yield ZCARD=5 and ZCARD=2 respectively.
  • RedisRateLimiter.java:60 supplies requestCount as ARGV[4]; the script reads it, but corrective counting is defeated by non-unique members, causing the limiter to over-admit (rate-limit bypass).

Recommended next steps

  • Use a per-request unique member (e.g. a request-scoped UUID or monotonic counter) instead of timestamp_key .. ':' .. i, so repeated requests to the same handle accumulate distinct ZSET entries and ZCARD reflects true usage.
  • Re-run the added unit tests; slidingWindowHonoursRequestedCount (expects ZCARD=10) and slidingWindowUnitsAreUniquePerCall (expects ZCARD=4) currently assert counts the implementation cannot produce with constant per-handle members.

Scope: captured diff and GitHub check metadata only; no local tests were executed.

Task: eff07857-dc99-46dd-b7a5-e0914f3b849c · Commit: 549542248b5e31a92da6737de73aadf856f2a817

Review verdict: NEEDS_CHANGES

The sliding window script now reads ARGV[4]=requestCount and records requested zadd entries, but the ZSET member is timestamp_key .. ':' .. i, which is constant for a given handle id. Because KEYS[2] (timestamp_key) is fixed per handle, repeated requests to the same handle re-add identical members and ZCARD never grows beyond requested, so usage is under-counted and the limiter over-admits (defeating the sliding-window limit). The PR's own added tests assert ZCARD=10/4, which the implementation cannot produce, confirming the fix is incorrect.

Publication: REQUEST_CHANGES (VERIFIED_BLOCKING_FINDINGS)

  • shenyu-plugin/shenyu-plugin-fault-tolerance/shenyu-plugin-ratelimiter/src/main/resources/META-INF/scripts/sliding_window_request_rate_limiter.lua:49 [RIGHT] ZSET member timestamp_key .. ':' .. i is constant per handle (KEYS[2] is fixed for a given id), so repeated requests to the same handle re-add identical members; ZCARD never exceeds requested, under-counting usage and allowing the limiter to over-admit (rate-limit bypass). This also makes the PR's own tests asserting ZCARD=10/4 fail.

@konlue

konlue commented Nov 13, 2026

Copy link
Copy Markdown
Contributor Author

@Aias00 The finding rests on an assumption that doesn't hold: KEYS[2] (the timestamp_key woven into the member) is not fixed per handle. SlidingWindowRateLimiterAlgorithm.getKeys (shenyu-plugin-ratelimiter, SlidingWindowRateLimiterAlgorithm.java:43-47) prefixes it with a fresh id on every call — UUIDUtils.getInstance().generateShortUuid(), which is a snowflake nextId() (UUIDUtils.java:122) and is globally unique per invocation. Members therefore arrive as <fresh-id>.{id}.timestamp:1..n and are distinct for every script execution. Only KEYS[1] (the tokens sorted set that zcard counts) is fixed per handle — that is the usage counter; KEYS[2] is the per-call member namespace.

This uniqueness mechanism is pre-existing, not introduced here: the unmodified script already did zadd tokens_key now timestamp_key — one member per request carried by that same per-call key. The production path regenerates the keys on every request (RedisRateLimiter.java:59), so repeated requests to the same handle accumulate distinct members and zcard reflects true usage.

The added tests exercise exactly this boundary and pass against real Redis: slidingWindowUnitsAreUniquePerCall (RedisRateLimiterScriptsTest.java:224-240) makes two independent getKeys calls — two distinct key sets, not identical keys — and asserts ZCARD=4 after two admitted calls of requestCount=2; slidingWindowHonoursRequestedCount (lines 190-207) drives four invocations with requestCount=5 and asserts ZCARD=10. I re-ran the full RedisRateLimiterScriptsTest on the PR head commit: 13/13 green on embedded Redis, and the CI build job is green.

If a per-timestamp member (e.g. now .. ':' .. i) is preferred, note it would be strictly weaker: now is second-granularity here, so two calls within the same second would collide where the current per-call id never does — happy to switch if maintainers see it as clearer, but the current form is both correct and the smaller diff over the existing design.

This branch has not been deployed

No deployments
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.

[BUG] Sliding-window rate limiter ignores requestCount and over-reports remaining requests

2 participants