Coding problem · Hard · Design & Data Structures
Row Lock Manager with Deadlock Detection
Hard Practical tasks
Implement a tiny row lock manager, like the one inside Postgres or InnoDB.
acquire(tx, row):- If the row is free, or already held by
tx, grant it and return"granted". - Otherwise, if waiting would create a cycle (the row's holder is — directly or through a chain of waits —
waiting for
tx), the request is the deadlock victim: aborttx(release all its locks, granting each released row to the next waiter) and return"deadlock". - Otherwise append
txto the row's FIFO wait queue and return"waiting". (A transaction waits for at most one row at a time.)
- If the row is free, or already held by
release(tx)— commit or rollback: removetxfrom any wait queue, release every row it holds, and grant each released row to the first waiter in its queue. Return the grants as"tx:row"strings, processing released rows in ascending row order.
Practice for How Deadlocks Happen.
Example 1
Input: ["LockManager","acquire","acquire","acquire","acquire","release","acquire"] [[],["T1","A"],["T2","B"],["T1","B"],["T2","A"],["T1"],["T2","A"]] Output: [null,"granted","granted","waiting","deadlock",[],"granted"] Explanation: T2 asking for A would close the cycle T2→T1→T2, so T2 is aborted; B passes to the waiting T1.
Hints
Hint 1
Track: row → holder, row → FIFO queue, tx → row it waits for.
Hint 2
Cycle check: start at the holder of the requested row; follow 'waits for row' → 'holder of that row' until you reach tx (deadlock) or a transaction that isn't waiting.
Hint 3
Aborting is the same as releasing, except acquire returns "deadlock".
Write and run your solution
Loading the editor… (JavaScript is required to run code in your browser.)
Solution and explanation
Show the solution
Time acquire O(chain length), release O(rows held · log rows held) · Space O(rows + transactions)
Keep three maps: the holder of each row, a FIFO queue per row, and which row each transaction waits for. Granting is
O(1). Before enqueueing, walk the wait-for chain from the row's holder: holder → the row it waits for → that row's
holder … If the walk reaches the requester, enqueueing would close a cycle, so the requester becomes the victim and
is aborted like a release. Real databases detect cycles the same way (Postgres after deadlock_timeout, InnoDB
immediately) and pick a victim, which surfaces in your app as SQLSTATE 40P01 / MySQL error 1213.
Reference solution
class LockManager {
constructor() {
this.holder = new Map(); // row -> tx
this.queue = new Map(); // row -> [tx]
this.waitingFor = new Map(); // tx -> row
}
acquire(tx, row) {
const owner = this.holder.get(row);
if (owner === undefined || owner === tx) {
this.holder.set(row, tx);
return "granted";
}
// Would waiting close a cycle? Follow holder -> row it waits for -> that row's holder ...
const seen = new Set();
let cur = owner;
while (cur !== undefined && !seen.has(cur)) {
if (cur === tx) {
this.release(tx);
return "deadlock";
}
seen.add(cur);
const row2 = this.waitingFor.get(cur);
cur = row2 === undefined ? undefined : this.holder.get(row2);
}
if (!this.queue.has(row)) this.queue.set(row, []);
this.queue.get(row).push(tx);
this.waitingFor.set(tx, row);
return "waiting";
}
release(tx) {
const waitRow = this.waitingFor.get(tx);
if (waitRow !== undefined) {
this.queue.set(waitRow, this.queue.get(waitRow).filter((t) => t !== tx));
this.waitingFor.delete(tx);
}
const rows = [...this.holder].filter(([, t]) => t === tx).map(([r]) => r).sort();
const grants = [];
for (const row of rows) {
this.holder.delete(row);
const q = this.queue.get(row) ?? [];
if (q.length) {
const next = q.shift();
this.holder.set(row, next);
this.waitingFor.delete(next);
grants.push(`${next}:${row}`);
}
}
return grants;
}
}
class LockManager {
private holder = new Map<string, string>();
private queue = new Map<string, string[]>();
private waitingFor = new Map<string, string>();
acquire(tx: string, row: string): string {
const owner = this.holder.get(row);
if (owner === undefined || owner === tx) {
this.holder.set(row, tx);
return "granted";
}
const seen = new Set<string>();
let cur: string | undefined = owner;
while (cur !== undefined && !seen.has(cur)) {
if (cur === tx) {
this.release(tx);
return "deadlock";
}
seen.add(cur);
const waitRow = this.waitingFor.get(cur);
cur = waitRow === undefined ? undefined : this.holder.get(waitRow);
}
if (!this.queue.has(row)) this.queue.set(row, []);
this.queue.get(row)!.push(tx);
this.waitingFor.set(tx, row);
return "waiting";
}
release(tx: string): string[] {
const waitRow = this.waitingFor.get(tx);
if (waitRow !== undefined) {
this.queue.set(waitRow, (this.queue.get(waitRow) ?? []).filter((t) => t !== tx));
this.waitingFor.delete(tx);
}
const rows = [...this.holder].filter(([, t]) => t === tx).map(([r]) => r).sort();
const grants: string[] = [];
for (const row of rows) {
this.holder.delete(row);
const q = this.queue.get(row) ?? [];
const next = q.shift();
if (next !== undefined) {
this.holder.set(row, next);
this.waitingFor.delete(next);
grants.push(`${next}:${row}`);
}
}
return grants;
}
}
class LockManager:
def __init__(self):
self.holder: dict[str, str] = {}
self.queue: dict[str, list[str]] = {}
self.waiting_for: dict[str, str] = {}
def acquire(self, tx: str, row: str) -> str:
owner = self.holder.get(row)
if owner is None or owner == tx:
self.holder[row] = tx
return "granted"
seen: set[str] = set()
cur = owner
while cur is not None and cur not in seen:
if cur == tx:
self.release(tx)
return "deadlock"
seen.add(cur)
wait_row = self.waiting_for.get(cur)
cur = self.holder.get(wait_row) if wait_row is not None else None
self.queue.setdefault(row, []).append(tx)
self.waiting_for[tx] = row
return "waiting"
def release(self, tx: str) -> list[str]:
wait_row = self.waiting_for.pop(tx, None)
if wait_row is not None:
self.queue[wait_row].remove(tx)
grants = []
for row in sorted(r for r, t in self.holder.items() if t == tx):
del self.holder[row]
q = self.queue.get(row, [])
if q:
nxt = q.pop(0)
self.holder[row] = nxt
self.waiting_for.pop(nxt, None)
grants.append(f"{nxt}:{row}")
return grants
<?php
class LockManager
{
private array $holder = []; // row => tx
private array $queue = []; // row => [tx, ...]
private array $waitingFor = []; // tx => row
public function acquire(string $tx, string $row): string
{
$owner = $this->holder[$row] ?? null;
if ($owner === null || $owner === $tx) {
$this->holder[$row] = $tx;
return 'granted';
}
$seen = [];
$cur = $owner;
while ($cur !== null && !isset($seen[$cur])) {
if ($cur === $tx) {
$this->release($tx);
return 'deadlock';
}
$seen[$cur] = true;
$waitRow = $this->waitingFor[$cur] ?? null;
$cur = $waitRow === null ? null : ($this->holder[$waitRow] ?? null);
}
$this->queue[$row][] = $tx;
$this->waitingFor[$tx] = $row;
return 'waiting';
}
public function release(string $tx): array
{
if (isset($this->waitingFor[$tx])) {
$waitRow = $this->waitingFor[$tx];
$this->queue[$waitRow] = array_values(array_filter($this->queue[$waitRow], fn ($t) => $t !== $tx));
unset($this->waitingFor[$tx]);
}
$rows = [];
foreach ($this->holder as $row => $t) {
if ($t === $tx) {
$rows[] = (string) $row;
}
}
sort($rows);
$grants = [];
foreach ($rows as $row) {
unset($this->holder[$row]);
if (!empty($this->queue[$row])) {
$next = array_shift($this->queue[$row]);
$this->holder[$row] = $next;
unset($this->waitingFor[$next]);
$grants[] = "{$next}:{$row}";
}
}
return $grants;
}
}
class LockManager {
private final Map<String, String> holder = new HashMap<>();
private final Map<String, Deque<String>> queue = new HashMap<>();
private final Map<String, String> waitingFor = new HashMap<>();
public LockManager() {}
public String acquire(String tx, String row) {
String owner = holder.get(row);
if (owner == null || owner.equals(tx)) {
holder.put(row, tx);
return "granted";
}
Set<String> seen = new HashSet<>();
String cur = owner;
while (cur != null && seen.add(cur)) {
if (cur.equals(tx)) {
release(tx);
return "deadlock";
}
String waitRow = waitingFor.get(cur);
cur = waitRow == null ? null : holder.get(waitRow);
}
queue.computeIfAbsent(row, k -> new ArrayDeque<>()).addLast(tx);
waitingFor.put(tx, row);
return "waiting";
}
public List<String> release(String tx) {
String waitRow = waitingFor.remove(tx);
if (waitRow != null) queue.get(waitRow).remove(tx);
List<String> rows = new ArrayList<>();
for (Map.Entry<String, String> e : holder.entrySet()) if (e.getValue().equals(tx)) rows.add(e.getKey());
Collections.sort(rows);
List<String> grants = new ArrayList<>();
for (String row : rows) {
holder.remove(row);
Deque<String> q = queue.get(row);
if (q != null && !q.isEmpty()) {
String next = q.pollFirst();
holder.put(row, next);
waitingFor.remove(next);
grants.add(next + ":" + row);
}
}
return grants;
}
}
Common mistakes
- Forgetting to remove an aborted or released transaction from wait queues.
- Granting a released row to a waiter without clearing its 'waiting for' entry.
- Treating a re-request of a row you already hold as a wait.
Follow-up questions
- Shared (read) and exclusive (write) lock modes.
- Lock timeouts: give up after N ms instead of waiting forever.
- Choose the youngest transaction as the victim instead of the requester.
More Design & Data Structures problems
- LRU Cache · Medium
- Token Bucket Rate Limiter · Medium
- Key-Value Store with TTL · Medium
- URL Shortener Codes (Base62) · Easy
- Build an EventEmitter · Medium
- Versioned Store with Optimistic Locking · Easy