Nobody has started this yet — be first.
Business impact
The entire point of an idempotency key is that it survives long enough for a retried request to find it. If the background janitor deletes keys before they expire instead of after, the at-most-once guarantee this whole project exists to provide breaks down for any request slower than one sweep interval (one minute by default): a retry that lands after the key was wrongly evicted sees Proceed instead of Replay/InProgress and re-executes the operation -- silently reintroducing the exact duplicate-charge risk this library is supposed to eliminate. The other half of the same bug (expired keys are never evicted at all) is a slow, unbounded memory leak in the opposite direction.
Problem
MemoryStore's background janitor is supposed to delete entries whose TTL has passed (now is after expiresAt). The comparison it actually uses is backwards.
Current behavior
A key created with a 5-second TTL is gone from the store within roughly one sweep interval (as little as ~20ms in a fast-sweep configuration, up to the default 1-minute interval otherwise) -- long before its TTL actually elapses. Meanwhile, a key that has genuinely expired is never swept.
Expected behavior
Only entries whose expiresAt is in the past should ever be deleted by the janitor; a key with time remaining on its TTL must survive sweeps indefinitely until it either completes its lifecycle normally or its TTL genuinely elapses.
Steps to reproduce
store := idempotency.NewMemoryStoreWithSweepInterval(5time.Second, 20time.Millisecond) defer store.Close() store.Begin(ctx, "fresh-key", "fp", 5*time.Second) time.Sleep(150 * time.Millisecond) // several sweeps, nowhere near the 5s TTL _, ok, _ := store.Get(ctx, "fresh-key") fmt.Println(ok) // got: false, want: true
Why this matters
This is the worst kind of correctness bug: it's invisible in a quick manual test (a curl sent right after the fix would "work"), but corrupts the guarantee under exactly the condition -- any request slower than the janitor's sweep interval -- that idempotency keys with a multi-hour TTL were provisioned to handle in the first place.
Suggested approach
sweep() in pkg/idempotency/janitor.go compares now against each entry's expiresAt with a single boolean condition. Think about which direction "has this entry's time run out" should point, and compare that to what the condition currently does -- the fix is a single method call swap (.After vs. .Before), not a restructuring.
Acceptance criteria
Verification
go test ./test/ -run TestTask07 -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/idempotency-janitor-cutoff-signFix it and commit
Meet every acceptance criterion, and add a test that would have caught this.
Push the branch
$git push -u origin fix/idempotency-janitor-cutoff-signSubmit 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:8080POST /v1/chargesTo run against Redis (the multi-replica-safe backend) with zero local setup:
docker compose up --build # app + redis, hot-reloading on save
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
Level 1
Fix a bug
Read existing behaviour, correct it.