Skip to content

fix(rate-limiter): namespace EWMA keys away from sliding-window keys - #945

Merged
cameri merged 3 commits into
mainfrom
fix/ewma-key-namespace
Oct 24, 2026
Merged

cameri merged 3 commits into
mainfrom
fix/ewma-key-namespace

Conversation

@chappie-daemon

Copy link
Copy Markdown
Collaborator

Takes @cameri's call in the issue and @Perlover's diagnosis: namespace the EWMA keys. Closes #930.

The bug, reproduced

Seeded a real Redis 6.2 with a sliding-window sorted set at 1.2.3.4:connection:120000 and ran the shipped EWMA script against it:

ERR Error running script ... @user_script:6: WRONGTYPE Operation against a key holding the wrong kind of value

Same error, same line as the report. Both strategies build their key the same way (<identity>:<bucket>:<period>) but store different types — a sorted set versus a hash — so any key left by sliding_window breaks every ewma request that touches it.

The fix

EWMARateLimiter now writes under an ewma: prefix:

export const EWMA_KEY_PREFIX = 'ewma:'
export const ewmaKey = (key: string): string => `${EWMA_KEY_PREFIX}${key}`

A prefix rather than the self-healing TYPE/DEL guard the issue also suggested. Deleting the key would destroy the sliding window's state, so switching strategy back would silently start from a fresh window — and that switch is a normal operation, not an upgrade path.

Affected call sites are unchanged; the prefix is applied once, inside the limiter, so nothing has to remember it.

Verification

Check Before After
hit() against a legacy zset WRONGTYPE @user_script:6 returns normally
key written …:connection:120000 (hash, collided) ewma:…:connection:120000 (hash)
legacy zset — untouched, still zset
TTL on the new key — 120s, as before

The before/after used the built EWMARateLimiter class against real Redis 6.2, not a stub — a mocked adapter cannot produce a WRONGTYPE, which is exactly why the existing unit test never caught this.

Tests. A unit test pins the key the script receives. An integration scenario seeds a real sliding-window zset under the shared name and still expects the client to be rate limited, so a regression fails on behaviour. The existing @rate-limiter step now seeds the namespaced key, since seeding the old name would leave the limiter on a fresh window and the original scenario would stop covering anything.

Full unit suite: 2521 passing, 5 failing — the same user-subscription-repository failures at base, untouched by this change. tsc --noEmit and biome clean.

Upgrade note

No manual Redis cleanup: old zsets stay where they are and simply stop being read. They expire on their own TTL, and the ewma: keys start empty, so the first request after the upgrade is unrestricted — the same fresh-start a restart already gives a sliding window.

@changeset-bot

changeset-bot Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 37685c9

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
nostream Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

Copy link
Copy Markdown
Contributor

🔍 Site preview for this PR (includes all changes on this branch): https://nostream-web-pr-945.cameri.workers.dev/preview/945/

The https://ema.tnsor.network/preview/945/ form serves the same build once this PR merges — the production router that proxies /preview/* ships with it.

Updates with every push to this PR; deleted when the PR closes.

@greptile-apps

greptile-apps Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge; no actionable new issues were found.

Summary

EWMA now adds ewma: to Redis keys inside EWMARateLimiter.hit(). Sliding-window keys stay unchanged.

  • EWMA rate limits keep their Redis state under a separate key.

Reviews (3) · Last reviewed commit: "Merge branch 'main' into fix/ewma-key-na..." · Reviewed by Greptile

Comment thread .github/workflows/release.yml Outdated
Comment thread docs/CI-MIGRATION.md Outdated
Comment thread docs/DEPLOY-RUNBOOK.md Outdated
@coveralls

coveralls commented Oct 23, 2026 •

Copy link
Copy Markdown
Collaborator

Coverage Report for CI Build 40101173613

Warning

Build has drifted: This PR's base is out of sync with its target branch, so coverage data may include unrelated changes.
Quick fix: rebase this PR. Learn more →

Coverage increased (+0.003%) to 75.742%

Details

  • Coverage increased (+0.003%) from the base build.
  • Patch coverage: 2 of 2 lines across 1 file are fully covered (100%).
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 11578
Covered Lines: 9176
Line Coverage: 79.25%
Relevant Branches: 5983
Covered Branches: 4125
Branch Coverage: 68.95%
Branches in Coverage %: Yes
Coverage Strength: 786.8 hits per line

💛 - Coveralls

EWMA and sliding_window store different Redis types under the same key name:
a sliding window keeps a sorted set, EWMA keeps a hash. Both write
`<identity>:<bucket>:<period>`, and 2.x shipped only the sliding window, so a
cache volume carried across the upgrade holds sorted sets where the EWMA script
then runs HGET - a WRONGTYPE error raised on every connection and every message
until the old key expires. Switching `limits.rateLimiter.strategy` back and
forth does the same thing to a live instance.

EWMA now writes under an `ewma:` prefix. A prefix rather than a
delete-if-wrong-type guard, so both strategies keep their own window and a
switch back to sliding_window resumes it instead of starting from zero.

Reproduced before the change on a real Redis 6.2 with a zset seeded at the
shared key name: `@user_script:6: WRONGTYPE`, the same error and line the issue
reports. After it, the same call returns normally, writes a hash under
`ewma:`, and leaves the legacy zset untouched.

The unit test pins the key the script receives. The integration scenario seeds
a real sliding-window zset under the shared name and asserts the client is still
rate limited, so a regression fails on the behaviour rather than on a string.
@cameri
cameri merged commit 062ee00 into main Oct 24, 2026
7 checks passed
@cameri
cameri deleted the fix/ewma-key-namespace branch October 24, 2026 14:08
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] Upgrade from 2.x: EWMA rate limiter hits WRONGTYPE on sliding-window keys left in Redis

3 participants