Katkat

Add Sliding-Window Get and Put Activity Metrics to a Key-Value Store

DatabricksSoftware Engineer · Onsite

Last updated

Design and implement an in-memory key-value store that tracks read and write throughput (operations per second) over configurable sliding windows.

Problem Statement

Design an in-memory key-value store that tracks operational activity metrics over sliding time windows. In addition to standard get and put operations, the store must report the average rate of operations per second (average_gets and average_puts) over arbitrary sliding windows up to one week.

Implement the MetricsKVStore class:

  • MetricsKVStore(): Initializes the key-value store and metrics tracker.
  • put(key: str, value: str, timestamp: int) -> None: Stores the key-value pair and records a put event at timestamp (in seconds).
  • get(key: str, timestamp: int) -> str | None: Retrieves the value associated with key (or None if absent) and records a get event at timestamp (in seconds).
  • average_gets(window_seconds: int, timestamp: int) -> float: Returns the average number of get operations per second within the half-open or closed window [timestamp - window_seconds + 1, timestamp]. If window_seconds float: Returns the average number of put operations per second within [timestamp - window_seconds + 1, timestamp].

All timestamps are integers representing Unix time in seconds and are provided in non-decreasing order across operations.

Example

kv = MetricsKVStore()

kv.put("a", "1", timestamp=10)
kv.put("b", "2", timestamp=10)
kv.get("a", timestamp=15)          # returns "1"

# Window of 10 seconds at t = 15 covers range [6, 15]
# Total gets in [6, 15]: 1 at t=15 -> average = 1 / 10 = 0.1
kv.average_gets(10, timestamp=15)  # returns 0.1

# Total puts in [6, 15]: 2 at t=10 -> average = 2 / 10 = 0.2
kv.average_puts(10, timestamp=15)  # returns 0.2

# Window of 5 seconds at t = 16 covers range [12, 16]
# Total puts in [12, 16]: 0 -> average = 0 / 5 = 0.0
kv.average_puts(5, timestamp=16)   # returns 0.0

Constraints

  • 1 <= len(key), len(value) <= 100
  • 1 <= timestamp <= 2 * 10^9
  • 1 <= window_seconds <= 604800 (1 week)
  • At most 2 * 10^5 total calls across all methods.
  • Memory usage should scale with the number of unique active time buckets, not the raw number of operations.

Edge Cases

  • Zero operations occurring within the specified window.
  • Multiple operations occurring at the exact same timestamp.
  • Windows that extend prior to timestamp 0 or prior to the first recorded operation.
  • Keys that are overwritten or queried when non-existent (metrics must still record the operation attempt).

Hints

Hint 1

Recording individual timestamps for every single operation will consume excessive memory under high throughput; consider bucketing operations by second.

Hint 2

A deque or circular buffer of `(timestamp_bucket, count)` pairs allows O(1) amortized updates and efficient range queries.

Hint 3

To support multiple large windows (e.g., 1 hour, 1 day, 1 week), you can either maintain a single bucketed queue pruned to the maximum window size (604,800 seconds) or maintain cumulative prefix counts.

Follow-up questions

  • How would you optimize memory if queries only need stats for 1 hour, 1 day, and 1 week, and a small approximation error (e.g., 1%) is acceptable?
  • How would you ensure thread-safety for read and write operations under heavy multi-threaded contention without blocking reads?
  • If timestamps can arrive slightly out of order due to network jitter across distributed nodes, how would your data structures adapt?