Implement an expiring GPU credits ledger
Company: OpenAI
Role: Software Engineer
Category: Coding & Algorithms
Difficulty: medium
Interview Round: Technical Screen
Implement an expiring GPU credits ledger for multiple users with three operations:
1) add_credit(user_id, amount, expiry_time): add a lot of credits that expires at expiry_time.
2) subtract(user_id, amount, timestamp): atomically deduct amount at timestamp by consuming unexpired lots in order of earliest expiry first; lots with expiry_time <= timestamp are expired and cannot be used; if total unexpired balance < amount, the operation must fail and make no changes.
3) get_balance(user_id, timestamp): return the total unexpired balance at timestamp and a breakdown by remaining lots (amount, expiry_time). Assumptions: time is integer seconds; amounts are non-negative integers; partial consumption of a lot is allowed; exact expiry boundary uses the rule "valid iff timestamp < expiry_time". Tasks: implement the three functions; include tests for
(a) single lot add and spend,
(b) spending across multiple lots with different expiries,
(c) attempts to spend after a lot expires,
(d) insufficient-balance failure leaving state unchanged,
(e) edge case at expiry boundary. Follow up: analyze time/space complexity; discuss how to optimize for many operations (e.g., data structures to achieve O(log n) per update) and how to persist/restore state.
Quick Answer: This question evaluates understanding of time-based resource accounting, design of data structures for expiring balances, atomic update semantics for concurrent operations, and algorithmic time/space complexity within the Coding & Algorithms domain.
Implement an **expiring GPU credits ledger** that processes a stream of operations for multiple users and reports the results of every query.
## What to implement
Implement:
```python
def solution(operations):
...
```
`operations` is a list of operation tuples, processed **in the given order**. Each credit a user adds creates a **separate lot** with its own expiry time. A lot is **valid (unexpired) at a timestamp `t` if and only if `t < expiry_time`** (so a lot with `expiry_time <= t` is expired at `t`).
Return a list containing the result of every **non-`add`** operation, in the order the operations were processed. `add` operations produce no output.
## Operations
Each tuple's first element is its type:
- **`('add', user_id, amount, expiry_time)`** — Add a new lot of `amount` credits to `user_id` that expires at `expiry_time`. Produces no result. (An `add` with `amount <= 0` adds nothing.)
- **`('subtract', user_id, amount, timestamp)`** — Atomically deduct `amount` credits from `user_id` using only lots that are unexpired at `timestamp`, spending from the lot with the **earliest `expiry_time` first**. **Partial consumption of a lot is allowed** (a lot may be left with a reduced amount). Returns **`True`** on success or **`False`** on failure:
- If the total of `user_id`'s unexpired credits is less than `amount`, the operation **fails**: return `False` and leave the ledger **unchanged** (no lots are modified).
- Otherwise it succeeds: deduct exactly `amount` earliest-expiry-first and return `True`.
- A subtract of `amount == 0` always **succeeds** (returns `True`) and changes nothing, even for a user with no credits.
- **`('balance', user_id, timestamp)`** — Report `user_id`'s credits that are unexpired at `timestamp`. Returns a tuple `(total, breakdown)` where:
- `total` is the sum of all unexpired lot amounts.
- `breakdown` is the list of remaining unexpired lots as `(amount, expiry_time)` pairs, **sorted by `expiry_time`** (ascending).
A user with no unexpired credits has balance `(0, [])`.
## Notes
- Expired lots never count toward a balance, can never be spent, and should be treated as gone once their `expiry_time` has been reached at the timestamp of a query.
- Users are independent; an operation on one `user_id` never affects another.
## Constraints
- `1 <= len(operations) <= 20000`
- `0 <= amount <= 10^9`
- `0 <= expiry_time, timestamp <= 10^9`
- Operations are processed in the given order.
- A lot is usable exactly when `timestamp < expiry_time`.
- The `timestamp` values of the **non-`add` operations** (`subtract` and `balance`) are **non-decreasing** in processing order (time only moves forward across queries). `add` operations carry no timestamp and may specify any `expiry_time`.
## Follow-up
Analyze the time and space complexity of your approach. Discuss how to optimize toward **O(log n)** updates and queries for heavy workloads, and describe how you would persist and restore the ledger state.
Constraints
- 1 <= len(operations) <= 20000
- 0 <= amount <= 10^9
- 0 <= expiry_time, timestamp <= 10^9
- Operations are processed in the given order
- A lot is usable exactly when timestamp < expiry_time
Examples
Input: ([('add', 'u1', 50, 10), ('balance', 'u1', 5), ('subtract', 'u1', 20, 6), ('balance', 'u1', 6)],)
Expected Output: [(50, [(50, 10)]), True, (30, [(30, 10)])]
Explanation: At time 5, the lot is still valid, so the balance is 50. Subtracting 20 at time 6 succeeds, leaving 30 in the same lot.
Input: ([('add', 'alice', 10, 5), ('add', 'alice', 30, 20), ('subtract', 'alice', 10, 5), ('balance', 'alice', 5)],)
Expected Output: [True, (20, [(20, 20)])]
Explanation: A lot is valid only when `timestamp < expiry_time`, so the lot expiring at 5 is already invalid at time 5. The subtraction uses the 30-credit lot expiring at 20, leaving 20.
Hints
- Store each user's credits as separate lots instead of merging everything into one number, because expiry times matter during subtraction.
- For atomic subtraction, first compute whether enough unexpired balance exists before changing any lot.