Katkat

Aggregate Recent Message Events And Active Chats

OpenAISoftware Engineer · Onsite

Last updated

Design an in-memory aggregation system that tracks rolling window message counts and distinct active entities across chats with bounded memory and O(1) amortized operations.

Problem Statement

Design a real-time event aggregation engine for a chat platform that maintains metrics over a rolling 5-minute (300-second) window. An event is considered within the window at a given current_time if:

current_time - 300 < timestamp <= current_time

Implement the ChatEventAggregator class with the following methods:

  • __init__(window_size: int = 300): Initializes the aggregator with a rolling window duration in seconds (default is 300).
  • record_message(timestamp: int, chat_id: str, user_id: str) -> None: Records that user_id sent a message in chat_id at unix epoch time timestamp (in seconds). You may assume incoming event timestamps are monotonically non-decreasing.
  • get_active_chat_count(current_time: int) -> int: Returns the number of unique chats that have had at least one message sent in the rolling window relative to current_time.
  • get_active_user_count(current_time: int, chat_id: str | None = None) -> int: If chat_id is provided, returns the number of distinct users who sent a message in that specific chat in the rolling window. If chat_id is None, returns the total number of distinct active users across all chats combined.
  • get_total_messages(current_time: int) -> int: Returns the total number of messages sent across all chats in the rolling window.

All queries must evict stale data and maintain strictly bounded memory: when an entity's (user or chat) active count drops to 0, it should not linger indefinitely in internal maps.

Constraints

  • window_size is an integer 1 = latest timestamp seen so far.
  • Total operations up to 2 * 10^5.
  • String identifiers chat_id and user_id have lengths between 1 and 32 characters.
  • Target time complexity: Amortized O(1) per record_message and query call.

Example

aggregator = ChatEventAggregator(window_size=300)

aggregator.record_message(timestamp=100, chat_id="c1", user_id="u1")
aggregator.record_message(timestamp=150, chat_id="c1", user_id="u2")
aggregator.record_message(timestamp=200, chat_id="c2", user_id="u1")

# At t = 250, all messages (100, 150, 200) are within (250 - 300, 250] = (-50, 250]
aggregator.get_total_messages(250)         # Returns 3
aggregator.get_active_chat_count(250)      # Returns 2 ("c1", "c2")
aggregator.get_active_user_count(250)      # Returns 2 ("u1", "u2")
aggregator.get_active_user_count(250, "c1")# Returns 2 ("u1", "u2")

# At t = 420, window is (120, 420]. Message at t=100 is evicted.
aggregator.get_total_messages(420)         # Returns 2 (timestamps 150, 200)
aggregator.get_active_user_count(420, "c1")# Returns 1 ("u2" only; "u1" expired for c1)
aggregator.get_active_user_count(420)      # Returns 2 ("u1" is still active in c2, "u2" in c1)

# At t = 501, window is (201, 501]. Timestamps 150 and 200 are also evicted.
aggregator.get_active_chat_count(501)      # Returns 0
aggregator.get_total_messages(501)         # Returns 0

Edge Cases to Consider

  • Queries called at timestamps far in the future with no intervening messages (should evict all expired events).
  • Multiple messages from the same user in the same chat within the window.
  • Memory leaks: ensure empty keys/sets are cleaned up upon eviction.

Hints

Hint 1

Use a double-ended queue (deque) to store chronological events, which allows evicting expired events from the left in amortized O(1) time.

Hint 2

Maintain frequency maps for active occurrences (global user counts, chat message counts, and per-chat user counts) so you know exactly when a distinct count drops to zero.

Hint 3

Make sure to delete keys from frequency dictionaries when their counts reach zero to avoid unbounded memory growth from one-off IDs.

Follow-up questions

  • How would you redesign this system if events can arrive out of order by up to K seconds?
  • If the ingestion rate exceeds the capacity of a single machine (e.g., millions of messages per second), how would you partition and aggregate this state across a cluster?
  • How would your memory management strategy change if tracking billions of distinct users where exact counts are too memory-intensive?