Skip to main content

orbit_lock/
state.rs

1//! Fixed-capacity authoritative lock state.
2
3use 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; // "OLST"
35#[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    /// Held so the fleet's address, which keys the registry, cannot be
431    /// reused by another fleet while this table lives.
432    _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);