Skip to main content

mongreldb_core/
locks.rs

1//! Key and predicate lock manager with deadlock detection (spec section 10.2,
2//! S1B-003). Implemented in the Stage 1 wave.
3//!
4//! This wave builds the manager and its tests as a self-contained unit.
5//! Wiring lock acquisition into the transaction, constraint, sequence, and DDL
6//! paths is a documented follow-up owned by the next Stage 1B wave; nothing
7//! outside this module calls into it yet.
8//!
9//! ## Modes
10//!
11//! Two modes, [`LockMode::Shared`] and [`LockMode::Exclusive`], cover every
12//! use case spec section 10.2 lists: `SELECT ... FOR UPDATE` takes row locks
13//! in Exclusive mode, unique-constraint probes and global sequence allocation
14//! take Exclusive key/barrier locks, foreign-key parent protection takes a
15//! Shared lock on the parent row (blocking a concurrent Exclusive delete or
16//! key update of the parent), and DDL takes the schema barrier in Exclusive
17//! mode while DML transactions hold it in Shared mode. A third Update mode is
18//! deliberately omitted: with only Shared/Exclusive the classic read-then-write
19//! conversion is a Shared → Exclusive upgrade, and the conversion deadlocks an
20//! Update mode exists to prevent are instead resolved deterministically by the
21//! deadlock detector below.
22//!
23//! ## Wait queues
24//!
25//! Per spec section 10.2, each queued [`Waiter`] carries exactly four things:
26//! the transaction ID, the deadline, the cancellation control, and the lock
27//! mode. Victim-selection priority and wait outcomes are bookkeeping and live
28//! in side tables ([`Inner::priorities`], [`Inner::fates`]) so the queue shape
29//! matches the spec verbatim.
30//!
31//! Transaction IDs are `u64` today: core allocates them monotonically
32//! (`txn::allocate_txn_id`), so the numerically largest ID is the youngest
33//! transaction. The later migration to the 128-bit
34//! [`mongreldb_types::ids::TransactionId`] (spec section 7) swaps the ID type;
35//! because random 128-bit IDs do not encode age, that migration must also give
36//! victim selection a separate age source.
37//!
38//! ## Fairness
39//!
40//! Grants are strict FIFO per key where modes allow: a waiter is granted only
41//! when every waiter ahead of it has been granted and its mode is compatible
42//! with every current holder. A reader arriving behind a queued writer never
43//! barges ahead of it, so a writer waits for at most one bounded window: the
44//! holder generation present when it enqueued plus the readers already queued
45//! ahead of it. Readers arriving later queue behind the writer and cannot
46//! extend its wait.
47//!
48//! ## Deadlock detection and victim selection
49//!
50//! The wait-for graph is rebuilt from queue state on every enqueue and on
51//! every grant (spec section 10.2). A waiter has an edge to every incompatible
52//! holder of its key and to every incompatible waiter ahead of it; a
53//! transaction never has an edge to itself. Cycle search iterates nodes and
54//! edges in ascending ID order, so the cycle found for a given state is
55//! deterministic. The victim is chosen deterministically: lowest priority
56//! first (priority is optional; all transactions default to
57//! [`DEFAULT_PRIORITY`], in which case the rule collapses to the spec's
58//! default), then the youngest transaction (largest `u64` ID). The victim's
59//! blocked `acquire` returns [`LockError::Deadlock`]; everyone else in the
60//! cycle keeps waiting and proceeds once the victim aborts and releases.
61//!
62//! ## Error mapping
63//!
64//! [`From<LockError> for MongrelError`] maps a deadlock onto the dedicated
65//! `MongrelError::Deadlock` variant (victim and wait-for cycle preserved), so
66//! the precise taxonomy category of spec section 9.7 —
67//! [`ErrorCategory::Deadlock`] — survives the bridge, alongside the same
68//! retry-the-whole-transaction discipline as `MongrelError::Conflict`
69//! ([`ErrorCategory::retry_class`]). Deadline and cancellation map onto the
70//! matching `MongrelError` variants; [`LockError::category`] exposes the
71//! taxonomy category of every failure directly.
72//!
73//! ## Blocking waits
74//!
75//! Thread-safe and synchronous only: one `parking_lot` mutex plus one
76//! condition variable. Waiters sleep in `wait_timeout` for the smaller of
77//! their remaining deadline and [`CANCELLATION_POLL_INTERVAL`], so
78//! cancellation through [`ExecutionControl`] (which offers no synchronous
79//! wait handle) is observed within that interval and deadlines within timer
80//! resolution.
81//!
82//! ## Usage contract
83//!
84//! A transaction has at most one `acquire` in flight at a time (its statements
85//! execute sequentially), and calls [`LockManager::release_all`] exactly once
86//! when the transaction ends — including when it is a deadlock victim, whose
87//! held locks are *not* released implicitly. Re-acquisition is re-entrant:
88//! requesting a mode already covered by the transaction's hold is a no-op, and
89//! a sole holder's Shared → Exclusive upgrade succeeds immediately. Re-issued
90//! acquisitions therefore can never deadlock a transaction against itself.
91
92use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
93use std::ops::Bound;
94use std::time::{Duration, Instant};
95
96use mongreldb_types::errors::ErrorCategory;
97use parking_lot::{Condvar, Mutex, MutexGuard};
98
99use crate::{ExecutionControl, MongrelError, RowId};
100
101/// How often a blocked waiter wakes to re-check cancellation when neither a
102/// notify nor its deadline comes first. This bounds the latency between an
103/// [`ExecutionControl::cancel`] and the waiter's [`LockError::Cancelled`].
104const CANCELLATION_POLL_INTERVAL: Duration = Duration::from_millis(10);
105
106/// Priority of a transaction that never stated one. Choosing zero as the
107/// default makes "lowest priority, then youngest" collapse to plain
108/// "youngest" — the spec's default rule — whenever no transaction in a cycle
109/// carries an explicit priority.
110const DEFAULT_PRIORITY: u64 = 0;
111
112/// The mode of a lock acquisition or hold.
113///
114/// Shared holds coexist with other Shared holds; Exclusive excludes
115/// everything. See the module docs for why there is deliberately no Update
116/// mode.
117#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
118pub enum LockMode {
119    /// Coexists with other Shared holds (foreign-key parent protection, DML
120    /// transactions under the schema barrier).
121    Shared,
122    /// Excludes every other mode (`SELECT ... FOR UPDATE` row locks,
123    /// unique-constraint probes, sequence allocation, DDL barriers).
124    Exclusive,
125}
126
127impl LockMode {
128    /// Whether two modes can be held on the same key at the same time.
129    pub const fn compatible(self, other: Self) -> bool {
130        matches!((self, other), (Self::Shared, Self::Shared))
131    }
132}
133
134/// A typed, lockable key.
135///
136/// The families cover the three key spaces of spec section 10.2: row keys
137/// (physical `RowId` or primary-key bytes), predicate/range keys, and named
138/// barriers for schema/DDL and sequence allocation.
139#[derive(Debug, Clone, PartialEq, Eq, Hash)]
140pub enum LockKey {
141    /// One physical row of a table (`SELECT ... FOR UPDATE`).
142    Row { table_id: u64, row_id: RowId },
143    /// One primary-key or unique-constraint key of a table. Callers with
144    /// several key spaces per table (multiple unique indexes) MUST fold the
145    /// index identity into `key`, since the manager compares bytes only.
146    Key { table_id: u64, key: Vec<u8> },
147    /// One predicate/range over a table's key space (serializable predicate
148    /// protection).
149    Range {
150        table_id: u64,
151        low: Bound<Vec<u8>>,
152        high: Bound<Vec<u8>>,
153    },
154    /// A named barrier (schema/DDL, sequence allocation). Names are compared
155    /// as strings; constructors below define the engine's namespaces.
156    Barrier { name: String },
157}
158
159impl LockKey {
160    /// A physical-row key (spec section 10.2 `SELECT ... FOR UPDATE`).
161    pub fn row(table_id: u64, row_id: RowId) -> Self {
162        Self::Row { table_id, row_id }
163    }
164
165    /// A primary-key or unique-constraint key from encoded key bytes.
166    pub fn key(table_id: u64, key: impl Into<Vec<u8>>) -> Self {
167        Self::Key {
168            table_id,
169            key: key.into(),
170        }
171    }
172
173    /// A predicate/range key over a table's key space.
174    pub fn range(table_id: u64, low: Bound<Vec<u8>>, high: Bound<Vec<u8>>) -> Self {
175        Self::Range {
176            table_id,
177            low,
178            high,
179        }
180    }
181
182    /// A named barrier.
183    pub fn barrier(name: impl Into<String>) -> Self {
184        Self::Barrier { name: name.into() }
185    }
186
187    /// The schema/DDL barrier: DDL takes it in Exclusive mode, DML
188    /// transactions in Shared mode, so schema changes exclude concurrent
189    /// DML and one another.
190    pub fn schema_barrier() -> Self {
191        Self::barrier("schema")
192    }
193
194    /// The barrier guarding allocation of one named sequence; always taken
195    /// in Exclusive mode so concurrent allocations serialize.
196    pub fn sequence_barrier(sequence: &str) -> Self {
197        Self::barrier(format!("sequence:{sequence}"))
198    }
199}
200
201/// One lock acquisition request.
202///
203/// The four fields the spec's wait queue carries — transaction ID, deadline,
204/// cancellation control, lock mode — are all here; `priority` is
205/// victim-selection input and never enters the queue itself.
206#[derive(Debug, Clone)]
207pub struct LockRequest {
208    /// The requesting transaction. `u64` today; see the module docs for the
209    /// 128-bit [`mongreldb_types::ids::TransactionId`] migration note.
210    pub txn_id: u64,
211    /// The requested mode.
212    pub mode: LockMode,
213    /// Optional lock-wait deadline, independent of any deadline inside
214    /// `control`. Expiry fails the wait with [`LockError::DeadlineExceeded`].
215    pub deadline: Option<Instant>,
216    /// Cooperative cancellation control; cancelling it fails the wait with
217    /// [`LockError::Cancelled`] (or [`LockError::DeadlineExceeded`] when the
218    /// control's own deadline fired first), observed within
219    /// [`CANCELLATION_POLL_INTERVAL`].
220    pub control: ExecutionControl,
221    /// Optional deadlock-victim priority: within one wait-for cycle the
222    /// lowest priority value dies first, ties break by youngest transaction.
223    /// `None` is [`DEFAULT_PRIORITY`].
224    pub priority: Option<u64>,
225}
226
227impl LockRequest {
228    /// A request with no lock-wait deadline and no explicit priority.
229    pub fn new(txn_id: u64, mode: LockMode, control: ExecutionControl) -> Self {
230        Self {
231            txn_id,
232            mode,
233            deadline: None,
234            control,
235            priority: None,
236        }
237    }
238
239    /// Sets the lock-wait deadline.
240    pub fn with_deadline(mut self, deadline: Instant) -> Self {
241        self.deadline = Some(deadline);
242        self
243    }
244
245    /// Sets the lock-wait deadline relative to now. Overflowing timeouts
246    /// collapse to an already-expired deadline (fail closed), matching
247    /// [`ExecutionControl::with_timeout`].
248    pub fn with_timeout(self, timeout: Duration) -> Self {
249        let now = Instant::now();
250        self.with_deadline(now.checked_add(timeout).unwrap_or(now))
251    }
252
253    /// Sets the deadlock-victim priority (lower values die first).
254    pub fn with_priority(mut self, priority: u64) -> Self {
255        self.priority = Some(priority);
256        self
257    }
258}
259
260/// A failed lock acquisition.
261///
262/// The taxonomy category of each variant is available through
263/// [`Self::category`]; conversion to [`MongrelError`] maps onto the closest
264/// existing variant (see the module docs for the deadlock mapping).
265#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
266pub enum LockError {
267    /// The transaction was chosen as the deterministic deadlock victim; retry
268    /// the whole transaction.
269    #[error(
270        "deadlock: transaction {victim} was chosen as the deadlock victim (wait-for cycle {cycle}); retry the whole transaction"
271    )]
272    Deadlock {
273        /// The doomed transaction (always the receiver of this error).
274        victim: u64,
275        /// The wait-for cycle, `a → b → …`, each waiting on the next.
276        cycle: String,
277    },
278    /// The lock-wait deadline (or the control's own deadline) expired.
279    #[error("lock wait deadline exceeded")]
280    DeadlineExceeded,
281    /// The wait was cancelled through its [`ExecutionControl`].
282    #[error("lock wait cancelled")]
283    Cancelled,
284    /// The request violates the usage contract (e.g. a second in-flight wait
285    /// for the same transaction on the same key).
286    #[error("invalid lock request: {0}")]
287    InvalidRequest(String),
288}
289
290impl LockError {
291    /// The stable cross-language taxonomy category of this failure (spec
292    /// section 9.7); identical to the category of the [`MongrelError`]
293    /// produced by the bridge below.
294    pub fn category(&self) -> ErrorCategory {
295        match self {
296            Self::Deadlock { .. } => ErrorCategory::Deadlock,
297            Self::DeadlineExceeded => ErrorCategory::DeadlineExceeded,
298            Self::Cancelled => ErrorCategory::Cancelled,
299            // Mirrors the MongrelError::InvalidArgument bridge.
300            Self::InvalidRequest(_) => ErrorCategory::ClusterVersionMismatch,
301        }
302    }
303}
304
305impl From<LockError> for MongrelError {
306    /// Maps onto the matching [`MongrelError`] variant. A deadlock becomes
307    /// `MongrelError::Deadlock` — victim and cycle preserved — so callers see
308    /// the precise [`ErrorCategory::Deadlock`] with the same
309    /// retry-the-whole-transaction discipline as `Conflict`.
310    fn from(error: LockError) -> Self {
311        match error {
312            LockError::Deadlock { victim, cycle } => MongrelError::Deadlock { victim, cycle },
313            LockError::DeadlineExceeded => MongrelError::DeadlineExceeded,
314            LockError::Cancelled => MongrelError::Cancelled,
315            LockError::InvalidRequest(message) => MongrelError::InvalidArgument(message),
316        }
317    }
318}
319
320/// One granted hold.
321#[derive(Debug, Clone, Copy)]
322struct Holder {
323    txn_id: u64,
324    mode: LockMode,
325}
326
327/// One queued acquisition. Carries exactly the four fields spec section 10.2
328/// lists: transaction ID, deadline, cancellation control, and lock mode.
329#[derive(Debug)]
330struct Waiter {
331    txn_id: u64,
332    deadline: Option<Instant>,
333    control: ExecutionControl,
334    mode: LockMode,
335}
336
337/// Per-key state: granted holds plus the strict-FIFO wait queue.
338#[derive(Debug, Default)]
339struct LockState {
340    holders: Vec<Holder>,
341    queue: VecDeque<Waiter>,
342}
343
344/// Why a waiter's entry left its queue without a grant; consumed by the
345/// blocked thread, which returns the matching [`LockError`].
346#[derive(Debug)]
347enum Fate {
348    Deadlock { victim: u64, cycle: String },
349    DeadlineExceeded,
350    Cancelled,
351}
352
353impl Fate {
354    fn into_error(self) -> LockError {
355        match self {
356            Self::Deadlock { victim, cycle } => LockError::Deadlock { victim, cycle },
357            Self::DeadlineExceeded => LockError::DeadlineExceeded,
358            Self::Cancelled => LockError::Cancelled,
359        }
360    }
361}
362
363#[derive(Debug, Default)]
364struct Inner {
365    locks: HashMap<LockKey, LockState>,
366    /// Keys currently held or awaited by each transaction. This makes
367    /// `release_all` proportional to one transaction's footprint rather than
368    /// the global lock-table size.
369    txn_keys: HashMap<u64, HashSet<LockKey>>,
370    /// Exact number of entries across every per-key wait queue. Deadlock
371    /// detection can skip graph construction entirely when this is zero.
372    queued_waiters: usize,
373    /// Wait outcomes for entries already removed from a queue (deadlock
374    /// victims, waiters purged by a grant pass). Keyed by transaction: a
375    /// transaction waits on at most one key at a time.
376    fates: HashMap<u64, Fate>,
377    /// Latest stated deadlock-victim priority per transaction; registered on
378    /// every acquire so holders that later join a cycle are covered too.
379    priorities: HashMap<u64, u64>,
380    #[cfg(test)]
381    deadlock_graph_builds: usize,
382    #[cfg(test)]
383    release_all_key_visits: usize,
384}
385
386/// The key and predicate lock manager. Cheap to share: all state lives
387/// behind one mutex and one condition variable.
388#[derive(Debug)]
389pub struct LockManager {
390    inner: Mutex<Inner>,
391    wake: Condvar,
392}
393
394impl Default for LockManager {
395    fn default() -> Self {
396        Self::new()
397    }
398}
399
400impl LockManager {
401    pub fn new() -> Self {
402        Self {
403            inner: Mutex::new(Inner::default()),
404            wake: Condvar::new(),
405        }
406    }
407
408    /// Acquires `key` for `request.txn_id`, blocking until granted, doomed,
409    /// cancelled, or past the deadline. See the module docs for the
410    /// re-entrancy, upgrade, fairness, and deadlock rules.
411    pub fn acquire(&self, key: LockKey, request: LockRequest) -> Result<(), LockError> {
412        let LockRequest {
413            txn_id,
414            mode,
415            deadline,
416            control,
417            priority,
418        } = request;
419
420        let mut inner = self.inner.lock();
421        check_request_live(&control, deadline)?;
422        if let Some(priority) = priority {
423            inner.priorities.insert(txn_id, priority);
424        }
425        inner
426            .txn_keys
427            .entry(txn_id)
428            .or_default()
429            .insert(key.clone());
430
431        {
432            let state = inner.locks.entry(key.clone()).or_default();
433            match state.holders.iter().position(|h| h.txn_id == txn_id) {
434                Some(position) => {
435                    let held = state.holders[position].mode;
436                    if held == LockMode::Exclusive || mode == LockMode::Shared {
437                        // Re-entrant no-op: an Exclusive hold covers any
438                        // re-request, a Shared hold covers a Shared one. A
439                        // transaction never deadlocks against itself here.
440                        return Ok(());
441                    }
442                    if state.holders.len() == 1 && state.queue.is_empty() {
443                        // Sole-holder Shared → Exclusive upgrade is immediate.
444                        state.holders[position].mode = LockMode::Exclusive;
445                        return Ok(());
446                    }
447                    // Otherwise the upgrade queues like any other request;
448                    // the grant pass completes it once only its own Shared
449                    // hold remains.
450                }
451                None => {
452                    let blocked = !state.queue.is_empty()
453                        || state.holders.iter().any(|h| !mode.compatible(h.mode));
454                    if !blocked {
455                        state.holders.push(Holder { txn_id, mode });
456                        return Ok(());
457                    }
458                }
459            }
460            if state.queue.iter().any(|w| w.txn_id == txn_id) {
461                return Err(LockError::InvalidRequest(format!(
462                    "transaction {txn_id} already has a pending wait on {key:?}"
463                )));
464            }
465            state.queue.push_back(Waiter {
466                txn_id,
467                deadline,
468                control: control.clone(),
469                mode,
470            });
471        }
472        inner.queued_waiters = inner.queued_waiters.saturating_add(1);
473
474        // Spec section 10.2: check the wait-for graph on enqueue. If this
475        // requester is the victim its fate is already recorded and the first
476        // loop iteration below returns it synchronously.
477        detect_deadlocks(&mut inner);
478        self.wake.notify_all();
479
480        let result = self.wait_for_grant(&mut inner, &key, txn_id, &control, deadline);
481        if result.is_err() {
482            // A departed waiter may unblock the queue behind it.
483            process_key(&mut inner, &key);
484            detect_deadlocks(&mut inner);
485            untrack_transaction_key_if_unused(&mut inner, &key, txn_id);
486            self.wake.notify_all();
487        }
488        prune_if_idle(&mut inner, &key);
489        result
490    }
491
492    /// Releases every hold `txn_id` has on `key`, granting queued waiters.
493    /// Releasing a key the transaction does not hold is a no-op.
494    pub fn release(&self, txn_id: u64, key: &LockKey) {
495        let mut inner = self.inner.lock();
496        if inner
497            .locks
498            .get(key)
499            .is_some_and(|s| s.holders.iter().any(|h| h.txn_id == txn_id))
500        {
501            if let Some(state) = inner.locks.get_mut(key) {
502                state.holders.retain(|h| h.txn_id != txn_id);
503            }
504            process_key(&mut inner, key);
505            // Spec section 10.2: check the wait-for graph on grant.
506            detect_deadlocks(&mut inner);
507            untrack_transaction_key_if_unused(&mut inner, key, txn_id);
508            prune_if_idle(&mut inner, key);
509        }
510        drop(inner);
511        self.wake.notify_all();
512    }
513
514    /// Releases every hold and pending wait of `txn_id`; called exactly once
515    /// when the transaction ends, including on abort as a deadlock victim.
516    pub fn release_all(&self, txn_id: u64) {
517        let mut inner = self.inner.lock();
518        inner.fates.remove(&txn_id);
519        inner.priorities.remove(&txn_id);
520        let Some(keys) = inner.txn_keys.remove(&txn_id) else {
521            return;
522        };
523        #[cfg(test)]
524        {
525            inner.release_all_key_visits = inner.release_all_key_visits.saturating_add(keys.len());
526        }
527
528        let mut affected = Vec::with_capacity(keys.len());
529        for key in keys {
530            let removed_waiters = if let Some(state) = inner.locks.get_mut(&key) {
531                state.holders.retain(|holder| holder.txn_id != txn_id);
532                let before = state.queue.len();
533                state.queue.retain(|waiter| waiter.txn_id != txn_id);
534                before - state.queue.len()
535            } else {
536                0
537            };
538            inner.queued_waiters = inner.queued_waiters.saturating_sub(removed_waiters);
539            affected.push(key);
540        }
541        for key in &affected {
542            process_key(&mut inner, key);
543        }
544        detect_deadlocks(&mut inner);
545        for key in &affected {
546            prune_if_idle(&mut inner, key);
547        }
548        drop(inner);
549        self.wake.notify_all();
550    }
551
552    /// Whether `txn_id` currently holds any mode on `key`.
553    pub fn holds(&self, txn_id: u64, key: &LockKey) -> bool {
554        self.inner
555            .lock()
556            .locks
557            .get(key)
558            .is_some_and(|state| state.holders.iter().any(|h| h.txn_id == txn_id))
559    }
560
561    /// Number of queued waiters on `key`; test-only introspection used to
562    /// sequence multi-threaded tests deterministically.
563    #[cfg(test)]
564    fn queued_waiters(&self, key: &LockKey) -> usize {
565        self.inner
566            .lock()
567            .locks
568            .get(key)
569            .map_or(0, |state| state.queue.len())
570    }
571
572    #[cfg(test)]
573    fn total_queued_waiters(&self) -> usize {
574        self.inner.lock().queued_waiters
575    }
576
577    #[cfg(test)]
578    fn tracked_key_count(&self, txn_id: u64) -> usize {
579        self.inner
580            .lock()
581            .txn_keys
582            .get(&txn_id)
583            .map_or(0, HashSet::len)
584    }
585
586    #[cfg(test)]
587    fn deadlock_graph_builds(&self) -> usize {
588        self.inner.lock().deadlock_graph_builds
589    }
590
591    #[cfg(test)]
592    fn release_all_key_visits(&self) -> usize {
593        self.inner.lock().release_all_key_visits
594    }
595
596    /// Blocks until this waiter is granted or leaves the queue with an error.
597    /// The queue invariant: an entry leaves only by grant (it becomes a
598    /// holder) or by fate (an error recorded in `fates`).
599    fn wait_for_grant(
600        &self,
601        inner: &mut MutexGuard<'_, Inner>,
602        key: &LockKey,
603        txn_id: u64,
604        control: &ExecutionControl,
605        deadline: Option<Instant>,
606    ) -> Result<(), LockError> {
607        loop {
608            if let Some(fate) = inner.fates.remove(&txn_id) {
609                return Err(fate.into_error());
610            }
611            let (queued, held) = match inner.locks.get(key) {
612                Some(state) => (
613                    state.queue.iter().any(|w| w.txn_id == txn_id),
614                    state.holders.iter().any(|h| h.txn_id == txn_id),
615                ),
616                None => (false, false),
617            };
618            if !queued {
619                if held {
620                    return Ok(());
621                }
622                // The entry vanished without a grant and without a fate: only
623                // a release_all racing this wait (a usage-contract violation)
624                // can do that. Fail closed instead of hanging.
625                return Err(LockError::Cancelled);
626            }
627            if let Err(error) = check_request_live(control, deadline) {
628                remove_waiter(inner, key, txn_id);
629                return Err(error);
630            }
631            // Wake at the earlier of the lock-wait deadline and the next
632            // cancellation poll.
633            let wake_at = deadline
634                .unwrap_or_else(|| Instant::now() + CANCELLATION_POLL_INTERVAL)
635                .min(Instant::now() + CANCELLATION_POLL_INTERVAL);
636            let _ = self.wake.wait_until(inner, wake_at);
637        }
638    }
639}
640
641/// Fails a request whose cancellation already fired or whose lock-wait
642/// deadline already passed. [`ExecutionControl::checkpoint`] self-cancels an
643/// expired control with the Deadline reason, so the control's own deadline is
644/// honored here too.
645fn check_request_live(
646    control: &ExecutionControl,
647    deadline: Option<Instant>,
648) -> Result<(), LockError> {
649    match control.checkpoint() {
650        Ok(()) => {}
651        Err(MongrelError::DeadlineExceeded) => return Err(LockError::DeadlineExceeded),
652        Err(_) => return Err(LockError::Cancelled),
653    }
654    if deadline.is_some_and(|deadline| Instant::now() >= deadline) {
655        return Err(LockError::DeadlineExceeded);
656    }
657    Ok(())
658}
659
660fn remove_waiter(inner: &mut Inner, key: &LockKey, txn_id: u64) {
661    let removed = if let Some(state) = inner.locks.get_mut(key) {
662        let Some(position) = state
663            .queue
664            .iter()
665            .position(|waiter| waiter.txn_id == txn_id)
666        else {
667            return;
668        };
669        state.queue.remove(position);
670        true
671    } else {
672        false
673    };
674    if removed {
675        inner.queued_waiters = inner.queued_waiters.saturating_sub(1);
676        untrack_transaction_key_if_unused(inner, key, txn_id);
677    }
678}
679
680fn transaction_touches_key(inner: &Inner, key: &LockKey, txn_id: u64) -> bool {
681    inner.locks.get(key).is_some_and(|state| {
682        state.holders.iter().any(|holder| holder.txn_id == txn_id)
683            || state.queue.iter().any(|waiter| waiter.txn_id == txn_id)
684    })
685}
686
687fn untrack_transaction_key_if_unused(inner: &mut Inner, key: &LockKey, txn_id: u64) {
688    if transaction_touches_key(inner, key, txn_id) {
689        return;
690    }
691    let remove_transaction = inner.txn_keys.get_mut(&txn_id).is_some_and(|keys| {
692        keys.remove(key);
693        keys.is_empty()
694    });
695    if remove_transaction {
696        inner.txn_keys.remove(&txn_id);
697    }
698}
699
700fn prune_if_idle(inner: &mut Inner, key: &LockKey) {
701    if inner
702        .locks
703        .get(key)
704        .is_some_and(|state| state.holders.is_empty() && state.queue.is_empty())
705    {
706        inner.locks.remove(key);
707    }
708}
709
710/// Purges waiters that can never proceed (cancelled or past deadline) and
711/// grants the maximal compatible FIFO prefix of the queue. Granting stops at
712/// the first waiter whose mode conflicts with any holder — no barging, no
713/// skipping.
714fn process_key(inner: &mut Inner, key: &LockKey) {
715    let now = Instant::now();
716    let dead: Vec<u64> = match inner.locks.get(key) {
717        Some(state) => state
718            .queue
719            .iter()
720            .filter(|w| w.control.is_cancelled() || w.deadline.is_some_and(|d| now >= d))
721            .map(|w| w.txn_id)
722            .collect(),
723        None => return,
724    };
725    let mut removed = Vec::new();
726    for txn_id in dead {
727        if let Some(state) = inner.locks.get_mut(key) {
728            if let Some(position) = state.queue.iter().position(|w| w.txn_id == txn_id) {
729                let waiter = state.queue.remove(position).expect("position found above");
730                inner.queued_waiters = inner.queued_waiters.saturating_sub(1);
731                let fate = match waiter.control.checkpoint() {
732                    Err(MongrelError::DeadlineExceeded) => Fate::DeadlineExceeded,
733                    Err(_) => Fate::Cancelled,
734                    // Not cancelled: the lock-wait deadline fired.
735                    Ok(()) => Fate::DeadlineExceeded,
736                };
737                inner.fates.entry(txn_id).or_insert(fate);
738                removed.push(txn_id);
739            }
740        }
741    }
742
743    while let Some(state) = inner.locks.get_mut(key) {
744        let Some(front) = state.queue.front() else {
745            break;
746        };
747        // The waiter's own existing hold never blocks it: that is what lets a
748        // queued Shared → Exclusive upgrade complete once other holds drain.
749        let grantable = state
750            .holders
751            .iter()
752            .all(|h| h.txn_id == front.txn_id || front.mode.compatible(h.mode));
753        if !grantable {
754            break;
755        }
756        let waiter = state.queue.pop_front().expect("front checked above");
757        inner.queued_waiters = inner.queued_waiters.saturating_sub(1);
758        match state.holders.iter_mut().find(|h| h.txn_id == waiter.txn_id) {
759            Some(holder) => holder.mode = LockMode::Exclusive,
760            None => state.holders.push(Holder {
761                txn_id: waiter.txn_id,
762                mode: waiter.mode,
763            }),
764        }
765    }
766    for txn_id in removed {
767        untrack_transaction_key_if_unused(inner, key, txn_id);
768    }
769}
770
771/// Builds the wait-for graph: an edge `waiter → other` means `waiter` cannot
772/// be granted until `other` leaves. Targets are incompatible holders of the
773/// waiter's key and incompatible waiters ahead of it in the FIFO queue. A
774/// transaction never waits on itself (re-entrancy and upgrade rules above),
775/// so self-edges are excluded. Nodes and adjacency lists are sorted, making
776/// the subsequent cycle search deterministic.
777fn build_wait_for_graph(inner: &Inner) -> BTreeMap<u64, Vec<u64>> {
778    let mut graph: BTreeMap<u64, Vec<u64>> = BTreeMap::new();
779    for state in inner.locks.values() {
780        for (index, waiter) in state.queue.iter().enumerate() {
781            let edges = graph.entry(waiter.txn_id).or_default();
782            for holder in &state.holders {
783                if holder.txn_id != waiter.txn_id && !waiter.mode.compatible(holder.mode) {
784                    edges.push(holder.txn_id);
785                }
786            }
787            for ahead in state.queue.iter().take(index) {
788                if ahead.txn_id != waiter.txn_id && !waiter.mode.compatible(ahead.mode) {
789                    edges.push(ahead.txn_id);
790                }
791            }
792        }
793    }
794    for edges in graph.values_mut() {
795        edges.sort_unstable();
796        edges.dedup();
797    }
798    graph
799}
800
801/// Finds one cycle with a deterministic depth-first search: roots and
802/// adjacency lists are visited in ascending transaction-ID order, so equal
803/// states always yield the same cycle. Returns the cycle members in wait
804/// order (each waits on the next, the last on the first).
805fn find_cycle(graph: &BTreeMap<u64, Vec<u64>>) -> Option<Vec<u64>> {
806    #[derive(Clone, Copy, PartialEq, Eq)]
807    enum Mark {
808        Visiting,
809        Done,
810    }
811
812    let mut marks: HashMap<u64, Mark> = HashMap::new();
813    let mut stack: Vec<u64> = Vec::new();
814    for &start in graph.keys() {
815        if marks.contains_key(&start) {
816            continue;
817        }
818        marks.insert(start, Mark::Visiting);
819        stack.push(start);
820        let mut frames: Vec<(u64, usize)> = vec![(start, 0)];
821        while let Some((node, index)) = frames.last().copied() {
822            let edges = graph.get(&node).map(Vec::as_slice).unwrap_or(&[]);
823            if index < edges.len() {
824                frames.last_mut().expect("last copied above").1 += 1;
825                let next = edges[index];
826                match marks.get(&next) {
827                    None => {
828                        marks.insert(next, Mark::Visiting);
829                        stack.push(next);
830                        frames.push((next, 0));
831                    }
832                    Some(Mark::Visiting) => {
833                        let position = stack
834                            .iter()
835                            .position(|member| *member == next)
836                            .expect("a visiting node is on the stack");
837                        return Some(stack[position..].to_vec());
838                    }
839                    Some(Mark::Done) => {}
840                }
841            } else {
842                marks.insert(node, Mark::Done);
843                stack.pop();
844                frames.pop();
845            }
846        }
847    }
848    None
849}
850
851/// Deterministic victim selection (spec section 10.2): the lowest priority
852/// value dies first — transactions that never stated one share
853/// [`DEFAULT_PRIORITY`] — and ties go to the youngest transaction, the largest
854/// `u64` ID under core's monotonic allocator.
855fn choose_victim(cycle: &[u64], priorities: &HashMap<u64, u64>) -> u64 {
856    *cycle
857        .iter()
858        .min_by(|a, b| {
859            let priority_a = priorities.get(a).copied().unwrap_or(DEFAULT_PRIORITY);
860            let priority_b = priorities.get(b).copied().unwrap_or(DEFAULT_PRIORITY);
861            priority_a.cmp(&priority_b).then_with(|| b.cmp(a))
862        })
863        .expect("a deadlock cycle is never empty")
864}
865
866/// Detects and resolves deadlocks until none remain: each doomed victim is
867/// removed from its queue and its [`Fate::Deadlock`] recorded, so every
868/// iteration strictly shrinks the waiter set and the loop terminates.
869fn detect_deadlocks(inner: &mut Inner) {
870    while inner.queued_waiters != 0 {
871        #[cfg(test)]
872        {
873            inner.deadlock_graph_builds = inner.deadlock_graph_builds.saturating_add(1);
874        }
875        let graph = build_wait_for_graph(inner);
876        let Some(cycle) = find_cycle(&graph) else {
877            return;
878        };
879        let victim = choose_victim(&cycle, &inner.priorities);
880        // Every cycle member waits on the next, so the victim is always a
881        // queued waiter of exactly one key.
882        let victim_key = inner.locks.iter().find_map(|(key, state)| {
883            state
884                .queue
885                .iter()
886                .any(|w| w.txn_id == victim)
887                .then(|| key.clone())
888        });
889        let Some(victim_key) = victim_key else {
890            // Defensive: the victim left the queue between graph build and
891            // doom. Nothing to kill; re-detect on the next state change.
892            return;
893        };
894        let removed = inner.locks.get_mut(&victim_key).is_some_and(|state| {
895            let Some(position) = state
896                .queue
897                .iter()
898                .position(|waiter| waiter.txn_id == victim)
899            else {
900                return false;
901            };
902            state.queue.remove(position);
903            true
904        });
905        if removed {
906            inner.queued_waiters = inner.queued_waiters.saturating_sub(1);
907            untrack_transaction_key_if_unused(inner, &victim_key, victim);
908        }
909        let cycle = cycle
910            .iter()
911            .map(u64::to_string)
912            .collect::<Vec<_>>()
913            .join(" → ");
914        inner.fates.insert(victim, Fate::Deadlock { victim, cycle });
915    }
916}
917
918#[cfg(test)]
919mod tests {
920    use super::*;
921    use crate::CancellationReason;
922    use std::sync::mpsc;
923    use std::sync::Arc;
924    use std::thread;
925
926    use LockMode::{Exclusive, Shared};
927
928    fn manager() -> Arc<LockManager> {
929        Arc::new(LockManager::new())
930    }
931
932    fn request(txn_id: u64, mode: LockMode) -> LockRequest {
933        LockRequest::new(txn_id, mode, ExecutionControl::new(None))
934    }
935
936    fn timed_request(txn_id: u64, mode: LockMode, timeout: Duration) -> LockRequest {
937        request(txn_id, mode).with_timeout(timeout)
938    }
939
940    fn row_key(n: u64) -> LockKey {
941        LockKey::row(1, RowId(n))
942    }
943
944    /// Runs one acquire on its own thread and delivers the result.
945    fn spawn_acquire(
946        manager: &Arc<LockManager>,
947        key: LockKey,
948        request: LockRequest,
949    ) -> mpsc::Receiver<Result<(), LockError>> {
950        let (sender, receiver) = mpsc::channel();
951        let manager = Arc::clone(manager);
952        thread::spawn(move || {
953            let result = manager.acquire(key, request);
954            sender.send(result).expect("receiver alive");
955        });
956        receiver
957    }
958
959    /// Waits until `key` has at least `n` queued waiters, so multi-threaded
960    /// tests sequence enqueue order deterministically instead of relying on
961    /// sleeps. Setup-only; assertions never depend on this timing.
962    fn wait_for_queue(manager: &LockManager, key: &LockKey, n: usize) {
963        for _ in 0..200 {
964            if manager.queued_waiters(key) >= n {
965                return;
966            }
967            thread::sleep(Duration::from_millis(10));
968        }
969        panic!("queue did not reach {n} waiters on {key:?}");
970    }
971
972    #[test]
973    fn acquire_and_release_round_trip() {
974        let manager = LockManager::new();
975        let key = row_key(1);
976        manager.acquire(key.clone(), request(1, Exclusive)).unwrap();
977        assert!(manager.holds(1, &key));
978        manager.release(1, &key);
979        assert!(!manager.holds(1, &key));
980
981        manager.acquire(key.clone(), request(2, Shared)).unwrap();
982        assert!(manager.holds(2, &key));
983        manager.release_all(2);
984        assert!(!manager.holds(2, &key));
985    }
986
987    #[test]
988    fn mode_compatibility_matrix() {
989        assert!(Shared.compatible(Shared));
990        assert!(!Shared.compatible(Exclusive));
991        assert!(!Exclusive.compatible(Shared));
992        assert!(!Exclusive.compatible(Exclusive));
993    }
994
995    #[test]
996    fn shared_locks_coexist() {
997        let manager = LockManager::new();
998        let key = row_key(7);
999        manager.acquire(key.clone(), request(1, Shared)).unwrap();
1000        manager.acquire(key.clone(), request(2, Shared)).unwrap();
1001        assert!(manager.holds(1, &key));
1002        assert!(manager.holds(2, &key));
1003    }
1004
1005    #[test]
1006    fn exclusive_blocks_shared_and_exclusive_until_release() {
1007        let manager = LockManager::new();
1008        let key = row_key(8);
1009        manager.acquire(key.clone(), request(1, Exclusive)).unwrap();
1010
1011        let shared = manager.acquire(
1012            key.clone(),
1013            timed_request(2, Shared, Duration::from_millis(40)),
1014        );
1015        assert_eq!(shared, Err(LockError::DeadlineExceeded));
1016        let exclusive = manager.acquire(
1017            key.clone(),
1018            timed_request(3, Exclusive, Duration::from_millis(40)),
1019        );
1020        assert_eq!(exclusive, Err(LockError::DeadlineExceeded));
1021
1022        manager.release(1, &key);
1023        manager.acquire(key.clone(), request(2, Shared)).unwrap();
1024        let still_blocked = manager.acquire(
1025            key.clone(),
1026            timed_request(3, Exclusive, Duration::from_millis(40)),
1027        );
1028        assert_eq!(still_blocked, Err(LockError::DeadlineExceeded));
1029    }
1030
1031    #[test]
1032    fn wait_respects_the_lock_deadline() {
1033        let manager = LockManager::new();
1034        let key = row_key(9);
1035        manager.acquire(key.clone(), request(1, Exclusive)).unwrap();
1036
1037        let started = Instant::now();
1038        let result = manager.acquire(key, timed_request(2, Exclusive, Duration::from_millis(80)));
1039        let elapsed = started.elapsed();
1040        assert_eq!(result, Err(LockError::DeadlineExceeded));
1041        assert_eq!(
1042            result.unwrap_err().category(),
1043            ErrorCategory::DeadlineExceeded
1044        );
1045        assert!(
1046            elapsed >= Duration::from_millis(70),
1047            "returned early: {elapsed:?}"
1048        );
1049        assert!(
1050            elapsed < Duration::from_secs(5),
1051            "returned late: {elapsed:?}"
1052        );
1053    }
1054
1055    #[test]
1056    fn expired_deadline_fails_fast_even_when_unlocked() {
1057        let manager = LockManager::new();
1058        let result = manager.acquire(row_key(10), timed_request(1, Exclusive, Duration::ZERO));
1059        assert_eq!(result, Err(LockError::DeadlineExceeded));
1060    }
1061
1062    #[test]
1063    fn cancellation_wakes_the_waiter_and_clears_its_queue_entry() {
1064        let manager = manager();
1065        let key = row_key(11);
1066        manager.acquire(key.clone(), request(1, Exclusive)).unwrap();
1067
1068        let control = ExecutionControl::new(None);
1069        let cancelled_rx = spawn_acquire(
1070            &manager,
1071            key.clone(),
1072            LockRequest::new(2, Exclusive, control.clone()),
1073        );
1074        wait_for_queue(&manager, &key, 1);
1075        control.cancel(CancellationReason::ClientRequest);
1076        let result = cancelled_rx.recv_timeout(Duration::from_secs(5)).unwrap();
1077        assert_eq!(result, Err(LockError::Cancelled));
1078        assert!(!manager.holds(2, &key));
1079
1080        // The cancelled waiter's entry is gone: the next requester is granted
1081        // as soon as the holder releases.
1082        let next_rx = spawn_acquire(&manager, key.clone(), request(3, Exclusive));
1083        wait_for_queue(&manager, &key, 1);
1084        manager.release(1, &key);
1085        assert_eq!(
1086            next_rx.recv_timeout(Duration::from_secs(5)).unwrap(),
1087            Ok(())
1088        );
1089    }
1090
1091    #[test]
1092    fn cancelled_control_fails_fast_even_when_unlocked() {
1093        let manager = LockManager::new();
1094        let control = ExecutionControl::new(None);
1095        control.cancel(CancellationReason::ClientRequest);
1096        let result = manager.acquire(row_key(12), LockRequest::new(1, Exclusive, control));
1097        assert_eq!(result, Err(LockError::Cancelled));
1098    }
1099
1100    #[test]
1101    fn control_deadline_maps_to_deadline_exceeded() {
1102        let manager = LockManager::new();
1103        let key = row_key(13);
1104        manager.acquire(key.clone(), request(1, Exclusive)).unwrap();
1105        let control = ExecutionControl::with_timeout(Duration::from_millis(50));
1106        let result = manager.acquire(key, LockRequest::new(2, Exclusive, control));
1107        assert_eq!(result, Err(LockError::DeadlineExceeded));
1108    }
1109
1110    #[test]
1111    fn fifo_grant_order_blocks_barging_readers() {
1112        let manager = manager();
1113        let key = row_key(14);
1114        manager.acquire(key.clone(), request(1, Shared)).unwrap();
1115
1116        // The writer queues behind the shared holder.
1117        let writer_rx = spawn_acquire(&manager, key.clone(), request(2, Exclusive));
1118        wait_for_queue(&manager, &key, 1);
1119        // A reader arriving after the writer must not barge ahead of it, even
1120        // though Shared is compatible with the current holder: this is the
1121        // bounded writer-wait window (module docs).
1122        let reader_rx = spawn_acquire(&manager, key.clone(), request(3, Shared));
1123        wait_for_queue(&manager, &key, 2);
1124
1125        manager.release(1, &key);
1126        assert_eq!(
1127            writer_rx.recv_timeout(Duration::from_secs(5)).unwrap(),
1128            Ok(())
1129        );
1130        assert!(
1131            reader_rx.recv_timeout(Duration::from_millis(150)).is_err(),
1132            "reader barged ahead of the queued writer"
1133        );
1134        manager.release(2, &key);
1135        assert_eq!(
1136            reader_rx.recv_timeout(Duration::from_secs(5)).unwrap(),
1137            Ok(())
1138        );
1139    }
1140
1141    #[test]
1142    fn release_all_grants_blocked_waiters() {
1143        let manager = manager();
1144        let key = row_key(24);
1145        manager.acquire(key.clone(), request(1, Exclusive)).unwrap();
1146        let waiter_rx = spawn_acquire(&manager, key.clone(), request(2, Exclusive));
1147        wait_for_queue(&manager, &key, 1);
1148        manager.release_all(1);
1149        assert_eq!(
1150            waiter_rx.recv_timeout(Duration::from_secs(5)).unwrap(),
1151            Ok(())
1152        );
1153        assert!(manager.holds(2, &key));
1154    }
1155
1156    #[test]
1157    fn ab_ba_deadlock_kills_requester_when_it_is_youngest() {
1158        let manager = manager();
1159        let key_a = row_key(20);
1160        let key_b = row_key(21);
1161        manager
1162            .acquire(key_a.clone(), request(1, Exclusive))
1163            .unwrap();
1164        manager
1165            .acquire(key_b.clone(), request(2, Exclusive))
1166            .unwrap();
1167
1168        // t1 blocks on B (held by t2).
1169        let t1_rx = spawn_acquire(&manager, key_b.clone(), request(1, Exclusive));
1170        wait_for_queue(&manager, &key_b, 1);
1171        // t2 closes the cycle and is the youngest transaction, so t2 — the
1172        // requester — is the deterministic victim and fails synchronously.
1173        let error = manager
1174            .acquire(key_a.clone(), request(2, Exclusive))
1175            .unwrap_err();
1176        match &error {
1177            LockError::Deadlock { victim, cycle } => {
1178                assert_eq!(*victim, 2, "youngest transaction must be the victim");
1179                assert!(
1180                    cycle.contains('1') && cycle.contains('2'),
1181                    "cycle names both: {cycle}"
1182                );
1183            }
1184            other => panic!("expected t2 to be the deadlock victim, got {other:?}"),
1185        }
1186        assert_eq!(error.category(), ErrorCategory::Deadlock);
1187
1188        // The victim aborts: the survivor's wait completes.
1189        manager.release_all(2);
1190        assert_eq!(t1_rx.recv_timeout(Duration::from_secs(5)).unwrap(), Ok(()));
1191        assert!(manager.holds(1, &key_b));
1192    }
1193
1194    #[test]
1195    fn ab_ba_deadlock_kills_youngest_even_when_it_is_not_the_requester() {
1196        // IDs chosen so the requester closing the cycle is the OLDER
1197        // transaction: victim selection, not request order, decides who dies.
1198        let manager = manager();
1199        let key_a = row_key(22);
1200        let key_b = row_key(23);
1201        manager
1202            .acquire(key_a.clone(), request(100, Exclusive))
1203            .unwrap();
1204        manager
1205            .acquire(key_b.clone(), request(200, Exclusive))
1206            .unwrap();
1207
1208        // t200 blocks on A (held by t100), then aborts if doomed.
1209        let t200 = {
1210            let manager = Arc::clone(&manager);
1211            let key_a = key_a.clone();
1212            thread::spawn(move || {
1213                let result = manager.acquire(key_a, request(200, Exclusive));
1214                if result.is_err() {
1215                    manager.release_all(200);
1216                }
1217                result
1218            })
1219        };
1220        wait_for_queue(&manager, &key_a, 1);
1221
1222        // t100 closes the cycle and SURVIVES: it waits until the victim
1223        // aborts, then is granted B.
1224        manager
1225            .acquire(key_b.clone(), request(100, Exclusive))
1226            .unwrap();
1227        assert!(manager.holds(100, &key_b));
1228
1229        let victim = t200.join().unwrap();
1230        assert!(
1231            matches!(victim, Err(LockError::Deadlock { victim: 200, .. })),
1232            "youngest transaction must be the victim: {victim:?}"
1233        );
1234    }
1235
1236    #[test]
1237    fn three_transaction_cycle_kills_youngest() {
1238        let manager = manager();
1239        let key_a = row_key(30);
1240        let key_b = row_key(31);
1241        let key_c = row_key(32);
1242        manager
1243            .acquire(key_a.clone(), request(1, Exclusive))
1244            .unwrap();
1245        manager
1246            .acquire(key_b.clone(), request(2, Exclusive))
1247            .unwrap();
1248        manager
1249            .acquire(key_c.clone(), request(3, Exclusive))
1250            .unwrap();
1251
1252        // t1 waits on B, t2 waits on C.
1253        let t1_rx = spawn_acquire(&manager, key_b.clone(), request(1, Exclusive));
1254        wait_for_queue(&manager, &key_b, 1);
1255        let t2_rx = spawn_acquire(&manager, key_c.clone(), request(2, Exclusive));
1256        wait_for_queue(&manager, &key_c, 1);
1257        // t3 waits on A, closing t1 → t2 → t3 → t1; t3 is youngest and dies.
1258        let error = manager
1259            .acquire(key_a.clone(), request(3, Exclusive))
1260            .unwrap_err();
1261        assert!(
1262            matches!(error, LockError::Deadlock { victim: 3, .. }),
1263            "youngest of the 3-cycle must die: {error:?}"
1264        );
1265
1266        // Unwind: t3's abort grants C to t2; t2's abort grants B to t1.
1267        manager.release_all(3);
1268        assert_eq!(t2_rx.recv_timeout(Duration::from_secs(5)).unwrap(), Ok(()));
1269        manager.release_all(2);
1270        assert_eq!(t1_rx.recv_timeout(Duration::from_secs(5)).unwrap(), Ok(()));
1271    }
1272
1273    #[test]
1274    fn lowest_priority_dies_before_age_is_considered() {
1275        let manager = manager();
1276        let key_a = row_key(40);
1277        let key_b = row_key(41);
1278        let key_c = row_key(42);
1279        manager
1280            .acquire(key_a.clone(), request(1, Exclusive))
1281            .unwrap();
1282        manager
1283            .acquire(key_b.clone(), request(2, Exclusive))
1284            .unwrap();
1285        manager
1286            .acquire(key_c.clone(), request(3, Exclusive))
1287            .unwrap();
1288
1289        // Priorities t1 = 10, t2 = 20, t3 = 30: t1 is the victim even though
1290        // t3 is the youngest transaction and t3's enqueue closes the cycle.
1291        let t1 = {
1292            let manager = Arc::clone(&manager);
1293            let key_b = key_b.clone();
1294            thread::spawn(move || {
1295                let result = manager.acquire(key_b, request(1, Exclusive).with_priority(10));
1296                if result.is_err() {
1297                    manager.release_all(1);
1298                }
1299                result
1300            })
1301        };
1302        wait_for_queue(&manager, &key_b, 1);
1303        let t2_rx = spawn_acquire(
1304            &manager,
1305            key_c.clone(),
1306            request(2, Exclusive).with_priority(20),
1307        );
1308        wait_for_queue(&manager, &key_c, 1);
1309
1310        // t3 closes the cycle, survives, and is granted A once t1 aborts.
1311        manager
1312            .acquire(key_a.clone(), request(3, Exclusive).with_priority(30))
1313            .unwrap();
1314        assert!(manager.holds(3, &key_a));
1315
1316        let victim = t1.join().unwrap();
1317        assert!(
1318            matches!(victim, Err(LockError::Deadlock { victim: 1, .. })),
1319            "lowest priority must die: {victim:?}"
1320        );
1321
1322        manager.release_all(3);
1323        assert_eq!(t2_rx.recv_timeout(Duration::from_secs(5)).unwrap(), Ok(()));
1324    }
1325
1326    #[test]
1327    fn equal_priorities_fall_back_to_youngest() {
1328        let manager = manager();
1329        let key_a = row_key(50);
1330        let key_b = row_key(51);
1331        manager
1332            .acquire(key_a.clone(), request(1, Exclusive).with_priority(5))
1333            .unwrap();
1334        manager
1335            .acquire(key_b.clone(), request(2, Exclusive).with_priority(5))
1336            .unwrap();
1337
1338        let t1_rx = spawn_acquire(
1339            &manager,
1340            key_b.clone(),
1341            request(1, Exclusive).with_priority(5),
1342        );
1343        wait_for_queue(&manager, &key_b, 1);
1344        let error = manager
1345            .acquire(key_a.clone(), request(2, Exclusive).with_priority(5))
1346            .unwrap_err();
1347        assert!(
1348            matches!(error, LockError::Deadlock { victim: 2, .. }),
1349            "equal priorities must fall back to youngest: {error:?}"
1350        );
1351        manager.release_all(2);
1352        assert_eq!(t1_rx.recv_timeout(Duration::from_secs(5)).unwrap(), Ok(()));
1353    }
1354
1355    #[test]
1356    fn same_transaction_requests_never_self_deadlock() {
1357        let manager = LockManager::new();
1358        let key = row_key(60);
1359        // Re-entrant requests are no-ops, so re-issued acquisitions cannot
1360        // deadlock a transaction against itself.
1361        manager.acquire(key.clone(), request(1, Exclusive)).unwrap();
1362        manager.acquire(key.clone(), request(1, Exclusive)).unwrap();
1363        manager.acquire(key.clone(), request(1, Shared)).unwrap();
1364        assert!(manager.holds(1, &key));
1365
1366        // A sole-holder Shared → Exclusive upgrade succeeds immediately and
1367        // really excludes others afterwards.
1368        let other = row_key(61);
1369        manager.acquire(other.clone(), request(2, Shared)).unwrap();
1370        manager
1371            .acquire(other.clone(), request(2, Exclusive))
1372            .unwrap();
1373        let blocked = manager.acquire(
1374            other,
1375            timed_request(3, Exclusive, Duration::from_millis(40)),
1376        );
1377        assert_eq!(blocked, Err(LockError::DeadlineExceeded));
1378    }
1379
1380    #[test]
1381    fn detector_kills_a_self_loop_if_one_is_ever_constructed() {
1382        // build_wait_for_graph never emits self-edges, so no real state can
1383        // produce a single-transaction cycle; if a self-loop ever appears the
1384        // detector must still doom the looping transaction instead of hanging.
1385        let graph = BTreeMap::from([(7_u64, vec![7_u64])]);
1386        let cycle = find_cycle(&graph).expect("a self loop is a cycle");
1387        assert_eq!(cycle, vec![7]);
1388        assert_eq!(choose_victim(&cycle, &HashMap::new()), 7);
1389    }
1390
1391    #[test]
1392    fn shared_to_exclusive_upgrade_deadlock_kills_youngest() {
1393        let manager = manager();
1394        let key = row_key(70);
1395        manager.acquire(key.clone(), request(1, Shared)).unwrap();
1396        manager.acquire(key.clone(), request(2, Shared)).unwrap();
1397
1398        // t1 converts S → X and queues behind t2's Shared hold.
1399        let t1_rx = spawn_acquire(&manager, key.clone(), request(1, Exclusive));
1400        wait_for_queue(&manager, &key, 1);
1401        // t2 converts S → X, closing the classic conversion cycle; t2, the
1402        // youngest, is the victim.
1403        let error = manager
1404            .acquire(key.clone(), request(2, Exclusive))
1405            .unwrap_err();
1406        assert!(
1407            matches!(error, LockError::Deadlock { victim: 2, .. }),
1408            "youngest upgrader must die: {error:?}"
1409        );
1410
1411        // t2 aborts; t1's queued upgrade completes over its own Shared hold.
1412        manager.release_all(2);
1413        assert_eq!(t1_rx.recv_timeout(Duration::from_secs(5)).unwrap(), Ok(()));
1414        let blocked = manager.acquire(
1415            key.clone(),
1416            timed_request(3, Shared, Duration::from_millis(40)),
1417        );
1418        assert_eq!(
1419            blocked,
1420            Err(LockError::DeadlineExceeded),
1421            "t1 must now hold Exclusive"
1422        );
1423    }
1424
1425    #[test]
1426    fn sequence_barrier_is_exclusive_across_transactions() {
1427        let manager = LockManager::new();
1428        let sequence = LockKey::sequence_barrier("order_id");
1429        manager
1430            .acquire(sequence.clone(), request(1, Exclusive))
1431            .unwrap();
1432
1433        // A concurrent allocation on the same sequence blocks ...
1434        let blocked = manager.acquire(
1435            sequence.clone(),
1436            timed_request(2, Exclusive, Duration::from_millis(40)),
1437        );
1438        assert_eq!(blocked, Err(LockError::DeadlineExceeded));
1439        // ... while a different sequence is unaffected.
1440        manager
1441            .acquire(
1442                LockKey::sequence_barrier("shipment_id"),
1443                request(2, Exclusive),
1444            )
1445            .unwrap();
1446
1447        manager.release_all(1);
1448        manager.acquire(sequence, request(2, Exclusive)).unwrap();
1449    }
1450
1451    #[test]
1452    fn ddl_barrier_excludes_dml_and_concurrent_ddl() {
1453        let manager = LockManager::new();
1454        let barrier = LockKey::schema_barrier();
1455        // DDL takes the barrier exclusively.
1456        manager
1457            .acquire(barrier.clone(), request(1, Exclusive))
1458            .unwrap();
1459        // DML sharing the barrier blocks for the DDL's duration ...
1460        let dml = manager.acquire(
1461            barrier.clone(),
1462            timed_request(2, Shared, Duration::from_millis(40)),
1463        );
1464        assert_eq!(dml, Err(LockError::DeadlineExceeded));
1465        // ... and so does concurrent DDL.
1466        let ddl = manager.acquire(
1467            barrier.clone(),
1468            timed_request(3, Exclusive, Duration::from_millis(40)),
1469        );
1470        assert_eq!(ddl, Err(LockError::DeadlineExceeded));
1471
1472        manager.release_all(1);
1473        // With no DDL in flight, many DML transactions share the barrier.
1474        manager
1475            .acquire(barrier.clone(), request(2, Shared))
1476            .unwrap();
1477        manager.acquire(barrier, request(3, Shared)).unwrap();
1478    }
1479
1480    #[test]
1481    fn key_families_do_not_false_share() {
1482        let manager = LockManager::new();
1483        manager
1484            .acquire(LockKey::row(1, RowId(5)), request(1, Exclusive))
1485            .unwrap();
1486        // Same table, same numeric identity, different key family: no conflict.
1487        manager
1488            .acquire(
1489                LockKey::key(1, 5_u64.to_be_bytes().to_vec()),
1490                request(2, Exclusive),
1491            )
1492            .unwrap();
1493        manager
1494            .acquire(
1495                LockKey::range(
1496                    1,
1497                    Bound::Included(1_u64.to_be_bytes().to_vec()),
1498                    Bound::Excluded(6_u64.to_be_bytes().to_vec()),
1499                ),
1500                request(3, Exclusive),
1501            )
1502            .unwrap();
1503    }
1504
1505    #[test]
1506    fn lock_errors_bridge_to_mongrel_error_and_the_taxonomy() {
1507        let deadlock = LockError::Deadlock {
1508            victim: 9,
1509            cycle: "9 → 4 → 9".to_string(),
1510        };
1511        assert_eq!(deadlock.category(), ErrorCategory::Deadlock);
1512        let mapped = MongrelError::from(deadlock);
1513        assert!(
1514            matches!(
1515                &mapped,
1516                MongrelError::Deadlock { victim: 9, cycle } if cycle == "9 → 4 → 9"
1517            ),
1518            "deadlock maps to the dedicated variant, victim and cycle preserved: {mapped:?}"
1519        );
1520        assert_eq!(mapped.category(), ErrorCategory::Deadlock);
1521
1522        assert!(matches!(
1523            MongrelError::from(LockError::DeadlineExceeded),
1524            MongrelError::DeadlineExceeded
1525        ));
1526        assert!(matches!(
1527            MongrelError::from(LockError::Cancelled),
1528            MongrelError::Cancelled
1529        ));
1530        assert_eq!(LockError::Cancelled.category(), ErrorCategory::Cancelled);
1531        assert!(matches!(
1532            MongrelError::from(LockError::InvalidRequest("x".to_string())),
1533            MongrelError::InvalidArgument(_)
1534        ));
1535    }
1536
1537    #[test]
1538    fn release_all_visits_only_the_transactions_tracked_keys() {
1539        let manager = LockManager::new();
1540        for txn_id in 1..=128 {
1541            manager
1542                .acquire(row_key(txn_id), request(txn_id, Exclusive))
1543                .unwrap();
1544        }
1545        assert_eq!(manager.release_all_key_visits(), 0);
1546
1547        manager.release_all(999);
1548        assert_eq!(manager.release_all_key_visits(), 0);
1549
1550        manager.release_all(64);
1551        assert_eq!(manager.release_all_key_visits(), 1);
1552        assert!(!manager.holds(64, &row_key(64)));
1553        assert!(manager.holds(63, &row_key(63)));
1554        assert!(manager.holds(65, &row_key(65)));
1555    }
1556
1557    #[test]
1558    fn uncontended_release_skips_deadlock_graph_construction() {
1559        let manager = LockManager::new();
1560        for txn_id in 1..=128 {
1561            manager
1562                .acquire(row_key(txn_id), request(txn_id, Exclusive))
1563                .unwrap();
1564            manager.release_all(txn_id);
1565        }
1566        assert_eq!(manager.deadlock_graph_builds(), 0);
1567        assert_eq!(manager.total_queued_waiters(), 0);
1568    }
1569
1570    #[test]
1571    fn waiter_accounting_covers_grant_cancel_and_deadlock() {
1572        let manager = manager();
1573
1574        let grant_key = row_key(200);
1575        manager
1576            .acquire(grant_key.clone(), request(1, Exclusive))
1577            .unwrap();
1578        let granted = spawn_acquire(&manager, grant_key.clone(), request(2, Exclusive));
1579        wait_for_queue(&manager, &grant_key, 1);
1580        assert_eq!(manager.total_queued_waiters(), 1);
1581        assert_eq!(manager.tracked_key_count(2), 1);
1582        manager.release_all(1);
1583        assert_eq!(
1584            granted.recv_timeout(Duration::from_secs(5)).unwrap(),
1585            Ok(())
1586        );
1587        assert_eq!(manager.total_queued_waiters(), 0);
1588        manager.release_all(2);
1589
1590        let cancel_key = row_key(201);
1591        manager
1592            .acquire(cancel_key.clone(), request(3, Exclusive))
1593            .unwrap();
1594        let control = ExecutionControl::new(None);
1595        let cancelled = spawn_acquire(
1596            &manager,
1597            cancel_key.clone(),
1598            LockRequest::new(4, Exclusive, control.clone()),
1599        );
1600        wait_for_queue(&manager, &cancel_key, 1);
1601        assert_eq!(manager.total_queued_waiters(), 1);
1602        control.cancel(CancellationReason::ClientRequest);
1603        assert_eq!(
1604            cancelled.recv_timeout(Duration::from_secs(5)).unwrap(),
1605            Err(LockError::Cancelled)
1606        );
1607        assert_eq!(manager.total_queued_waiters(), 0);
1608        assert_eq!(manager.tracked_key_count(4), 0);
1609        manager.release_all(3);
1610
1611        let key_a = row_key(202);
1612        let key_b = row_key(203);
1613        manager
1614            .acquire(key_a.clone(), request(10, Exclusive))
1615            .unwrap();
1616        manager
1617            .acquire(key_b.clone(), request(11, Exclusive))
1618            .unwrap();
1619        let survivor = spawn_acquire(&manager, key_b.clone(), request(10, Exclusive));
1620        wait_for_queue(&manager, &key_b, 1);
1621        assert_eq!(manager.total_queued_waiters(), 1);
1622        assert!(matches!(
1623            manager.acquire(key_a, request(11, Exclusive)),
1624            Err(LockError::Deadlock { victim: 11, .. })
1625        ));
1626        assert_eq!(manager.total_queued_waiters(), 1);
1627        manager.release_all(11);
1628        assert_eq!(
1629            survivor.recv_timeout(Duration::from_secs(5)).unwrap(),
1630            Ok(())
1631        );
1632        assert_eq!(manager.total_queued_waiters(), 0);
1633        manager.release_all(10);
1634    }
1635
1636    #[test]
1637    fn wait_for_graph_cycle_search_is_deterministic() {
1638        // No cycle: a plain wait chain.
1639        let graph = BTreeMap::from([(1, vec![2]), (2, vec![3])]);
1640        assert_eq!(find_cycle(&graph), None);
1641
1642        // 1 → 2 → 3 → 2: the cycle is {2, 3}, found deterministically.
1643        let graph = BTreeMap::from([(1, vec![2]), (2, vec![3]), (3, vec![2])]);
1644        let cycle = find_cycle(&graph).expect("cycle");
1645        assert_eq!(cycle, vec![2, 3]);
1646
1647        // Victim selection: no priorities → youngest; explicit priorities →
1648        // lowest value dies; ties fall back to youngest.
1649        assert_eq!(choose_victim(&cycle, &HashMap::new()), 3);
1650        assert_eq!(choose_victim(&cycle, &HashMap::from([(2, 5), (3, 1)])), 3);
1651        assert_eq!(choose_victim(&cycle, &HashMap::from([(2, 1), (3, 5)])), 2);
1652    }
1653}