Implement account scheduler with locking and LRU
Company: Stripe
Role: Software Engineer
Category: Coding & Algorithms
Difficulty: medium
Interview Round: Technical Screen
Quick Answer: This question evaluates understanding of time-based resource scheduling, locking semantics, LRU eviction policies, and stateful data-structure management in the coding & algorithms domain.
Constraints
- All account IDs are integers; accounts is a fixed set provided at construction.
- locked_until maps account_id -> integer timestamp; accounts absent from it default to 0.
- All timestamps t are non-negative integers and are supplied in non-decreasing order across calls.
- duration is a positive integer.
- is_available is true exactly when t >= locked_until[account_id].
- acquire fails (returns False) when the account is not available at time t.
- auto_acquire returns None when no account is available at time t.
- Calls are sequential; no concurrency handling is required.
Examples
Input: ([1, 2, 3, 4], {1: 10, 2: 5, 3: 0, 4: 20}, [["is_available", 1, 8], ["is_available", 2, 8], ["is_available", 3, 1], ["is_available", 4, 21]])
Expected Output: [False, True, True, True]
Explanation: Part 1 basic queries: account 1 is locked until 10 so unavailable at 8; account 2 unlocks at 5 (available at 8); account 3 default-available; account 4 unlocks at 20 (available at 21).
Input: ([1, 2], {}, [["is_available", 1, 0], ["acquire", 1, 0, 5], ["is_available", 1, 3], ["is_available", 1, 5], ["acquire", 2, 0, 3]])
Expected Output: [True, True, False, True, True]
Explanation: Part 2: account 1 starts available; acquiring it at t=0 for 5 locks it until 5, so it is unavailable at 3 but available again at 5 (boundary is inclusive). Account 2 is independently available and acquired.
Input: ([1, 2, 3], {}, [["acquire", 1, 0, 100], ["auto_acquire", 5, 10], ["auto_acquire", 5, 10], ["auto_acquire", 5, 10]])
Expected Output: [True, 2, 3, None]
Explanation: Part 3: account 1 is locked until 100. auto_acquire at t=5 picks among {2,3} (both never used) the smaller id 2, then 3; the third call finds nothing available and returns None.
Input: ([1, 2, 3], {}, [["acquire", 1, 0, 2], ["acquire", 2, 1, 2], ["auto_acquire", 10, 1], ["auto_acquire", 10, 1], ["auto_acquire", 10, 1]])
Expected Output: [True, True, 3, 1, 2]
Explanation: LRU ordering: by t=10 all are available. last_used is {1:0, 2:1}, 3 never used. auto_acquire picks 3 (never used) first, then 1 (oldest use at 0), then 2 (used at 1).
Input: ([7], {7: 50}, [["is_available", 7, 49], ["acquire", 7, 49, 5], ["auto_acquire", 49, 1]])
Expected Output: [False, False, None]
Explanation: Account 7 is locked until 50, so it is unavailable at 49; the acquire at t=49 therefore fails (returns False) and leaves state unchanged, so auto_acquire at 49 finds nothing and returns None.
Input: ([], {}, [["auto_acquire", 0, 5]])
Expected Output: [None]
Explanation: Edge case: with no accounts in the system, auto_acquire has nothing to select and returns None.
Input: ([5], {}, [["acquire", 5, 0, 3], ["acquire", 5, 1, 10], ["is_available", 5, 2], ["is_available", 5, 3], ["auto_acquire", 3, 1]])
Expected Output: [True, False, False, True, 5]
Explanation: First acquire locks account 5 until 3. The second acquire at t=1 fails because 5 is not available then, so it does NOT extend the lock. Account 5 becomes available again at t=3 (boundary inclusive) and auto_acquire at t=3 selects it.
Hints
- Keep a single dict locked_until[account_id]; availability at time t is simply t >= locked_until.get(account_id, 0).
- For acquire, the new lock end is max(locked_until[account_id], t) + duration so an already-future lock extends correctly rather than being shortened.
- Track a separate last_used map updated only on a *successful* acquire (explicit or auto). Never-acquired accounts should compare as older than any used account.
- For auto_acquire, scan available accounts and pick the LRU using the tuple key (was_used?, last_used_time, account_id) so never-used wins first, then oldest, then smallest id breaks ties deterministically.