# 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` -- Added `github.com/redis/go-redis/v9` dependency - **Modified:** `go.sum` -- Updated checksums ## Implementation Details - `NewCache(cfg config.RedisConfig)` -- Connects to Redis using `config.RedisConfig` (fixed from the brief's typo `model.config.RedisConfig`) - `CachedRule` struct -- RuleID, TemplateID, Content, Conditions (JSON string) - `CachedChannel` struct -- ID, Type, Config (*json.RawMessage) - `GetRule` / `SetRule` / `InvalidateRule` -- Keys: `notify:rule:{sourceID}:{event}`, TTL 5 min - `GetChannels` / `SetChannels` / `InvalidateChannels` -- Keys: `notify:channels:{ruleID}`, TTL 5 min - `InvalidateBySource` -- Scans `notify:rule:{sourceID}:*` and deletes matches - `InvalidateByTemplate` -- Scans all `notify:rule:*` keys and deletes them - `CheckRateLimit` -- Sliding window via sorted set (ZRemRangeByScore + ZCard), key `ratelimit:{sourceID}` - `Close` -- Closes the Redis client ## Verification - `go build ./internal/cache/...` completed with no errors - Commit: `3f75699` with 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 errors - `go vet ./internal/cache/...` completed with no warnings