1use std::collections::HashMap;
4use std::marker::PhantomData;
5use std::sync::atomic::{AtomicU8, AtomicU64, Ordering};
6use std::sync::{Arc, LazyLock, Mutex, MutexGuard, Weak};
7
8#[cfg(unix)]
9use orbit_core::shm::{ShmRegion, ring_segment_name};
10use orbit_core::{Fleet, NetId64, NodeId};
11
12use crate::layout::LockLayout;
13use crate::{
14 Error, LockAcquire, LockKey, LockLease, LockOwner, LockTransition, Result,
15};
16
17pub const LOCK_STATE_CAPACITY: usize = orbit_core::compile::usize_from_env(
18 option_env!("ORBIT_LOCK_STATE_CAPACITY"),
19 256,
20);
21pub const LOCK_STATE_PAYLOAD_MAX: usize = orbit_core::compile::usize_from_env(
22 option_env!("ORBIT_LOCK_STATE_PAYLOAD_CAPACITY"),
23 960,
24);
25
26const LOCK_COUNTER_MAX: u64 = 0xff_ffff_ffff;
27const SLOT_EMPTY: u8 = 0;
28const SLOT_OCCUPIED: u8 = 1;
29const SLOT_TOMBSTONE: u8 = 2;
30const FNV_OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
31const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
32
33#[cfg(unix)]
34const STATE_MAGIC: u32 = 0x4f_4c_53_54; #[cfg(unix)]
36const STATE_VERSION: u16 = 2;
37
38#[derive(Clone)]
39pub(crate) struct LockStateStore<L: LockLayout> {
40 backend: StateBacking,
41 state_kind: u8,
42 node_id: NodeId,
43 _layout: PhantomData<L>,
44}
45
46#[derive(Clone)]
47enum StateBacking {
48 InMemory(Arc<Mutex<MemoryTable>>),
49 #[cfg(unix)]
50 Shm(Arc<ShmStateStore>),
51}
52
53impl<L: LockLayout> LockStateStore<L> {
54 pub(crate) fn new(fleet: &Arc<Fleet>) -> Self {
55 let backend = if fleet.is_shm() {
56 #[cfg(unix)]
57 {
58 StateBacking::Shm(Arc::new(ShmStateStore::new(
59 &ring_segment_name(fleet.name(), L::STATE_KIND),
60 )))
61 }
62 #[cfg(not(unix))]
63 unreachable!("non-Unix fleets cannot use POSIX SHM")
64 } else {
65 StateBacking::InMemory(memory_table(fleet, L::STATE_KIND))
66 };
67 Self {
68 backend,
69 state_kind: L::STATE_KIND,
70 node_id: fleet.node_id(),
71 _layout: PhantomData,
72 }
73 }
74
75 pub(crate) fn acquire(
76 &self,
77 key: &LockKey,
78 owner: &LockOwner,
79 now_ms: u64,
80 expires_at_ms: u64,
81 publish: impl FnOnce(&LockTransition) -> Result<()>,
82 ) -> Result<LockAcquire> {
83 validate_entry(key, owner)?;
84 self.with_table(|header, slots| {
85 let hash = lock_hash(key);
86 let mut insertion = None;
87 for offset in 0..slots.len() {
88 let index = probe_index(hash, offset, slots.len());
89 let slot = &mut slots[index];
90 match slot.state() {
91 SLOT_EMPTY => {
92 insertion.get_or_insert(index);
93 break;
94 }
95 SLOT_TOMBSTONE => {
96 insertion.get_or_insert(index);
97 }
98 SLOT_OCCUPIED if slot.expires_at_ms <= now_ms => {
99 slot.tombstone();
100 insertion.get_or_insert(index);
101 }
102 SLOT_OCCUPIED if slot.matches(hash, key) => {
103 return Ok(LockAcquire::Occupied(slot.lease()));
104 }
105 _ => {}
106 }
107 }
108
109 let index =
110 insertion.ok_or(Error::StateFull { capacity: slots.len() })?;
111 let lease = LockLease {
112 lock_id: mint_lock_id(header, self.state_kind, self.node_id)?,
113 key: key.clone(),
114 owner: owner.clone(),
115 acquired_at_ms: now_ms,
116 expires_at_ms,
117 state_revision: mint_revision(header)?,
118 };
119 publish(&LockTransition::Acquired(lease.clone()))?;
120 slots[index].write(hash, &lease);
121 Ok(LockAcquire::Acquired(lease))
122 })
123 }
124
125 pub(crate) fn renew_id(
126 &self,
127 lease: &LockLease,
128 now_ms: u64,
129 requested_expires_at_ms: u64,
130 publish: impl FnOnce(&LockTransition) -> Result<()>,
131 ) -> Result<Option<LockLease>> {
132 validate_entry(&lease.key, &lease.owner)?;
133 self.with_table(|header, slots| {
134 let Some(slot) = live_slot(slots, &lease.key, now_ms) else {
135 return Ok(None);
136 };
137 if slot.lock_id != lease.lock_id.raw() {
138 return Ok(None);
139 }
140 renew_slot(header, slot, requested_expires_at_ms, publish)
141 })
142 }
143
144 pub(crate) fn renew_owner(
145 &self,
146 key: &LockKey,
147 owner: &LockOwner,
148 now_ms: u64,
149 requested_expires_at_ms: u64,
150 publish: impl FnOnce(&LockTransition) -> Result<()>,
151 ) -> Result<Option<LockLease>> {
152 validate_entry(key, owner)?;
153 self.with_table(|header, slots| {
154 let Some(slot) = live_slot(slots, key, now_ms) else {
155 return Ok(None);
156 };
157 if slot.owner() != owner.as_bytes() {
158 return Ok(None);
159 }
160 renew_slot(header, slot, requested_expires_at_ms, publish)
161 })
162 }
163
164 pub(crate) fn release_id(
165 &self,
166 lease: &LockLease,
167 now_ms: u64,
168 publish: impl FnOnce(&LockTransition) -> Result<()>,
169 ) -> Result<bool> {
170 validate_entry(&lease.key, &lease.owner)?;
171 self.with_table(|header, slots| {
172 let Some(slot) = live_slot(slots, &lease.key, now_ms) else {
173 return Ok(false);
174 };
175 if slot.lock_id != lease.lock_id.raw() {
176 return Ok(false);
177 }
178 release_slot(header, slot, publish)
179 })
180 }
181
182 pub(crate) fn release_owner(
183 &self,
184 key: &LockKey,
185 owner: &LockOwner,
186 now_ms: u64,
187 publish: impl FnOnce(&LockTransition) -> Result<()>,
188 ) -> Result<bool> {
189 validate_entry(key, owner)?;
190 self.with_table(|header, slots| {
191 let Some(slot) = live_slot(slots, key, now_ms) else {
192 return Ok(false);
193 };
194 if slot.owner() != owner.as_bytes() {
195 return Ok(false);
196 }
197 release_slot(header, slot, publish)
198 })
199 }
200
201 pub(crate) fn force_release(
202 &self,
203 key: &LockKey,
204 now_ms: u64,
205 publish: impl FnOnce(&LockTransition) -> Result<()>,
206 ) -> Result<bool> {
207 validate_key(key)?;
208 self.with_table(|header, slots| {
209 let Some(index) = find_slot(slots, lock_hash(key), key) else {
210 return Ok(false);
211 };
212 let slot = &mut slots[index];
213 if slot.expires_at_ms <= now_ms {
214 slot.tombstone();
215 return Ok(false);
216 }
217 release_slot(header, slot, publish)
218 })
219 }
220
221 pub(crate) fn current(
222 &self,
223 key: &LockKey,
224 now_ms: u64,
225 ) -> Result<Option<LockLease>> {
226 validate_key(key)?;
227 self.with_table(|_header, slots| {
228 Ok(live_slot(slots, key, now_ms).map(|slot| slot.lease()))
229 })
230 }
231
232 pub(crate) fn reset(&self) -> Result<()> {
233 self.with_table(|header, slots| {
234 for slot in slots {
235 slot.clear();
236 }
237 header.next_lock.store(0, Ordering::Relaxed);
238 header.next_revision.store(0, Ordering::Relaxed);
239 Ok(())
240 })
241 }
242
243 #[cfg(unix)]
244 pub(crate) fn unlink(&self) -> Result<()> {
245 match &self.backend {
246 StateBacking::InMemory(_) => self.reset(),
247 StateBacking::Shm(store) => {
248 store.open()?.unlink().map_err(Error::Io)
249 }
250 }
251 }
252
253 fn with_table<T>(
254 &self,
255 operation: impl FnOnce(&LockStateHeader, &mut [LockStateSlot]) -> Result<T>,
256 ) -> Result<T> {
257 match &self.backend {
258 StateBacking::InMemory(table) => {
259 let mut table = lock_unpoisoned(table);
260 let MemoryTable { header, slots, .. } = &mut *table;
261 operation(header, slots)
262 }
263 #[cfg(unix)]
264 StateBacking::Shm(store) => {
265 store.open()?.with_table(operation).map_err(Error::Io)?
266 }
267 }
268 }
269}
270
271fn renew_slot(
272 header: &LockStateHeader,
273 slot: &mut LockStateSlot,
274 requested_expires_at_ms: u64,
275 publish: impl FnOnce(&LockTransition) -> Result<()>,
276) -> Result<Option<LockLease>> {
277 let mut lease = slot.lease();
278 lease.expires_at_ms = lease.expires_at_ms.max(requested_expires_at_ms);
279 lease.state_revision = mint_revision(header)?;
280 publish(&LockTransition::Renewed(lease.clone()))?;
281 let hash = slot.key_hash;
282 slot.write(hash, &lease);
283 Ok(Some(lease))
284}
285
286fn release_slot(
287 header: &LockStateHeader,
288 slot: &mut LockStateSlot,
289 publish: impl FnOnce(&LockTransition) -> Result<()>,
290) -> Result<bool> {
291 let lease = slot.lease();
292 let state_revision = mint_revision(header)?;
293 publish(&LockTransition::Released {
294 lock_id: lease.lock_id,
295 key: lease.key,
296 owner: lease.owner,
297 state_revision,
298 })?;
299 slot.tombstone();
300 Ok(true)
301}
302
303fn live_slot<'a>(
304 slots: &'a mut [LockStateSlot],
305 key: &LockKey,
306 now_ms: u64,
307) -> Option<&'a mut LockStateSlot> {
308 let hash = lock_hash(key);
309 let index = find_slot(slots, hash, key)?;
310 let slot = &mut slots[index];
311 if slot.expires_at_ms <= now_ms {
312 slot.tombstone();
313 None
314 } else {
315 Some(slot)
316 }
317}
318
319fn find_slot(
320 slots: &[LockStateSlot],
321 hash: u64,
322 key: &LockKey,
323) -> Option<usize> {
324 for offset in 0..slots.len() {
325 let index = probe_index(hash, offset, slots.len());
326 let slot = &slots[index];
327 match slot.state() {
328 SLOT_EMPTY => return None,
329 SLOT_OCCUPIED if slot.matches(hash, key) => return Some(index),
330 _ => {}
331 }
332 }
333 None
334}
335
336fn validate_key(key: &LockKey) -> Result<()> {
337 if key.namespace().is_empty() {
338 return Err(Error::NamespaceEmpty);
339 }
340 if key.label().is_empty() {
341 return Err(Error::KeyEmpty);
342 }
343 if key.namespace().len() > u16::MAX as usize
344 || key.label().len() > u16::MAX as usize
345 || key.namespace().len().saturating_add(key.label().len())
346 > LOCK_STATE_PAYLOAD_MAX
347 {
348 return Err(Error::EntryTooLarge {
349 namespace_len: key.namespace().len(),
350 key_len: key.label().len(),
351 owner_len: 0,
352 max_payload: LOCK_STATE_PAYLOAD_MAX,
353 });
354 }
355 Ok(())
356}
357
358fn validate_entry(
359 key: &LockKey,
360 owner: &LockOwner,
361) -> Result<()> {
362 validate_key(key)?;
363 if owner.as_bytes().is_empty() {
364 return Err(Error::OwnerEmpty);
365 }
366 if key.namespace().len() > u16::MAX as usize
367 || key.label().len() > u16::MAX as usize
368 || owner.as_bytes().len() > u16::MAX as usize
369 || key
370 .namespace()
371 .len()
372 .saturating_add(key.label().len())
373 .saturating_add(owner.as_bytes().len())
374 > LOCK_STATE_PAYLOAD_MAX
375 {
376 return Err(Error::EntryTooLarge {
377 namespace_len: key.namespace().len(),
378 key_len: key.label().len(),
379 owner_len: owner.as_bytes().len(),
380 max_payload: LOCK_STATE_PAYLOAD_MAX,
381 });
382 }
383 Ok(())
384}
385
386fn mint_lock_id(
387 header: &LockStateHeader,
388 state_kind: u8,
389 node_id: NodeId,
390) -> Result<NetId64> {
391 let current = header.next_lock.load(Ordering::Relaxed);
392 let counter = current.checked_add(1).ok_or(Error::IdExhausted)?;
393 if counter > LOCK_COUNTER_MAX {
394 return Err(Error::IdExhausted);
395 }
396 header.next_lock.store(counter, Ordering::Relaxed);
397 Ok(NetId64::make(state_kind, node_id.get(), counter))
398}
399
400fn mint_revision(header: &LockStateHeader) -> Result<u64> {
401 let current = header.next_revision.load(Ordering::Relaxed);
402 let revision = current.checked_add(1).ok_or(Error::IdExhausted)?;
403 header.next_revision.store(revision, Ordering::Relaxed);
404 Ok(revision)
405}
406
407fn lock_hash(key: &LockKey) -> u64 {
408 let mut hash = FNV_OFFSET_BASIS;
409 for byte in key.namespace().iter().chain(key.label()) {
410 hash ^= u64::from(*byte);
411 hash = hash.wrapping_mul(FNV_PRIME);
412 }
413 hash
414}
415
416fn probe_index(
417 hash: u64,
418 offset: usize,
419 capacity: usize,
420) -> usize {
421 debug_assert!(capacity.is_power_of_two());
422 (hash as usize).wrapping_add(offset) & (capacity - 1)
423}
424
425fn lock_unpoisoned<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
426 mutex.lock().unwrap_or_else(|error| error.into_inner())
427}
428
429struct MemoryTable {
430 _fleet: Arc<Fleet>,
433 header: LockStateHeader,
434 slots: Vec<LockStateSlot>,
435}
436
437impl MemoryTable {
438 fn new(fleet: Arc<Fleet>) -> Self {
439 Self {
440 _fleet: fleet,
441 header: LockStateHeader::new(),
442 slots: (0..LOCK_STATE_CAPACITY)
443 .map(|_| LockStateSlot::empty())
444 .collect(),
445 }
446 }
447}
448
449type MemoryRegistry = HashMap<(usize, u8), Weak<Mutex<MemoryTable>>>;
450static MEMORY_TABLES: LazyLock<Mutex<MemoryRegistry>> =
451 LazyLock::new(|| Mutex::new(HashMap::new()));
452
453fn memory_table(
454 fleet: &Arc<Fleet>,
455 state_kind: u8,
456) -> Arc<Mutex<MemoryTable>> {
457 let fleet_identity = Arc::as_ptr(fleet) as usize;
458 let mut tables = lock_unpoisoned(&MEMORY_TABLES);
459 tables.retain(|_, table| table.strong_count() > 0);
460 if let Some(table) =
461 tables.get(&(fleet_identity, state_kind)).and_then(Weak::upgrade)
462 {
463 return table;
464 }
465 let table = Arc::new(Mutex::new(MemoryTable::new(Arc::clone(fleet))));
466 tables.insert((fleet_identity, state_kind), Arc::downgrade(&table));
467 table
468}
469
470#[repr(C, align(64))]
471struct LockStateHeader {
472 #[cfg(unix)]
473 magic: u32,
474 #[cfg(not(unix))]
475 _magic: u32,
476 #[cfg(unix)]
477 version: u16,
478 #[cfg(not(unix))]
479 _version: u16,
480 header_size: u16,
481 capacity: u32,
482 slot_size: u32,
483 next_lock: AtomicU64,
484 next_revision: AtomicU64,
485 _reserved: [u8; 32],
486}
487
488impl LockStateHeader {
489 fn new() -> Self {
490 Self {
491 #[cfg(unix)]
492 magic: STATE_MAGIC,
493 #[cfg(not(unix))]
494 _magic: 0,
495 #[cfg(unix)]
496 version: STATE_VERSION,
497 #[cfg(not(unix))]
498 _version: 0,
499 header_size: std::mem::size_of::<Self>() as u16,
500 capacity: LOCK_STATE_CAPACITY as u32,
501 slot_size: std::mem::size_of::<LockStateSlot>() as u32,
502 next_lock: AtomicU64::new(0),
503 next_revision: AtomicU64::new(0),
504 _reserved: [0; 32],
505 }
506 }
507}
508
509#[repr(C, align(64))]
510struct LockStateSlot {
511 state: AtomicU8,
512 _reserved: u8,
513 namespace_len: u16,
514 key_len: u16,
515 owner_len: u16,
516 key_hash: u64,
517 lock_id: u64,
518 state_revision: u64,
519 acquired_at_ms: u64,
520 expires_at_ms: u64,
521 payload: [u8; LOCK_STATE_PAYLOAD_MAX],
522}
523
524impl LockStateSlot {
525 fn empty() -> Self {
526 Self {
527 state: AtomicU8::new(SLOT_EMPTY),
528 _reserved: 0,
529 namespace_len: 0,
530 key_len: 0,
531 owner_len: 0,
532 key_hash: 0,
533 lock_id: 0,
534 state_revision: 0,
535 acquired_at_ms: 0,
536 expires_at_ms: 0,
537 payload: [0; LOCK_STATE_PAYLOAD_MAX],
538 }
539 }
540
541 fn state(&self) -> u8 {
542 self.state.load(Ordering::Acquire)
543 }
544
545 fn namespace_bytes(&self) -> &[u8] {
546 &self.payload[..usize::from(self.namespace_len)]
547 }
548
549 fn key_bytes(&self) -> &[u8] {
550 let start = usize::from(self.namespace_len);
551 let end = start + usize::from(self.key_len);
552 &self.payload[start..end]
553 }
554
555 fn owner(&self) -> &[u8] {
556 let start = usize::from(self.namespace_len) + usize::from(self.key_len);
557 let end = start + usize::from(self.owner_len);
558 &self.payload[start..end]
559 }
560
561 fn matches(
562 &self,
563 hash: u64,
564 key: &LockKey,
565 ) -> bool {
566 self.state() == SLOT_OCCUPIED
567 && self.key_hash == hash
568 && self.namespace_bytes() == key.namespace()
569 && self.key_bytes() == key.label()
570 }
571
572 fn lease(&self) -> LockLease {
573 LockLease {
574 lock_id: NetId64::from_raw(self.lock_id),
575 key: LockKey::from_parts(
576 bytes::Bytes::copy_from_slice(self.namespace_bytes()),
577 bytes::Bytes::copy_from_slice(self.key_bytes()),
578 ),
579 owner: LockOwner::from(bytes::Bytes::copy_from_slice(self.owner())),
580 acquired_at_ms: self.acquired_at_ms,
581 expires_at_ms: self.expires_at_ms,
582 state_revision: self.state_revision,
583 }
584 }
585
586 fn write(
587 &mut self,
588 hash: u64,
589 lease: &LockLease,
590 ) {
591 self.state.store(SLOT_TOMBSTONE, Ordering::Release);
592 self.namespace_len = lease.key.namespace().len() as u16;
593 self.key_len = lease.key.label().len() as u16;
594 self.owner_len = lease.owner.as_bytes().len() as u16;
595 self.key_hash = hash;
596 self.lock_id = lease.lock_id.raw();
597 self.state_revision = lease.state_revision;
598 self.acquired_at_ms = lease.acquired_at_ms;
599 self.expires_at_ms = lease.expires_at_ms;
600 self.payload.fill(0);
601 let namespace_end = lease.key.namespace().len();
602 self.payload[..namespace_end].copy_from_slice(lease.key.namespace());
603 let key_end = namespace_end + lease.key.label().len();
604 self.payload[namespace_end..key_end].copy_from_slice(lease.key.label());
605 let owner_start = key_end;
606 let owner_end = owner_start + lease.owner.as_bytes().len();
607 self.payload[owner_start..owner_end]
608 .copy_from_slice(lease.owner.as_bytes());
609 self.state.store(SLOT_OCCUPIED, Ordering::Release);
610 }
611
612 fn tombstone(&self) {
613 self.state.store(SLOT_TOMBSTONE, Ordering::Release);
614 }
615
616 fn clear(&self) {
617 self.state.store(SLOT_EMPTY, Ordering::Release);
618 }
619}
620
621#[cfg(unix)]
622struct ShmStateStore {
623 name: String,
624 opened: Mutex<Option<Arc<ShmLockTable>>>,
625}
626
627#[cfg(unix)]
628impl ShmStateStore {
629 fn new(name: &str) -> Self {
630 Self { name: name.to_owned(), opened: Mutex::new(None) }
631 }
632
633 fn open(&self) -> Result<Arc<ShmLockTable>> {
634 let mut opened = lock_unpoisoned(&self.opened);
635 if let Some(table) = opened.as_ref() {
636 return Ok(table.clone());
637 }
638 let table = Arc::new(ShmLockTable::open_or_create(&self.name)?);
639 *opened = Some(table.clone());
640 Ok(table)
641 }
642}
643
644#[cfg(unix)]
645struct ShmLockTable {
646 region: ShmRegion,
647 local_lock: Mutex<()>,
648}
649
650#[cfg(unix)]
651impl ShmLockTable {
652 fn open_or_create(name: &str) -> Result<Self> {
653 use std::{io, ptr};
654
655 let (region, _initialization_lock) =
656 ShmRegion::open_or_create_locked(name, shm_segment_size())?;
657 if region.created() {
658 unsafe {
659 ptr::write(
660 region.as_ptr().cast::<LockStateHeader>(),
661 LockStateHeader::new(),
662 );
663 let slots =
664 region.as_ptr().add(std::mem::size_of::<LockStateHeader>());
665 ptr::write_bytes(
666 slots,
667 0,
668 LOCK_STATE_CAPACITY * std::mem::size_of::<LockStateSlot>(),
669 );
670 }
671 } else {
672 let header = unsafe { &*region.as_ptr().cast::<LockStateHeader>() };
673 if header.magic != STATE_MAGIC {
674 return Err(Error::Io(io::Error::new(
675 io::ErrorKind::InvalidData,
676 format!(
677 "SHM segment {name} has wrong magic 0x{:08X} (expected 0x{STATE_MAGIC:08X})",
678 header.magic
679 ),
680 )));
681 }
682 if header.version != STATE_VERSION
683 || usize::from(header.header_size)
684 != std::mem::size_of::<LockStateHeader>()
685 || header.capacity as usize != LOCK_STATE_CAPACITY
686 || header.slot_size as usize
687 != std::mem::size_of::<LockStateSlot>()
688 {
689 return Err(Error::Io(io::Error::new(
690 io::ErrorKind::InvalidData,
691 format!(
692 "SHM segment {name} has an incompatible lock-state layout"
693 ),
694 )));
695 }
696 }
697
698 Ok(Self { region, local_lock: Mutex::new(()) })
699 }
700
701 fn with_table<T>(
702 &self,
703 operation: impl FnOnce(&LockStateHeader, &mut [LockStateSlot]) -> Result<T>,
704 ) -> std::io::Result<Result<T>> {
705 let _local = lock_unpoisoned(&self.local_lock);
706 let _process = self.region.lock_exclusive()?;
707 let header =
708 unsafe { &*self.region.as_ptr().cast::<LockStateHeader>() };
709 let slots = unsafe {
710 std::slice::from_raw_parts_mut(
711 self.region
712 .as_ptr()
713 .add(std::mem::size_of::<LockStateHeader>())
714 .cast::<LockStateSlot>(),
715 LOCK_STATE_CAPACITY,
716 )
717 };
718 Ok(operation(header, slots))
719 }
720
721 fn unlink(&self) -> std::io::Result<()> {
722 self.region.unlink()
723 }
724}
725
726#[cfg(unix)]
727fn shm_segment_size() -> usize {
728 std::mem::size_of::<LockStateHeader>()
729 + LOCK_STATE_CAPACITY * std::mem::size_of::<LockStateSlot>()
730}
731
732const _: () = assert!(LOCK_STATE_CAPACITY.is_power_of_two());
733const _: () = assert!(LOCK_STATE_CAPACITY <= u32::MAX as usize);
734const _: () = assert!(LOCK_STATE_PAYLOAD_MAX > 0);
735const _: () = assert!(std::mem::size_of::<LockStateHeader>() == 64);
736const _: () =
737 assert!(std::mem::size_of::<LockStateSlot>() <= u32::MAX as usize);