Requirements & constraints:- Input: 100k events/sec, high ingest, deduplicate by event-id within short window (minutes–hours).- Memory: 1 GB for state; must be approximate for long window, exact cache for hot items.- Latency: <100ms per decision. False positives acceptable (drop unique rarely); false negatives should be minimized.- Durability: Minimal operator restarts; filters can be rebuilt from short-term persistent log.High-level multi-level design:1. Front: fast exact short-term cache (in-memory LRU hash) holding recent K IDs (e.g., 10M entries if fits).2. Mid: Counting Bloom Filter (CBF) for sliding window membership with decay/periodic rotate to forget old items.3. Back: Count-Min Sketch (CMS) for frequency estimates to assist admission (prevent poisoning by high-volume IDs).4. Write-ahead log (append-only) for recent events on local SSD for recovery and rebuilding filters.Component responsibilities:- Ingest worker: validate, hash id, consult levels, decide emit/drop, append to WAL, update structures.- Eviction manager: maintain LRU size, rotate Bloom filters periodically (e.g., every T minutes), decrement CBF counts or swap to new filter to implement sliding window.- Recovery service: replay WAL up to a cutoff to reconstruct LRU and CBF/CMS.Data flow:Event -> Ingest worker -> LRU exact check -> CBF check -> CMS evaluate (admit or not) -> emit/drop -> append WAL -> update LRU/CBF/CMS.Memory budgeting (example):- LRU: 200MB (store recent 2M compact IDs using 8-byte hashed keys + overhead)- CBF: 600MB (counting buckets)- CMS: 200MBPseudocode (admission, eviction, recovery):python
# compact hash function
def h(id): return farmhash64(id) # 64-bit
# Admission logic
def admit(event_id):
key = h(event_id)
if LRU.contains(key):
return DROP # exact duplicate
if CBF.might_contain(key):
# possible duplicate
return DROP_WITH_FP_RATE # acceptable false positive
# use CMS to check heavy hitter protection
if CMS.estimate(key) > HEAVY_THRESHOLD:
# protect against skew: prefer admitting to avoid dropping many uniques from heavy ID? or vice-versa
# here we still insert but mark as heavy
pass
# new item
LRU.insert(key)
CBF.add(key)
CMS.update(key, 1)
WAL.append(key)
return EMIT
# Periodic eviction / rotate (run every T seconds)
def rotate_filters():
# use double-buffered Bloom filters to implement sliding window
old_bf = bf_old
bf_old = bf_current
bf_current = CountingBloom(size=...) # fresh zeroed
# To decay counts: swap and rebuild CMS if needed
# Optionally replay subset of WAL for last window to restore precise counts
# Recovery (on restart)
def recover():
# Read WAL from last checkpoint position
for key, ts in WAL.read_from(checkpoint):
if ts >= now - WINDOW:
LRU.insert(key) # up to capacity
CBF.add(key)
CMS.update(key,1)
Key trade-offs and reasoning:- Bloom/CBF offers compact membership with controlled false positives; Counting Bloom supports deletions/decay by using count buckets or periodic rotation.- LRU ensures zero-FP for very recent items; tuning LRU size reduces FP at cost of memory.- CMS helps detect heavy hitters so we can bias admission/eviction policies (prevent poisoning).- WAL allows bounded recovery without full upstream replay.- Tune parameters: BF false-positive p, number of hash functions k, CMS width/depth given 1GB budget.Edge cases:- Filter saturation: when CBF counters approach max, trigger rotate and partial WAL replay to reconstruct only active window.- Clock skew: use event timestamps and windowing consistent across workers.- Hot keys: shard by hash to distribute state; each shard keeps its own LRU/filters.This multi-level approach balances memory, throughput, and acceptable error rates for ML pipelines that consume deduplicated logs.