Consolidate rate limit Redis operations into a single EVAL call
Consolidate the rate limit evaluator's Redis operations into a single `EVAL` (Lua script) call, replacing the current multi-round-trip approach. Parent epic: https://gitlab.com/groups/gitlab-com/gl-infra/-/work_items/2021 Context: https://gitlab.com/gitlab-org/ruby/gems/labkit-ruby/-/merge_requests/274#note_3302529826 ## Problem The rate limit evaluator currently makes 2-3 Redis round-trips per check: 1. `INCR` — increment counter 2. `EXPIRE` — set TTL (only on first write when count == 1) 3. `TTL` — read remaining time for `reset_at` MR !274 optimizes this by pipelining INCR + TTL into one round-trip, with a separate EXPIRE when needed. This issue takes it further: a single `EVAL` call that does everything atomically. ## Proposed Lua script ```lua local count = redis.call('INCR', KEYS[1]) if count == 1 then redis.call('EXPIRE', KEYS[1], ARGV[1]) end local ttl = redis.call('TTL', KEYS[1]) return {count, ttl} ``` ## Benefits - **1 round-trip** always (vs 1-2 with the pipeline approach) - **Atomic**: the INCR, conditional EXPIRE, and TTL happen as a single atomic operation — no race between INCR and EXPIRE - **Less Ruby overhead**: the counter logic moves to Redis, reducing per-request Ruby work on the hot path ## Considerations - Use `EVALSHA` with `SCRIPT LOAD` fallback for performance (avoid sending the script text on every call) - Single-key operation — Redis Cluster safe (`KEYS[1]` only) - `FakeRedis` in tests will need an `eval` method, or tests mock the eval response - The Redis client passed to labkit must support `eval`/`evalsha` — standard for all Redis clients GitLab uses ## Acceptance Criteria - `incr_with_ttl` in `Labkit::RateLimit::Evaluator` replaced with a single `EVAL` call - Script cached via `EVALSHA` with `SCRIPT LOAD` fallback on `NOSCRIPT` error - All existing rate limit tests pass (behavior unchanged) - New test covering the Lua script execution path
issue