2.8 KiB
Task 6: Redis Cache Layer -- Complete
Summary
Created the Redis cache layer in internal/cache/redis.go with rule caching, channel caching, bulk invalidation, and rate limiting.
Files Changed
- Created:
internal/cache/redis.go-- Cache struct with full implementation - Modified:
go.mod-- Addedgithub.com/redis/go-redis/v9dependency - Modified:
go.sum-- Updated checksums
Implementation Details
NewCache(cfg config.RedisConfig)-- Connects to Redis usingconfig.RedisConfig(fixed from the brief's typomodel.config.RedisConfig)CachedRulestruct -- RuleID, TemplateID, Content, Conditions (JSON string)CachedChannelstruct -- ID, Type, Config (*json.RawMessage)GetRule/SetRule/InvalidateRule-- Keys:notify:rule:{sourceID}:{event}, TTL 5 minGetChannels/SetChannels/InvalidateChannels-- Keys:notify:channels:{ruleID}, TTL 5 minInvalidateBySource-- Scansnotify:rule:{sourceID}:*and deletes matchesInvalidateByTemplate-- Scans allnotify:rule:*keys and deletes themCheckRateLimit-- Sliding window via sorted set (ZRemRangeByScore + ZCard), keyratelimit:{sourceID}Close-- Closes the Redis client
Verification
go build ./internal/cache/...completed with no errors- Commit:
3f75699with message "feat: redis cache layer with rule/channel caching and rate limiter"
Fix Section -- Race Condition and Error Handling
Issue 1: TOCTOU race condition in CheckRateLimit
The original implementation used a pipeline where ZRemRangeByScore (cleanup) and ZCard (count check) ran together, but the subsequent ZAdd (add request) ran separately in Go code. This created a time-of-check-to-time-of-use window where concurrent requests could all pass the limit check before any of them recorded the add.
Fix: Replaced the pipeline approach with an atomic Lua script. The entire operation — cleanup, count check, and conditional add — now runs as a single atomic unit inside Redis, eliminating the race window. The script also switched from second-precision timestamps to millisecond-precision (time.Now().UnixNano() / 1e6) for unique sorted set members.
Issue 2: Discarded Del errors in InvalidateBySource and InvalidateByTemplate
Both invalidate methods called c.rdb.Del() inside a scan loop but did not check the returned error, silently swallowing deletion failures.
Fix: Both methods now collect errors from each failed Del call into an []error slice. If any deletions failed, an aggregated error is returned quoting the count and the first error. The scan iterator error (iter.Err()) is still returned when no deletion errors occurred.
Verification
go build ./internal/cache/...completed with no errorsgo vet ./internal/cache/...completed with no warnings