Design event timeout detector
Company: Applied Intuition
Role: Software Engineer
Category: Coding & Algorithms
Difficulty: Medium
Interview Round: Technical Screen
Quick Answer: This question evaluates a candidate's competency in designing efficient event-driven algorithms and time-based state management, including handling duplicates, out-of-order events, concurrent updates, and edge-case timeout semantics.
Constraints
- 0 <= len(operations) <= 200000
- event_id and timestamps are integers in the range [-10^9, 10^9]
- type is one of 'start', 'ping', or 'end'
- All `get` operations are given in non-decreasing `now_ts` order
Examples
Input: (5, [('process', 1, 'start', 0), ('process', 2, 'start', 1), ('process', 1, 'ping', 3), ('get', 6), ('process', 2, 'end', 7), ('get', 9)])
Expected Output: [[], [1]]
Explanation: At time 6, event 1 was updated at 3 and event 2 at 1, so neither satisfies `now - last_updated > 5`. After event 2 ends, time 9 makes event 1 timed out because `9 - 3 = 6 > 5`.
Input: (4, [('process', 10, 'start', 5), ('process', 10, 'ping', 3), ('process', 10, 'start', 5), ('process', 10, 'ping', 9), ('process', 10, 'end', 8), ('get', 13), ('get', 14)])
Expected Output: [[], [10]]
Explanation: The ping at 3 and repeated start at 5 are stale/duplicate and ignored. The end at 8 is stale because the latest accepted timestamp is 9. At time 13, `13 - 9 = 4` is not greater than 4, but at time 14 it is.
Input: (3, [('process', 1, 'end', 2), ('process', 1, 'start', 4), ('process', 1, 'start', 6), ('get', 9), ('get', 10), ('process', 1, 'start', 11), ('get', 11), ('get', 15)])
Expected Output: [[], [1], [], [1]]
Explanation: The first end is ignored because event 1 was never active. The second start at 6 restarts the event. It times out at time 10, then a newer start at 11 clears the timeout, and it times out again at 15.
Input: (-1, [('process', 2, 'start', 7), ('get', 7), ('process', 2, 'ping', 8), ('get', 8), ('process', 3, 'ping', 0), ('get', 8)])
Expected Output: [[2], [2], [2]]
Explanation: With `T = -1`, an active event satisfies the timeout rule immediately on the next `get` because `now - last_updated > -1`. The ping for event 3 is ignored because it never started.
Input: (2, [('process', 5, 'start', 0), ('process', 3, 'start', 1), ('process', 4, 'start', 0), ('get', 3), ('process', 5, 'ping', 4), ('get', 4), ('get', 7)])
Expected Output: [[4, 5], [4, 3], [4, 3, 5]]
Explanation: At time 3, events 4 and 5 time out together, and smaller `event_id` comes first. Event 5 is later refreshed by a ping, so it leaves the timed-out set. Event 3 then times out at time 4, and event 5 times out again at time 7.
Input: (0, [('process', 1, 'start', 10), ('get', 10), ('get', 11), ('process', 1, 'ping', 11), ('get', 11), ('process', 1, 'end', 11), ('get', 12)])
Expected Output: [[], [1], [], []]
Explanation: With `T = 0`, an event times out only when `now_ts` is strictly greater than its last update time. So it is not timed out at 10, is timed out at 11, the ping at 11 clears it, and the end at 11 removes it.
Solution
def solution(T, operations):
import heapq
from collections import OrderedDict
# event_id -> [active, last_ts, version]
states = {}
heap = [] # (deadline, event_id, version)
timed_out = OrderedDict()
result = []
for op in operations:
if not op:
continue
if op[0] == 'process':
_, event_id, event_type, ts = op
state = states.get(event_id)
if event_type == 'start':
if state is None:
states[event_id] = [True, ts, 1]
heapq.heappush(heap, (ts + T, event_id, 1))
else:
active, last_ts, version = state
if ts <= last_ts:
continue
version += 1
state[0] = True
state[1] = ts
state[2] = version
timed_out.pop(event_id, None)
heapq.heappush(heap, (ts + T, event_id, version))
elif event_type == 'ping':
if state is None:
continue
active, last_ts, version = state
if (not active) or ts <= last_ts:
continue
version += 1
state[1] = ts
state[2] = version
timed_out.pop(event_id, None)
heapq.heappush(heap, (ts + T, event_id, version))
elif event_type == 'end':
if state is None:
continue
active, last_ts, version = state
if (not active) or ts < last_ts:
continue
state[0] = False
state[1] = ts
timed_out.pop(event_id, None)
elif op[0] == 'get':
_, now_ts = op
while heap and heap[0][0] < now_ts:
deadline, event_id, version = heapq.heappop(heap)
state = states.get(event_id)
if state is None:
continue
active, last_ts, current_version = state
if not active or current_version != version:
continue
if event_id not in timed_out:
timed_out[event_id] = None
result.append(list(timed_out.keys()))
return resultTime complexity: Overall O(m log m + total_output), where m is the number of operations. Each accepted start/ping adds one heap entry, and each heap entry is popped at most once.. Space complexity: O(m) in the worst case because the heap may contain stale entries from lazy deletion, plus O(n) event states..
Hints
- Use a hash map to store the current state of each event: whether it is active, its latest accepted timestamp, and a version number.
- Use a min-heap of expiration deadlines (`last_updated_ts + T`) plus lazy deletion so old heap entries do not need to be removed immediately.