Files

48 lines
2.8 KiB
Markdown

# 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