lock_db/mode.rs
1//! Lock modes and the compatibility matrix.
2//!
3//! The compatibility matrix is the correctness core of a lock manager: it
4//! decides, for any two transactions contending for the same resource, whether
5//! both requests can be held at once. Get this wrong and the manager hands out
6//! conflicting locks; everything above it then corrupts data. For that reason
7//! the matrix lives in one small, `const`, exhaustively tested place rather than
8//! being scattered across the acquire path.
9//!
10//! `lock-db` implements the five standard multi-granularity locking (MGL)
11//! modes. Two are the fundamental modes — shared (read) and exclusive (write).
12//! The other three are *intention* modes that a transaction takes on a coarse
13//! resource (a table, say) to announce that it intends to take a finer lock (a
14//! row) underneath it. Intention locks are what make hierarchical locking
15//! correct without forcing every reader to inspect every individual row lock:
16//! a transaction wanting to lock a whole table exclusively need only check that
17//! no one holds an intention lock on the table, instead of scanning every row.
18//!
19//! See the [crate-level docs](crate) for the granularity protocol that ties the
20//! modes to a database/table/page/row hierarchy.
21
22/// The mode in which a transaction holds, or wants to hold, a lock.
23///
24/// The five modes form a lattice ordered by privilege: holding a stronger mode
25/// grants the capabilities of every weaker mode it [covers](LockMode::covers).
26/// Acquiring a mode while already holding another upgrades to their
27/// [join](LockMode::join) (least upper bound) — for example shared + intention
28/// exclusive becomes [`SharedIntentionExclusive`](LockMode::SharedIntentionExclusive).
29///
30/// # Examples
31///
32/// ```
33/// use lock_db::LockMode;
34///
35/// // Two readers coexist; a writer excludes everyone.
36/// assert!(LockMode::Shared.compatible_with(LockMode::Shared));
37/// assert!(!LockMode::Shared.compatible_with(LockMode::Exclusive));
38///
39/// // Intention-shared coexists with everything except an exclusive lock.
40/// assert!(LockMode::IntentionShared.compatible_with(LockMode::SharedIntentionExclusive));
41/// assert!(!LockMode::IntentionShared.compatible_with(LockMode::Exclusive));
42/// ```
43#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
44#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
45pub enum LockMode {
46 /// Intention shared (IS). Announces intent to take shared locks on finer
47 /// resources beneath this one. Compatible with everything except exclusive.
48 IntentionShared,
49
50 /// Intention exclusive (IX). Announces intent to take exclusive (or shared)
51 /// locks on finer resources. Compatible with the intention modes only.
52 IntentionExclusive,
53
54 /// Shared (S). A read lock. Any number of transactions may hold a resource
55 /// `Shared` at once, alongside intention-shared holders.
56 Shared,
57
58 /// Shared and intention exclusive (SIX). The transaction reads the whole
59 /// subtree (S) and intends to write part of it (IX). Compatible only with
60 /// intention-shared.
61 SharedIntentionExclusive,
62
63 /// Exclusive (X). A write lock. Held by at most one transaction, and only
64 /// when no other transaction holds the resource in any mode.
65 Exclusive,
66}
67
68impl LockMode {
69 /// Maps a mode to its row/column in the compatibility and join tables.
70 #[inline]
71 const fn index(self) -> usize {
72 match self {
73 LockMode::IntentionShared => 0,
74 LockMode::IntentionExclusive => 1,
75 LockMode::Shared => 2,
76 LockMode::SharedIntentionExclusive => 3,
77 LockMode::Exclusive => 4,
78 }
79 }
80
81 /// Returns `true` if a lock in `self` mode and a lock in `other` mode may be
82 /// held on the same resource by two different transactions at once.
83 ///
84 /// This is the symmetric compatibility relation of the standard MGL matrix:
85 ///
86 /// | | IS | IX | S | SIX | X |
87 /// |-------|----|----|----|-----|----|
88 /// | **IS** | ✓ | ✓ | ✓ | ✓ | ✗ |
89 /// | **IX** | ✓ | ✓ | ✗ | ✗ | ✗ |
90 /// | **S** | ✓ | ✗ | ✓ | ✗ | ✗ |
91 /// | **SIX** | ✓ | ✗ | ✗ | ✗ | ✗ |
92 /// | **X** | ✗ | ✗ | ✗ | ✗ | ✗ |
93 ///
94 /// # Examples
95 ///
96 /// ```
97 /// use lock_db::LockMode;
98 ///
99 /// assert!(LockMode::IntentionShared.compatible_with(LockMode::IntentionExclusive));
100 /// assert!(!LockMode::IntentionExclusive.compatible_with(LockMode::Shared));
101 /// assert!(!LockMode::Exclusive.compatible_with(LockMode::IntentionShared));
102 /// ```
103 #[inline]
104 #[must_use]
105 pub const fn compatible_with(self, other: LockMode) -> bool {
106 // Rows/cols ordered IS, IX, S, SIX, X.
107 const COMPAT: [[bool; 5]; 5] = [
108 [true, true, true, true, false], // IS
109 [true, true, false, false, false], // IX
110 [true, false, true, false, false], // S
111 [true, false, false, false, false], // SIX
112 [false, false, false, false, false], // X
113 ];
114 COMPAT[self.index()][other.index()]
115 }
116
117 /// Returns the least mode that grants everything both `self` and `other`
118 /// grant — their least upper bound in the privilege lattice.
119 ///
120 /// This is what an upgrade resolves to: a transaction already holding `self`
121 /// that requests `other` ends up holding `self.join(other)`. For example a
122 /// reader (`Shared`) that announces intent to write part of the subtree
123 /// (`IntentionExclusive`) upgrades to `SharedIntentionExclusive`. `join` is
124 /// commutative and associative.
125 ///
126 /// # Examples
127 ///
128 /// ```
129 /// use lock_db::LockMode;
130 ///
131 /// assert_eq!(
132 /// LockMode::Shared.join(LockMode::IntentionExclusive),
133 /// LockMode::SharedIntentionExclusive,
134 /// );
135 /// assert_eq!(LockMode::IntentionShared.join(LockMode::Shared), LockMode::Shared);
136 /// assert_eq!(LockMode::Shared.join(LockMode::Exclusive), LockMode::Exclusive);
137 /// // join with itself is itself.
138 /// assert_eq!(LockMode::IntentionExclusive.join(LockMode::IntentionExclusive), LockMode::IntentionExclusive);
139 /// ```
140 #[inline]
141 #[must_use]
142 pub const fn join(self, other: LockMode) -> LockMode {
143 use LockMode::{
144 Exclusive, IntentionExclusive, IntentionShared, Shared, SharedIntentionExclusive,
145 };
146 // Rows/cols ordered IS, IX, S, SIX, X. Symmetric.
147 const JOIN: [[LockMode; 5]; 5] = [
148 // IS
149 [
150 IntentionShared,
151 IntentionExclusive,
152 Shared,
153 SharedIntentionExclusive,
154 Exclusive,
155 ],
156 // IX
157 [
158 IntentionExclusive,
159 IntentionExclusive,
160 SharedIntentionExclusive,
161 SharedIntentionExclusive,
162 Exclusive,
163 ],
164 // S
165 [
166 Shared,
167 SharedIntentionExclusive,
168 Shared,
169 SharedIntentionExclusive,
170 Exclusive,
171 ],
172 // SIX
173 [
174 SharedIntentionExclusive,
175 SharedIntentionExclusive,
176 SharedIntentionExclusive,
177 SharedIntentionExclusive,
178 Exclusive,
179 ],
180 // X
181 [Exclusive, Exclusive, Exclusive, Exclusive, Exclusive],
182 ];
183 JOIN[self.index()][other.index()]
184 }
185
186 /// Returns `true` if holding `self` already grants everything `other` would.
187 ///
188 /// Equivalent to `self.join(other) == self`: `self` sits at or above `other`
189 /// in the lattice. This drives the idempotent path of acquisition — a
190 /// request for a mode you already cover is a no-op.
191 ///
192 /// # Examples
193 ///
194 /// ```
195 /// use lock_db::LockMode;
196 ///
197 /// assert!(LockMode::Exclusive.covers(LockMode::Shared));
198 /// assert!(LockMode::SharedIntentionExclusive.covers(LockMode::Shared));
199 /// assert!(LockMode::SharedIntentionExclusive.covers(LockMode::IntentionExclusive));
200 /// assert!(!LockMode::Shared.covers(LockMode::IntentionExclusive));
201 /// ```
202 #[inline]
203 #[must_use]
204 pub const fn covers(self, other: LockMode) -> bool {
205 // `PartialEq` is not `const`, so compare discriminants directly.
206 self.join(other) as u8 == self as u8
207 }
208
209 /// Returns `true` for [`LockMode::Exclusive`].
210 ///
211 /// # Examples
212 ///
213 /// ```
214 /// use lock_db::LockMode;
215 ///
216 /// assert!(LockMode::Exclusive.is_exclusive());
217 /// assert!(!LockMode::Shared.is_exclusive());
218 /// ```
219 #[inline]
220 #[must_use]
221 pub const fn is_exclusive(self) -> bool {
222 matches!(self, LockMode::Exclusive)
223 }
224
225 /// Returns `true` for the intention modes (IS, IX, SIX).
226 ///
227 /// Intention modes are placeholders taken on a coarse resource to signal
228 /// that a finer lock will be taken beneath it; they are not themselves read
229 /// or write locks on the resource's own data.
230 ///
231 /// # Examples
232 ///
233 /// ```
234 /// use lock_db::LockMode;
235 ///
236 /// assert!(LockMode::IntentionShared.is_intention());
237 /// assert!(LockMode::SharedIntentionExclusive.is_intention());
238 /// assert!(!LockMode::Shared.is_intention());
239 /// assert!(!LockMode::Exclusive.is_intention());
240 /// ```
241 #[inline]
242 #[must_use]
243 pub const fn is_intention(self) -> bool {
244 matches!(
245 self,
246 LockMode::IntentionShared
247 | LockMode::IntentionExclusive
248 | LockMode::SharedIntentionExclusive
249 )
250 }
251}
252
253#[cfg(test)]
254mod tests {
255 use super::LockMode::{
256 self, Exclusive, IntentionExclusive, IntentionShared, Shared, SharedIntentionExclusive,
257 };
258
259 const ALL: [LockMode; 5] = [
260 IntentionShared,
261 IntentionExclusive,
262 Shared,
263 SharedIntentionExclusive,
264 Exclusive,
265 ];
266
267 #[test]
268 fn test_compatibility_matches_standard_mgl_matrix() {
269 // The canonical Gray/Reuter compatibility matrix, IS IX S SIX X.
270 let expected = [
271 [true, true, true, true, false],
272 [true, true, false, false, false],
273 [true, false, true, false, false],
274 [true, false, false, false, false],
275 [false, false, false, false, false],
276 ];
277 for (i, a) in ALL.iter().enumerate() {
278 for (j, b) in ALL.iter().enumerate() {
279 assert_eq!(
280 a.compatible_with(*b),
281 expected[i][j],
282 "compat({a:?}, {b:?})"
283 );
284 }
285 }
286 }
287
288 #[test]
289 fn test_compatible_is_symmetric() {
290 for a in ALL {
291 for b in ALL {
292 assert_eq!(a.compatible_with(b), b.compatible_with(a), "{a:?} vs {b:?}");
293 }
294 }
295 }
296
297 #[test]
298 fn test_exclusive_incompatible_with_all() {
299 for m in ALL {
300 assert!(!Exclusive.compatible_with(m));
301 assert!(!m.compatible_with(Exclusive));
302 }
303 }
304
305 #[test]
306 fn test_intention_shared_compatible_with_all_but_exclusive() {
307 for m in ALL {
308 assert_eq!(IntentionShared.compatible_with(m), m != Exclusive);
309 }
310 }
311
312 #[test]
313 fn test_join_is_commutative() {
314 for a in ALL {
315 for b in ALL {
316 assert_eq!(a.join(b), b.join(a), "join({a:?}, {b:?})");
317 }
318 }
319 }
320
321 #[test]
322 fn test_join_is_associative() {
323 for a in ALL {
324 for b in ALL {
325 for c in ALL {
326 assert_eq!(a.join(b).join(c), a.join(b.join(c)));
327 }
328 }
329 }
330 }
331
332 #[test]
333 fn test_join_idempotent() {
334 for m in ALL {
335 assert_eq!(m.join(m), m);
336 }
337 }
338
339 #[test]
340 fn test_join_examples() {
341 assert_eq!(Shared.join(IntentionExclusive), SharedIntentionExclusive);
342 assert_eq!(IntentionShared.join(Shared), Shared);
343 assert_eq!(IntentionShared.join(IntentionExclusive), IntentionExclusive);
344 assert_eq!(Shared.join(Exclusive), Exclusive);
345 assert_eq!(SharedIntentionExclusive.join(Exclusive), Exclusive);
346 }
347
348 #[test]
349 fn test_covers_is_join_equals_self() {
350 for a in ALL {
351 for b in ALL {
352 assert_eq!(a.covers(b), a.join(b) == a, "covers({a:?}, {b:?})");
353 }
354 }
355 }
356
357 #[test]
358 fn test_exclusive_covers_everything() {
359 for m in ALL {
360 assert!(Exclusive.covers(m));
361 }
362 }
363
364 #[test]
365 fn test_six_covers_its_components() {
366 assert!(SharedIntentionExclusive.covers(Shared));
367 assert!(SharedIntentionExclusive.covers(IntentionExclusive));
368 assert!(SharedIntentionExclusive.covers(IntentionShared));
369 assert!(!SharedIntentionExclusive.covers(Exclusive));
370 }
371
372 #[test]
373 fn test_covers_reflexive() {
374 for m in ALL {
375 assert!(m.covers(m));
376 }
377 }
378
379 #[test]
380 fn test_is_exclusive_and_is_intention() {
381 assert!(Exclusive.is_exclusive());
382 assert!(!Shared.is_exclusive());
383 for m in [
384 IntentionShared,
385 IntentionExclusive,
386 SharedIntentionExclusive,
387 ] {
388 assert!(m.is_intention());
389 }
390 assert!(!Shared.is_intention());
391 assert!(!Exclusive.is_intention());
392 }
393
394 #[test]
395 fn test_two_compatible_modes_join_below_any_conflict() {
396 // Sanity: if two modes are compatible, neither is exclusive.
397 for a in ALL {
398 for b in ALL {
399 if a.compatible_with(b) {
400 assert!(!(a.is_exclusive() && b.is_exclusive()));
401 }
402 }
403 }
404 }
405}