Nobody has started this yet — be first.
Business impact
This is the hardest class of production incident to catch: one specific customer/key silently hangs forever, with no error, no alert, and every system-wide dashboard looking perfectly healthy. Support hears "your API just hangs for us" while on-call sees nothing wrong -- that mismatch alone can burn hours or days before anyone even suspects a single-key deadlock, let alone finds the exact oversized-request-during-rollover trigger. The cost isn't just the affected customer's downtime; it's the disproportionate engineering time spent diagnosing a bug that gives almost no observable signal to go on.
Problem
A "defensive" early-return was recently added to SlidingWindowMemory.AllowN to reject oversized single requests (n bigger than the whole configured limit) before they can pollute the window bookkeeping. It returns before unlocking the mutex it's holding. Any request that hits this branch leaves that key's rollover lock permanently held -- every subsequent call for that same key (from any goroutine) blocks on e.rolloverMu.Lock() forever.
Current behavior
Inside the window-rollover critical section (between e.rolloverMu.Lock() and e.rolloverMu.Unlock()), a request with n larger than sw.limit, arriving exactly when a window rollover is due, causes an early return that skips the matching Unlock().
Expected behavior
However the oversized-n case should be handled (reject it, clamp it -- the ticket doesn't mandate a specific one), the mutex this code path took must always be released, on every exit path, including this one.
Steps to reproduce
sw := ratelimit.NewSlidingWindowMemory(5 /* limit /, 100time.Millisecond /* window */, time.Minute) sw.Allow(ctx, "user-1") // first-ever call, establishes windowStart, fine time.Sleep(150 * time.Millisecond) // now well into the next window sw.AllowN(ctx, "user-1", 999) // n=999 > limit=5, AND a rollover is due sw.Allow(ctx, "user-1") // got: hangs forever -- the rollover mutex was leaked by the early return above // want: returns promptly, whatever the outcome of the oversized request was
Why this matters
An unreleased lock on a per-key mutex doesn't crash anything visibly -- the process keeps running, other keys keep working fine, /healthz stays green -- but that one key is now permanently wedged: every future request for it hangs until it times out upstream (or forever, if nothing upstream has a timeout). In a real deployment this is one of the nastier classes of bug to diagnose, because nothing errors; requests for one specific key just never complete.
Suggested approach
The safest general fix for "a critical section might return early on any number of branches added later" is the same pattern already used everywhere else a mutex guards a function in this package (token_bucket.go, leaky_bucket.go): acquire the lock, then immediately defer the unlock, so every return path -- including ones added after you fix this bug -- is covered automatically. sliding_window.go's rollover section is the one place in the package doing manual (non-deferred) lock/unlock; consider whether that was ever actually necessary here.
Acceptance criteria
Verification
go test ./test/ -run TestTask15 -v
Hints (0/2)
Try it without hints first — the reading is the exercise.
Working on this ticket
Work on a branch named for the ticket — that's what you'll submit.
Branch off your fork
Sign in to create it automatically, or do it yourself:
$git checkout -b fix/sliding-window-deadlockFix it and commit
Meet every acceptance criterion, and add a test that would have caught this.
Push the branch
$git push -u origin fix/sliding-window-deadlockSubmit it below
Paste your fork URL and the branch name, with a short write-up of the root cause.
Questions
Ask about anything unclear in the ticket — the maintainer and anyone who has solved it can answer. Please don't post full solutions.
Sign in to ask a question or reply.
Sign inhttp://localhost:8080localhost:6380http://localhost:8081curl localhost:8080/healthz
curl localhost:8080/api/v1/rules
curl -s -X POST localhost:8080/api/v1/check -d '{"rule":"free-tier","key":"user-123"}' | jq
No Docker? STORE_BACKEND=memory is the default, so go run ./cmd/server alone (no Redis needed) exercises everything except the Redis-backed code paths.
Work the tickets in tasks/ (ordered easy → hard); each names one Go test in test/:
make test-race # go test -race -count=1 ./...
go test ./test/ -run TestTask01 -v # a single ticket
./test/run_all.sh # all 15, pass/fail summary
Level 1
Fix a bug
Read existing behaviour, correct it.