Files
aiaa-notification-server/.superpowers/sdd/task-6-report.md
T

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 -- 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