fix(collection): fix race causing retained cache entries without expiry timers by littfed · Pull Request #5834 · zeromicro/go-zero · GitHub
Skip to content

fix(collection): fix race causing retained cache entries without expiry timers - #5834

Open
littfed wants to merge 1 commit into
zeromicro:masterfrom
littfed:fix/issue-5803
Open

littfed wants to merge 1 commit into
zeromicro:masterfrom
littfed:fix/issue-5803

Conversation

@littfed

@littfed littfed commented Oct 15, 2026

Copy link
Copy Markdown
Contributor

Fixes #5803

Description

Under concurrent Set / SetWithExpire calls, collection.Cache could retain entries in Cache.data indefinitely without active expiry timers in TimingWheel. Consequently, Get continued returning hits beyond the configured TTL.

Root causes:

  1. When a timer expired, TimingWheel invoked cache.Del(key). Del invoked c.timingWheel.RemoveTimer(key) outside c.lock. If a concurrent SetWithExpire registered a new timer for the same key in between, that new timer was cancelled by the lingering RemoveTimer call, leaving the new entry in c.data without any timer.
  2. In SetWithExpire, MoveTimer was invoked when the key was present in c.data. However, if the old timer had already expired or been dequeued from tw.timers, moveTask silently did nothing without registering a new timer.
  3. The expiration callback did not verify whether an entry had been overwritten by a newer Set before deleting.
  4. Get / doGet did not validate an expiration deadline, allowing expired entries to be served if timer execution lagged under load.

Solution

  1. Introduce cacheEntry tracking a monotonic generation id and an expiration deadline.
  2. Synchronize SetTimer and RemoveTimer under Cache.lock to preserve registration and invalidation ordering.
  3. In SetWithExpire, always call SetTimer with the entry generation id so that timers are always properly scheduled or moved.
  4. Replace cache.Del in the TimingWheel callback with delIfMatch(key, id), ensuring only the entry matching the expired generation ID is removed, and avoid calling RemoveTimer for an already-expired timer.
  5. In doGet, validate entry.deadline and clean up if expired, guaranteeing callers never receive stale data.

Tests

…ry timers

Under concurrent Set operations, entries could be retained in Cache.data
indefinitely without active timers in TimingWheel due to races between
timer expiration callbacks, SetWithExpire, and MoveTimer/RemoveTimer.

1. Track entry generation ID and deadline in cacheEntry.
2. Synchronize SetTimer and RemoveTimer invocations under Cache.lock to
   preserve registration order.
3. In the timing wheel expiration callback, only delete entries matching
   the expired generation ID without invoking RemoveTimer.
4. Validate expiration deadline during doGet to prevent serving stale data.
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: collection.Cache can retain entries without expiry timers after concurrent Set calls

1 participant