This PR addresses cache consistency issues across event-indexer replicas by:
- Adding TTL support to EventCache to bound staleness
- Adding leadership-loss cache invalidation
- Exposing ApiCache backend status in the health endpoint
- Adding comprehensive adversarial tests
- Updating documentation
Added:
CachedEntrystruct wrapping events with insertion timestampsttl_secsfield toEventCache(default: 5 minutes)with_ttl()constructor for custom TTL configurationis_expired()helper method checking entry age- TTL filtering in
get()andget_by_match()methods
Effect:
- Former leaders can no longer serve indefinitely-stale data
- Staleness is bounded to TTL window (5 minutes default)
- After TTL, cache misses force DB fallback (authoritative source)
Tests Added:
ttl_causes_cache_miss_after_expiryget_by_match_filters_expired_entriesreinsertion_refreshes_ttl
Modified event_poller():
- Added
was_leaderstate tracking - Detects leadership loss transition
- Calls
cache.clear()immediately on leadership loss - Logs warning when clearing cache
Effect:
- Eager invalidation on known leadership changes
- Complements TTL for immediate consistency on failover
- Prevents stale reads from former leaders
Modified HealthResponse:
- Added
cache_backend: Stringfield - Added
cache_shared: boolfield
Modified health_check():
- Reports
cache_backend(fromApiCache::backend_name()) - Reports
cache_shared(fromApiCache::is_shared()) - Returns
503 degradedwhencache_sharedis false
Effect:
- External monitoring can detect non-shared cache degradation
- No longer a silent failure logged only to STDERR
- Load balancers can route away from degraded instances
Updated ApiCache::from_config():
- Corrected warning from "latency will be higher" to describe actual consistency risk
- Now explicitly states: "multiple replicas will serve different cached responses"
- References health endpoint reporting
Effect:
- Operators understand this is a correctness issue, not just performance
- Clear guidance on detection mechanism
New test file covering:
- Split-brain scenario: two replicas with different cached state
- Convergence after TTL expiry
- Former leader staleness bounded by TTL
- ApiCache backend detection via
is_shared()andbackend_name() - Leadership loss cache clearing
Test scenarios:
two_replicas_different_events_diverge_before_ttl- proves the bug exists before TTLtwo_replicas_converge_after_ttl- proves TTL bounds stalenessformer_leader_serves_stale_data_within_ttl_window- proves bounded staleness windowapi_cache_redis_unreachable_detectable- proves health endpoint detectionleadership_loss_clears_cache- proves eager invalidation
Added tests:
health_check_reports_cache_backend- verifies cache_backend fieldhealth_check_reports_cache_shared- verifies cache_shared fieldhealth_check_degraded_when_cache_not_shared- verifies degraded statushealth_check_disabled_cache_not_shared- verifies disabled cache behavior
Added sections:
- TTL and staleness bounds - explains TTL mechanism and bounded staleness
- Leadership-loss invalidation - documents eager cache clearing
- Multi-replica consistency - documents consistency guarantees
- Redis unavailable (ApiCache degradation) - failure mode documentation
Updated:
- LRU Cache section with TTL details
- Failure Modes section with Redis fallback monitoring
- TTL mechanism added to EventCache
- Leadership-loss cache clearing implemented
- Health endpoint exposes cache backend status
- ApiCache warning message corrected
- Adversarial tests cover split-brain scenarios
- Health check tests cover new fields
- Documentation updated with consistency guarantees
- All existing tests should still pass (LRU semantics unchanged)
- DB-authoritative fallback path preserved
After this fix:
Before: Former leader serves stale cache indefinitely → unbounded staleness
After: Staleness bounded to max(TTL_SECS, time_since_detected_leadership_loss)
- Leadership loss detected → immediate cache clear
- Undetected failure / split-brain → TTL bounds staleness to 5 minutes
- All replicas converge to DB state after window
- EventCache default TTL: 5 minutes (300 seconds)
- Can be overridden via
EventCache::with_ttl(size, ttl_secs) - No breaking API changes
- Existing cache users automatically get TTL behavior
- Health endpoint response schema expanded (backwards compatible JSON)