nic-6443 opened a new pull request, #13996:
URL: https://github.com/apache/apisix/pull/13996

   `$rate_limiting_info` only carries the key, limit, remaining and reset, 
which isn't enough to tell from an access log why a request was let through or 
rejected, or when exactly a window started. This PR appends the window the 
request was counted in, with millisecond timestamps. The four existing fields 
are unchanged and still come first.
   
   Fixed window:
   
   ```json
   
{"rate_limiting_key":"/apisix/routes/1:1:127.0.0.1","rate_limiting_limit":10,"rate_limiting_remaining":3,"rate_limiting_reset":42,"window_type":"fixed","window_size_ms":60000,"decision":"allowed","cost":1,"evaluated_at_ms":1759212345678,"current_window":{"start_ms":1759212300123,"end_ms":1759212360123,"count":7,"created":false}}
   ```
   
   Sliding window:
   
   ```json
   
{"rate_limiting_key":"/apisix/routes/1:1:127.0.0.1","rate_limiting_limit":10,"rate_limiting_remaining":3,"rate_limiting_reset":14,"window_type":"sliding","window_size_ms":60000,"decision":"allowed","cost":1,"evaluated_at_ms":1759212345678,"current_window":{"id":29320205,"start_ms":1759212300000,"end_ms":1759212360000,"count":4},"previous_window":{"count":10,"weight":0.238700,"weighted_count":2.387}}
   ```
   
   With `sync_interval`, a 
`"delayed_sync":{"synced_at_ms":…,"synced_count":…,"local_delta":…}` object is 
added after `evaluated_at_ms`, and `current_window.count` becomes the estimate 
`synced_count + local_delta + cost`. `decision` is `allowed`, `rejected` or 
`error`, and an `error` only carries `window_type`, `window_size_ms` and 
`decision`, so a Redis failure no longer looks like a rejection in the log. 
`count` is the counter after this request and `cost` is what this request added 
to it, which is needed to reconstruct the decision: a fixed window counts 
rejected requests too, a sliding window and delayed sync don't.
   
   This is observability only. The limiters just pass out values they already 
have, as an extra return value of `incoming()`/`commit()`, plus three more 
fields in the quota JSON that the delayed syncer already writes to the shared 
dict. There is no extra shared dict or Redis call, no Redis script change, and 
no change to any decision. So a few fields are `null` where the code path 
doesn't have the value:
   
   - `current_window.created` with the Redis policies
   - `current_window.count` when the local fixed window rejects 
(`resty.limit.count` drops the counter on rejection)
   - the delayed-sync snapshot fields (`synced_count`, and the sliding window 
fields) until the first sync after an upgrade
   
   `delayed_sync.local_delta` is the local delta the decision subtracted. Right 
after a refresh of an expired cached quota, that delta was already flushed into 
`synced_count`, which is the double count #13950 fixes. With #13950 it is 0 
there, as it should be.
   
   The JSON is built in `core.utils.set_var_rate_limiting_info`, which still 
accepts the old five arguments.
   
   Two small fixes found along the way, in their own commits:
   
   - The key wasn't JSON-escaped, so a quote or backslash in, for example, a 
header-based key made the variable invalid JSON.
   - Local fixed window: the end time was recorded whenever the counter equaled 
the request's cost. ai-rate-limiting's zero-cost access check matches that on 
every call while the counter is 0, and so does the first real charge after it, 
so each of them pushed the recorded end time later while the counter kept its 
original TTL, and `X-AI-RateLimit-Reset` over-reported. The end time is now 
only recorded when the window has none yet (`add` instead of `set`). 
`t/plugin/ai-rate-limiting.t` TEST 42 fails without it (reset `3` instead of 
~`1.5`).
   
   Per-request overhead: I ran wrk2 at a fixed 10k req/s against a single 
worker with one local rule and the access log off, so this is the cost every 
request pays whether or not the variable is logged. Over 8 interleaved rounds 
against master, worker CPU per request was 52.1 vs 52.2 µs for the fixed window 
and 54.6 vs 54.6 µs for the sliding window (medians). The paired per-round 
differences stay within the ±3 µs run-to-run noise. Benchmarking 
`set_var_rate_limiting_info` alone puts the added formatting at about 0.7 µs 
(fixed) and 1.1 µs (sliding) per call with the JIT off, and 0.1 / 0.3 µs with 
it on. The common cases are formatted in a single `string.format` over integers 
for that reason.
   
   Tests: `t/plugin/limit-count-rate-limiting-info.t` covers local, redis and 
redis-cluster × fixed/sliding, delayed sync for both window types, and the 
degraded error path. It decodes the variable in the log phase and checks the 
timestamps against each other, so it doesn't depend on wall-clock values. The 
existing access-log assertions in `limit-count-variable.t` and `limit-count5.t` 
are adjusted for the appended fields. The docs gain a section on the variable 
in `limit-count.md` (en/zh).
   
   - [x] I have explained the need for this PR and the problem it solves
   - [x] I have explained the changes or the new features added to this PR
   - [x] I have added tests corresponding to this change
   - [x] I have updated the documentation to reflect this change
   - [x] I have verified that this change is backward compatible
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to