To solve this, keep an in-memory map of entity_id → last_seen (epoch seconds). As events arrive, update last_seen. Periodically (or on-demand) compute stale entities by comparing last_seen to now - threshold. Use a heap (min-heap) to avoid scanning all entities on every check when there are many entities: push (last_seen, entity_id) on updates and lazily pop expired entries. This gives efficient stale detection without full scans.python
import time
import heapq
from typing import Iterable, Dict, Set
def monitor_freshness(stream_iterable: Iterable[dict], freshness_threshold_seconds: float):
"""
Consumes events with {'entity_id', 'event_time'} (event_time: epoch seconds).
Yields a tuple (now, set_of_stale_entity_ids) whenever called (example yields periodically).
This implementation updates state on each event and maintains a min-heap for efficient stale checks.
"""
last_seen: Dict[str, float] = {} # entity_id -> last seen timestamp
heap = [] # list of (last_seen, entity_id) for lazy expiry
for event in stream_iterable:
eid = event['entity_id']
t = float(event['event_time'])
# Update last_seen if event is newer
prev = last_seen.get(eid)
if prev is None or t > prev:
last_seen[eid] = t
heapq.heappush(heap, (t, eid))
# Example: on each event, compute stale set (could be triggered less frequently)
now = time.time()
cutoff = now - freshness_threshold_seconds
stale: Set[str] = set()
# Pop heap entries older than cutoff; verify against last_seen for staleness
while heap and heap[0][0] <= cutoff:
ts, eid0 = heapq.heappop(heap)
# If this heap record matches current last_seen, it's truly stale
if last_seen.get(eid0) == ts:
stale.add(eid0)
yield now, stale
Key points:- Use dict for O(1) updates and lookups.- Use min-heap to avoid full scans: amortized efficient expiry.- Lazy deletion handles multiple updates per entity without expensive removals.Time complexity:- Update: O(log n) for heap push; lookup O(1).- Stale extraction: O(k log n) where k expired entries popped.Space: O(n) for map + heap (heap may contain duplicates proportional to updates).Edge cases:- Out-of-order events: code keeps the max timestamp per entity.- Clock skew: use event_time vs ingestion time carefully; choose authoritative clock.- Very high cardinality: memory pressure; consider eviction policies (LRU, TTL), approximate sets (Bloom filters) for alerts.Distributed extensions:- Sharding: partition entities by hash(entity_id) across workers; each worker runs same logic for its shard.- External state store: keep last_seen in Redis (hash) or RocksDB (local keyed state) to persist/scale; use sorted sets in Redis to get ranged expired entities efficiently (ZADD with score=timestamp; ZRANGEBYSCORE to find stale).- Exactly-once / fault tolerance: checkpoint state (e.g., Flink state backend, Kafka streams with changelog topics) so shards can recover.- Global stale view: aggregate per-shard stale sets via periodic reduction (e.g., push to central service or Kafka topic) or use consistent hashing so aggregation is minimal.This approach balances per-event low-latency updates with efficient stale detection at scale and maps well to streaming frameworks and external stores for production ML pipelines.