@harperfast/hnsw 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/format.rs ADDED
@@ -0,0 +1,556 @@
1
+ //! On-disk format: 4 KB header + fixed-size layer-0 slot array + upper-layer region.
2
+ //! See ../../../hnsw-native-plane.md §4. Format changes bump VERSION and require reindex.
3
+
4
+ use memmap2::MmapMut;
5
+ use std::fs::OpenOptions;
6
+ use std::io;
7
+ use std::path::Path;
8
+ use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
9
+
10
+ pub const MAGIC: u32 = 0x484e_5357; // "HNSW"
11
+ pub const VERSION: u32 = 5; // v5: opener registry + aligned freelist pointer (older files: reindex)
12
+ pub const HEADER_SIZE: usize = 4096;
13
+
14
+ // Header field byte offsets.
15
+ const H_MAGIC: usize = 0;
16
+ const H_VERSION: usize = 4;
17
+ const H_DIMS: usize = 8; // u16
18
+ const H_QUANT: usize = 10; // u8: 0 = int8, 1 = f32
19
+ const H_LAYER0_CAP: usize = 12; // u16
20
+ const H_SLOT_SIZE: usize = 16; // u32
21
+ const H_ENTRY: usize = 24; // u64 atomic: (level << 32) | id, one word so readers never see a torn pair
22
+ const H_ID_HIGH_WATER: usize = 32; // u64 atomic
23
+ const H_FREELIST_HEAD: usize = 40; // u64 atomic: (tag << 32) | id; id u32::MAX = empty
24
+ const H_TXN_WATERMARK: usize = 48; // u64
25
+ const H_CLEAN_SHUTDOWN: usize = 56; // u8
26
+ const H_MAX_NODES: usize = 64; // u64
27
+ const H_UPPER_HIGH_WATER: usize = 72; // u64 atomic: upper-entry allocator
28
+ const H_UPPER_FREELIST: usize = 80; // u64 atomic: (tag<<32)|idx; NO_UPPER = empty
29
+ const H_ENTRY_PREV: usize = 88; // u64: last replaced entry point (re-election hint)
30
+ // Opener registry: each live handle claims one slot, writes its random tag there, and holds
31
+ // a kernel OFD byte-range lock on the slot (released automatically when the handle - or its
32
+ // whole process - dies). A lock word's owner is dead iff its registry slot no longer carries
33
+ // its tag or the slot's byte range is lockable. Immune to pid reuse and pid namespaces.
34
+ const H_REGISTRY: usize = 128; // u32 x REGISTRY_SLOTS
35
+ pub const REGISTRY_SLOTS: usize = 64;
36
+
37
+ /// Upper-layer region geometry: fixed entries covering levels 1..=MAX_UPPER_LEVELS at
38
+ /// UPPER_CAP ids per level. P(level >= 1) = 1/M ~ 6.25%; the region reserves entries for
39
+ /// 1/8 of max_nodes (2x headroom). P(level >= 9) at mL = 1/ln16 is ~e^-25 — unreachable.
40
+ pub const MAX_UPPER_LEVELS: usize = 8;
41
+ pub const UPPER_CAP: usize = 64; // matches the JS graph's upper cap (M<<2 under optimizeRouting)
42
+ // entry: seq u32 | levels u8 | pad | per-level (degree u16 + ids u32*UPPER_CAP)
43
+ pub const U_SEQ: usize = 0;
44
+ pub const U_LEVELS: usize = 4;
45
+ pub const U_LISTS: usize = 8;
46
+ pub const UPPER_LEVEL_STRIDE: usize = 2 + UPPER_CAP * 4 + 2; // degree + ids + pad -> 132
47
+ pub const NO_UPPER: u32 = u32::MAX;
48
+
49
+ // Slot layout offsets (within a slot).
50
+ pub const S_SEQ: usize = 0; // u32 seqlock
51
+ pub const S_FLAGS: usize = 4; // u8: bit0 = valid, bit1 = deleted
52
+ pub const S_LEVEL: usize = 5; // u8
53
+ pub const S_DEGREE: usize = 6; // u16
54
+ pub const S_SCALE: usize = 8; // f32
55
+ pub const S_INV_MAG: usize = 12; // f32
56
+ pub const S_UPPER_IDX: usize = 16; // u32 index into the upper region; NO_UPPER = none
57
+ pub const S_VECTOR: usize = 20; // dims bytes (int8) or dims*4 (f32)
58
+ // neighbors: u32 * layer0_cap, follows vector
59
+ // deleted slots reuse the first neighbor word as freelist next-pointer
60
+
61
+ pub const FLAG_VALID: u8 = 1;
62
+ pub const FLAG_DELETED: u8 = 2;
63
+ pub const NO_ID: u32 = u32::MAX;
64
+
65
+ pub struct PlaneFile {
66
+ /// Kept open for the lifetime of the mapping: the opener-registry OFD lock lives on it.
67
+ file: std::fs::File,
68
+ /// This handle's registry tag (low bits encode its registry slot). 0 = unregistered
69
+ /// (registry full or platform without OFD locks): this handle's own dead locks cannot be
70
+ /// reclaimed by others, and it never reclaims.
71
+ pub self_tag: u32,
72
+ pub map: MmapMut,
73
+ pub dims: usize,
74
+ pub layer0_cap: usize,
75
+ pub slot_size: usize,
76
+ pub max_nodes: u64,
77
+ upper_offset: usize,
78
+ pub upper_capacity: u64,
79
+ /// Whether the file recorded a clean shutdown when opened (create() reports true).
80
+ /// An unclean open has had its torn seqlocks scrubbed, but individual slots may hold
81
+ /// unflushed/partial states — hosts should rebuild rather than trust completeness.
82
+ pub opened_clean: bool,
83
+ /// Slots per 4 KB page under page-grouped addressing; 0 = packed (slots may straddle
84
+ /// pages). Grouped is chosen at create when the per-page waste is small (e.g. 1,344 B
85
+ /// slots: 3/page, 64 B waste). Straddling only costs on cold faults, but the layout is
86
+ /// header-pinned so it must be decided before any data exists.
87
+ pub slots_per_page: usize,
88
+ }
89
+
90
+ const PAGE: usize = 4096;
91
+ const H_SLOTS_PER_PAGE: usize = 20; // u16
92
+
93
+ fn slot_size_for(dims: usize, layer0_cap: usize) -> usize {
94
+ let raw = S_VECTOR + dims + layer0_cap * 4;
95
+ raw.next_multiple_of(64) // cache-line align
96
+ }
97
+
98
+ fn upper_entry_size() -> usize {
99
+ (U_LISTS + MAX_UPPER_LEVELS * UPPER_LEVEL_STRIDE).next_multiple_of(64)
100
+ }
101
+
102
+ fn slot_region_len(max_nodes: u64, slot_size: usize, slots_per_page: usize) -> u64 {
103
+ if slots_per_page > 0 {
104
+ max_nodes.div_ceil(slots_per_page as u64) * PAGE as u64
105
+ } else {
106
+ max_nodes * slot_size as u64
107
+ }
108
+ }
109
+
110
+ fn slots_per_page_for(slot_size: usize) -> usize {
111
+ if slot_size > PAGE {
112
+ return 0;
113
+ }
114
+ let per = PAGE / slot_size;
115
+ let waste = PAGE - per * slot_size;
116
+ // group when waste is under ~3% of the page; otherwise pack
117
+ if waste <= 128 { per } else { 0 }
118
+ }
119
+
120
+ impl PlaneFile {
121
+ /// Create a new plane file with capacity for `max_nodes` (sparse; pages materialize on write).
122
+ pub fn create(path: &Path, dims: usize, layer0_cap: usize, max_nodes: u64) -> io::Result<Self> {
123
+ if max_nodes >= NO_ID as u64 {
124
+ return Err(io::Error::new(io::ErrorKind::InvalidInput, "maxNodes must be below 2^32-1"));
125
+ }
126
+ let slot_size = slot_size_for(dims, layer0_cap);
127
+ let slots_per_page = slots_per_page_for(slot_size);
128
+ let data_len = slot_region_len(max_nodes, slot_size, slots_per_page);
129
+ let upper_capacity = max_nodes / 8 + 64;
130
+ let len = HEADER_SIZE as u64 + data_len + upper_capacity * upper_entry_size() as u64;
131
+ let file = OpenOptions::new().read(true).write(true).create(true).truncate(true).open(path)?;
132
+ file.set_len(len)?;
133
+ let mut map = unsafe { MmapMut::map_mut(&file)? };
134
+ // geometry and allocator state first; MAGIC+VERSION last, so a concurrent opener
135
+ // in the create window sees an invalid header (retryable) rather than adopting a
136
+ // half-initialized plane with max_nodes = 0
137
+ map[H_DIMS..H_DIMS + 2].copy_from_slice(&(dims as u16).to_le_bytes());
138
+ map[H_QUANT] = 0;
139
+ map[H_LAYER0_CAP..H_LAYER0_CAP + 2].copy_from_slice(&(layer0_cap as u16).to_le_bytes());
140
+ map[H_SLOT_SIZE..H_SLOT_SIZE + 4].copy_from_slice(&(slot_size as u32).to_le_bytes());
141
+ map[H_SLOTS_PER_PAGE..H_SLOTS_PER_PAGE + 2].copy_from_slice(&(slots_per_page as u16).to_le_bytes());
142
+ map[H_ENTRY..H_ENTRY + 8].copy_from_slice(&(NO_ID as u64).to_le_bytes());
143
+ map[H_FREELIST_HEAD..H_FREELIST_HEAD + 8]
144
+ .copy_from_slice(&((NO_ID as u64) | 0u64 << 32).to_le_bytes());
145
+ map[H_MAX_NODES..H_MAX_NODES + 8].copy_from_slice(&max_nodes.to_le_bytes());
146
+ map[H_UPPER_FREELIST..H_UPPER_FREELIST + 8].copy_from_slice(&(NO_UPPER as u64).to_le_bytes());
147
+ map[H_VERSION..H_VERSION + 4].copy_from_slice(&VERSION.to_le_bytes());
148
+ std::sync::atomic::fence(Ordering::Release);
149
+ map[H_MAGIC..H_MAGIC + 4].copy_from_slice(&MAGIC.to_le_bytes());
150
+ let upper_offset = HEADER_SIZE + slot_region_len(max_nodes, slot_size, slots_per_page) as usize;
151
+ let mut plane = PlaneFile {
152
+ file,
153
+ self_tag: 0,
154
+ map,
155
+ dims,
156
+ layer0_cap,
157
+ slot_size,
158
+ max_nodes,
159
+ upper_offset,
160
+ upper_capacity,
161
+ slots_per_page,
162
+ opened_clean: true,
163
+ };
164
+ plane.register_opener();
165
+ Ok(plane)
166
+ }
167
+
168
+ pub fn open(path: &Path) -> io::Result<Self> {
169
+ let file = OpenOptions::new().read(true).write(true).open(path)?;
170
+ let file_len = file.metadata()?.len();
171
+ if file_len < HEADER_SIZE as u64 {
172
+ // a truncated or interrupted create must be a catchable error, not a slice panic
173
+ return Err(io::Error::new(io::ErrorKind::InvalidData, "plane file shorter than its header: recreate the index"));
174
+ }
175
+ let map = unsafe { MmapMut::map_mut(&file)? };
176
+ let magic = u32::from_le_bytes(map[H_MAGIC..H_MAGIC + 4].try_into().unwrap());
177
+ let version = u32::from_le_bytes(map[H_VERSION..H_VERSION + 4].try_into().unwrap());
178
+ if magic != MAGIC || version != VERSION {
179
+ return Err(io::Error::new(io::ErrorKind::InvalidData, "format mismatch: reindex required"));
180
+ }
181
+ let dims = u16::from_le_bytes(map[H_DIMS..H_DIMS + 2].try_into().unwrap()) as usize;
182
+ let layer0_cap = u16::from_le_bytes(map[H_LAYER0_CAP..H_LAYER0_CAP + 2].try_into().unwrap()) as usize;
183
+ let slot_size = u32::from_le_bytes(map[H_SLOT_SIZE..H_SLOT_SIZE + 4].try_into().unwrap()) as usize;
184
+ let slots_per_page = u16::from_le_bytes(map[H_SLOTS_PER_PAGE..H_SLOTS_PER_PAGE + 2].try_into().unwrap()) as usize;
185
+ let max_nodes = u64::from_le_bytes(map[H_MAX_NODES..H_MAX_NODES + 8].try_into().unwrap());
186
+ if dims == 0 || slot_size == 0 || slot_size != slot_size_for(dims, layer0_cap) {
187
+ return Err(io::Error::new(io::ErrorKind::InvalidData, "plane header geometry is inconsistent: recreate the index"));
188
+ }
189
+ if max_nodes > NO_ID as u64 || slots_per_page != slots_per_page_for(slot_size) {
190
+ return Err(io::Error::new(io::ErrorKind::InvalidData, "plane header geometry is inconsistent: recreate the index"));
191
+ }
192
+ let upper_offset = HEADER_SIZE + slot_region_len(max_nodes, slot_size, slots_per_page) as usize;
193
+ let upper_capacity = max_nodes / 8 + 64;
194
+ let expected = (upper_offset as u64)
195
+ .checked_add(upper_capacity.checked_mul(upper_entry_size() as u64).ok_or_else(|| {
196
+ io::Error::new(io::ErrorKind::InvalidData, "plane header geometry overflows: recreate the index")
197
+ })?)
198
+ .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "plane header geometry overflows: recreate the index"))?;
199
+ if file_len < expected {
200
+ // header-valid but short (rsync/backup truncation): mid-range slot_ptr/upper_ptr
201
+ // would otherwise read off the mapping
202
+ return Err(io::Error::new(
203
+ io::ErrorKind::InvalidData,
204
+ format!("plane file is {file_len} bytes but its header implies {expected}: recreate the index"),
205
+ ));
206
+ }
207
+ let opened_clean = map[H_CLEAN_SHUTDOWN] == 1;
208
+ let mut plane = PlaneFile {
209
+ file,
210
+ self_tag: 0,
211
+ map,
212
+ dims,
213
+ layer0_cap,
214
+ slot_size,
215
+ max_nodes,
216
+ upper_offset,
217
+ upper_capacity,
218
+ slots_per_page,
219
+ opened_clean,
220
+ };
221
+ plane.register_opener();
222
+ let hw = plane.id_high_water();
223
+ if hw > max_nodes {
224
+ return Err(io::Error::new(io::ErrorKind::InvalidData, "plane header id high-water exceeds capacity: recreate the index"));
225
+ }
226
+ // No open-time repair: seqlocks persisted odd by a dead writer are taken over lazily
227
+ // at the contended slot (seqlock.rs) — a whole-file scrub would page in the entire
228
+ // mapping and, with another process still mapping the file, could force a LIVE
229
+ // writer's lock. The clean-shutdown byte remains advisory metadata only.
230
+ Ok(plane)
231
+ }
232
+
233
+ /// Force any persisted-odd seqlocks (slot + upper regions) back to even after an unclean
234
+ /// shutdown. Safe because open() runs before any concurrent access exists.
235
+ #[inline]
236
+ pub fn slot_ptr(&self, id: u32) -> *const u8 {
237
+ let off = if self.slots_per_page > 0 {
238
+ (id as usize / self.slots_per_page) * PAGE + (id as usize % self.slots_per_page) * self.slot_size
239
+ } else {
240
+ id as usize * self.slot_size
241
+ };
242
+ unsafe { self.map.as_ptr().add(HEADER_SIZE + off) }
243
+ }
244
+
245
+ #[inline]
246
+ pub fn slot_ptr_mut(&self, id: u32) -> *mut u8 {
247
+ // Mutation through a shared map: all mutable slot access is mediated by the seqlock
248
+ // (seqlock.rs) and atomics; the mmap itself is plain memory.
249
+ self.slot_ptr(id) as *mut u8
250
+ }
251
+
252
+ #[inline]
253
+ fn header_atomic_u64(&self, offset: usize) -> &AtomicU64 {
254
+ unsafe { &*(self.map.as_ptr().add(offset) as *const AtomicU64) }
255
+ }
256
+
257
+ #[inline]
258
+ pub fn seq_atomic(&self, id: u32) -> &AtomicU32 {
259
+ unsafe { &*(self.slot_ptr(id).add(S_SEQ) as *const AtomicU32) }
260
+ }
261
+
262
+ /// Allocate a node id: pop the freelist, else bump the high-water. Returns NO_ID when
263
+ /// the plane is full (max_nodes reached) — an unchecked bump would address into the
264
+ /// upper-layer region and, past that, off the mapping.
265
+ pub fn allocate_id(&self) -> u32 {
266
+ let head = self.header_atomic_u64(H_FREELIST_HEAD);
267
+ loop {
268
+ let cur = head.load(Ordering::Acquire);
269
+ let id = (cur & 0xffff_ffff) as u32;
270
+ if id == NO_ID {
271
+ let hw = self.header_atomic_u64(H_ID_HIGH_WATER);
272
+ let new = hw.fetch_add(1, Ordering::AcqRel);
273
+ if new >= self.max_nodes {
274
+ hw.fetch_sub(1, Ordering::AcqRel);
275
+ return NO_ID;
276
+ }
277
+ return new as u32;
278
+ }
279
+ if (id as u64) >= self.max_nodes {
280
+ // corrupt freelist head (file-sourced): drop the chain rather than compute
281
+ // out-of-mapping pointers; capacity continues via the high-water
282
+ let _ = head.compare_exchange(cur, NO_ID as u64, Ordering::AcqRel, Ordering::Acquire);
283
+ continue;
284
+ }
285
+ // next-pointer lives in the dead slot's scale field: offset 8, aligned for any
286
+ // dims (the first neighbor word at S_VECTOR+dims is 4-aligned only when dims%4==0)
287
+ let next = unsafe { (*(self.slot_ptr(id).add(S_SCALE) as *const AtomicU32)).load(Ordering::Acquire) };
288
+ let tag = (cur >> 32).wrapping_add(1);
289
+ let new = (next as u64) | (tag << 32);
290
+ if head.compare_exchange(cur, new, Ordering::AcqRel, Ordering::Acquire).is_ok() {
291
+ return id;
292
+ }
293
+ }
294
+ }
295
+
296
+ /// Return a deleted node's id to the freelist. Caller must have already marked the slot
297
+ /// deleted (under its seqlock) so concurrent traversals skip it.
298
+ pub fn free_id(&self, id: u32) {
299
+ let head = self.header_atomic_u64(H_FREELIST_HEAD);
300
+ let next_word = unsafe { &*(self.slot_ptr(id).add(S_SCALE) as *const AtomicU32) };
301
+ loop {
302
+ let cur = head.load(Ordering::Acquire);
303
+ next_word.store((cur & 0xffff_ffff) as u32, Ordering::Release);
304
+ let tag = (cur >> 32).wrapping_add(1);
305
+ let new = (id as u64) | (tag << 32);
306
+ if head.compare_exchange(cur, new, Ordering::AcqRel, Ordering::Acquire).is_ok() {
307
+ return;
308
+ }
309
+ }
310
+ }
311
+
312
+ /// Raise the high-water to at least `id + 1` (dual-write mode: ids are allocated by the
313
+ /// host's existing allocator and mirrored in; the plane allocator is bypassed).
314
+ pub fn ensure_high_water(&self, id: u32) {
315
+ let hw = self.header_atomic_u64(H_ID_HIGH_WATER);
316
+ let want = id as u64 + 1;
317
+ let mut cur = hw.load(Ordering::Acquire);
318
+ while cur < want {
319
+ match hw.compare_exchange_weak(cur, want, Ordering::AcqRel, Ordering::Acquire) {
320
+ Ok(_) => break,
321
+ Err(now) => cur = now,
322
+ }
323
+ }
324
+ }
325
+
326
+ pub fn id_high_water(&self) -> u64 {
327
+ self.header_atomic_u64(H_ID_HIGH_WATER).load(Ordering::Acquire)
328
+ }
329
+
330
+ /// Entry point (id, level), read as one atomic word — a torn (new id, old level) pair
331
+ /// would blind a racing search.
332
+ pub fn entry_point(&self) -> (u32, u32) {
333
+ let packed = self.header_atomic_u64(H_ENTRY).load(Ordering::Acquire);
334
+ ((packed & 0xffff_ffff) as u32, (packed >> 32) as u32)
335
+ }
336
+
337
+ pub fn set_entry_point(&self, id: u32, level: u32) {
338
+ let prev = self.header_atomic_u64(H_ENTRY).swap((id as u64) | ((level as u64) << 32), Ordering::AcqRel);
339
+ if (prev & 0xffff_ffff) as u32 != NO_ID && (prev & 0xffff_ffff) as u32 != id {
340
+ self.header_atomic_u64(H_ENTRY_PREV).store(prev, Ordering::Release);
341
+ }
342
+ }
343
+
344
+ /// Entry-point CAS for re-election: install (id, level) only while the current entry is
345
+ /// still `expected_id` or is of a lower level — a concurrent insert that just promoted a
346
+ /// higher-level entry must not be clobbered by a delete's level-0 survivor.
347
+ pub fn set_entry_point_if_not_better(&self, id: u32, level: u32, expected_id: u32) {
348
+ let cell = self.header_atomic_u64(H_ENTRY);
349
+ let new = (id as u64) | ((level as u64) << 32);
350
+ let mut cur = cell.load(Ordering::Acquire);
351
+ loop {
352
+ let cur_id = (cur & 0xffff_ffff) as u32;
353
+ let cur_level = (cur >> 32) as u32;
354
+ if cur_id != expected_id && cur_id != NO_ID && cur_level > level {
355
+ return; // someone installed a better entry meanwhile
356
+ }
357
+ match cell.compare_exchange(cur, new, Ordering::AcqRel, Ordering::Acquire) {
358
+ Ok(_) => return,
359
+ Err(now) => cur = now,
360
+ }
361
+ }
362
+ }
363
+
364
+ pub fn set_watermark(&self, txn: u64) {
365
+ self.header_atomic_u64(H_TXN_WATERMARK).store(txn, Ordering::Release);
366
+ }
367
+
368
+ pub fn watermark(&self) -> u64 {
369
+ self.header_atomic_u64(H_TXN_WATERMARK).load(Ordering::Acquire)
370
+ }
371
+
372
+ #[inline]
373
+ pub fn upper_ptr(&self, idx: u32) -> *const u8 {
374
+ debug_assert!((idx as u64) < self.upper_capacity);
375
+ unsafe { self.map.as_ptr().add(self.upper_offset + idx as usize * upper_entry_size()) }
376
+ }
377
+
378
+ #[inline]
379
+ pub fn upper_ptr_mut(&self, idx: u32) -> *mut u8 {
380
+ self.upper_ptr(idx) as *mut u8
381
+ }
382
+
383
+ #[inline]
384
+ pub fn upper_seq_atomic(&self, idx: u32) -> &AtomicU32 {
385
+ unsafe { &*(self.upper_ptr(idx).add(U_SEQ) as *const AtomicU32) }
386
+ }
387
+
388
+ /// Allocate an upper-region entry: pop the upper freelist, else bump the high-water.
389
+ /// Returns NO_UPPER when exhausted — the node then simply has no upper links, which
390
+ /// degrades routing, not correctness. A dead entry's next-pointer lives in its first
391
+ /// list bytes (offset U_LISTS), clobbered on reuse by the full rewrite.
392
+ pub fn allocate_upper(&self) -> u32 {
393
+ let head = self.header_atomic_u64(H_UPPER_FREELIST);
394
+ loop {
395
+ let cur = head.load(Ordering::Acquire);
396
+ let idx = (cur & 0xffff_ffff) as u32;
397
+ if idx != NO_UPPER && (idx as u64) >= self.upper_capacity {
398
+ // corrupt upper freelist head (file-sourced): drop the chain
399
+ let _ = head.compare_exchange(cur, NO_UPPER as u64, Ordering::AcqRel, Ordering::Acquire);
400
+ continue;
401
+ }
402
+ if idx == NO_UPPER {
403
+ let hw = self.header_atomic_u64(H_UPPER_HIGH_WATER);
404
+ let new = hw.fetch_add(1, Ordering::AcqRel);
405
+ if new >= self.upper_capacity {
406
+ hw.fetch_sub(1, Ordering::AcqRel);
407
+ return NO_UPPER;
408
+ }
409
+ return new as u32;
410
+ }
411
+ let next = unsafe { (*(self.upper_ptr(idx).add(U_LISTS) as *const AtomicU32)).load(Ordering::Acquire) };
412
+ let tag = (cur >> 32).wrapping_add(1);
413
+ if head
414
+ .compare_exchange(cur, (next as u64) | (tag << 32), Ordering::AcqRel, Ordering::Acquire)
415
+ .is_ok()
416
+ {
417
+ return idx;
418
+ }
419
+ }
420
+ }
421
+
422
+ /// Return a dead upper entry to the freelist. Caller must have unlinked it from its
423
+ /// node's slot (or marked the node deleted) first.
424
+ pub fn free_upper(&self, idx: u32) {
425
+ if idx == NO_UPPER || (idx as u64) >= self.upper_capacity {
426
+ return;
427
+ }
428
+ let head = self.header_atomic_u64(H_UPPER_FREELIST);
429
+ let next_word = unsafe { &*(self.upper_ptr(idx).add(U_LISTS) as *const AtomicU32) };
430
+ loop {
431
+ let cur = head.load(Ordering::Acquire);
432
+ next_word.store((cur & 0xffff_ffff) as u32, Ordering::Release);
433
+ let tag = (cur >> 32).wrapping_add(1);
434
+ if head
435
+ .compare_exchange(cur, (idx as u64) | (tag << 32), Ordering::AcqRel, Ordering::Acquire)
436
+ .is_ok()
437
+ {
438
+ return;
439
+ }
440
+ }
441
+ }
442
+
443
+ #[inline]
444
+ fn registry_tag_cell(&self, slot: usize) -> &AtomicU32 {
445
+ unsafe { &*(self.map.as_ptr().add(H_REGISTRY + slot * 4) as *const AtomicU32) }
446
+ }
447
+
448
+ /// Claim a registry slot for this handle: take the slot's kernel byte-range lock (held
449
+ /// until this handle closes; released by the kernel if the process dies) and publish a
450
+ /// random tag whose low bits name the slot. On platforms without OFD locks, or with the
451
+ /// registry full, the handle stays unregistered (tag 0): it still works, but its own
452
+ /// abandoned locks are unreclaimable and it never reclaims others'.
453
+ fn register_opener(&mut self) {
454
+ #[cfg(target_os = "linux")]
455
+ for slot in 0..REGISTRY_SLOTS {
456
+ if !self.try_lock_registry_slot(slot, false) {
457
+ continue;
458
+ }
459
+ let nanos = std::time::SystemTime::now()
460
+ .duration_since(std::time::UNIX_EPOCH)
461
+ .map(|d| d.subsec_nanos())
462
+ .unwrap_or(0);
463
+ // identity = slot (low 6 bits) + a random per-open epoch (12 bits, nonzero):
464
+ // a dead lock value pointing at a since-re-occupied slot is recognized as dead
465
+ // by the epoch mismatch — the container same-pid restart lands exactly here
466
+ let mut epoch = (nanos ^ std::process::id().rotate_left(16) ^ (self as *const _ as u32)) & 0xfff;
467
+ if epoch == 0 {
468
+ epoch = 1;
469
+ }
470
+ let tag = (epoch << 6) | slot as u32;
471
+ self.registry_tag_cell(slot).store(tag, Ordering::Release);
472
+ self.self_tag = tag;
473
+ return;
474
+ }
475
+ }
476
+
477
+ /// Try to take the OFD write lock on a registry slot's byte range. `probe` releases it
478
+ /// immediately (liveness check); otherwise it is held for this handle's lifetime.
479
+ #[cfg(target_os = "linux")]
480
+ fn try_lock_registry_slot(&self, slot: usize, probe: bool) -> bool {
481
+ use std::os::unix::io::AsRawFd;
482
+ let mut fl: libc::flock = unsafe { std::mem::zeroed() };
483
+ fl.l_type = libc::F_WRLCK as libc::c_short;
484
+ fl.l_whence = libc::SEEK_SET as libc::c_short;
485
+ fl.l_start = (H_REGISTRY + slot * 4) as libc::off_t;
486
+ fl.l_len = 4;
487
+ let got = unsafe { libc::fcntl(self.file.as_raw_fd(), libc::F_OFD_SETLK, &fl) } == 0;
488
+ if got && probe {
489
+ fl.l_type = libc::F_UNLCK as libc::c_short;
490
+ unsafe { libc::fcntl(self.file.as_raw_fd(), libc::F_OFD_SETLK, &fl) };
491
+ }
492
+ got
493
+ }
494
+
495
+ /// Whether the handle behind a lock value is gone. Lock values carry a per-acquisition
496
+ /// salt in their upper bits, so ownership is keyed on the registry SLOT (low bits): the
497
+ /// owner is dead only with positive evidence — no registration in the slot, or the
498
+ /// slot's kernel lock acquirable (its holder's open handle closed; process death
499
+ /// included). Our own slot is always alive (probing our own OFD lock would succeed and
500
+ /// lie). A dead value pointing at a slot since re-occupied by a NEW live handle reads
501
+ /// alive; the bounded writer wedge covers that rare mis-attribution.
502
+ pub fn tag_is_dead(&self, lock_value: u32) -> bool {
503
+ let identity = lock_value & crate::seqlock::TAG_MASK;
504
+ if identity == 0 {
505
+ return false; // unregistered owner: unknowable
506
+ }
507
+ if self.self_tag != 0 && identity == self.self_tag {
508
+ // ourselves: probing our own OFD lock from the same description would succeed
509
+ // and lie, so self is answered structurally
510
+ return false;
511
+ }
512
+ let slot = (identity as usize) & (REGISTRY_SLOTS - 1);
513
+ let registered = self.registry_tag_cell(slot).load(Ordering::Acquire);
514
+ if registered == 0 || registered != identity {
515
+ return true; // slot empty, or re-occupied by a different epoch: owner departed
516
+ }
517
+ #[cfg(target_os = "linux")]
518
+ {
519
+ self.try_lock_registry_slot(slot, true)
520
+ }
521
+ #[cfg(not(target_os = "linux"))]
522
+ {
523
+ false
524
+ }
525
+ }
526
+
527
+ /// The re-election hint: the entry point most recently replaced by a promotion.
528
+ pub fn previous_entry_point(&self) -> u32 {
529
+ (self.header_atomic_u64(H_ENTRY_PREV).load(Ordering::Acquire) & 0xffff_ffff) as u32
530
+ }
531
+
532
+ pub fn set_clean_shutdown(&mut self, clean: bool) {
533
+ self.map[H_CLEAN_SHUTDOWN] = clean as u8;
534
+ }
535
+
536
+ pub fn msync(&self) -> io::Result<()> {
537
+ self.map.flush()
538
+ }
539
+
540
+ /// Durability barrier with watermark ordering: flush all data, then advance the
541
+ /// watermark and mark the shutdown clean, then flush the header page alone. A crash
542
+ /// between the two flushes leaves the OLD watermark over fully-durable data — replay
543
+ /// re-covers a suffix, which is idempotent — never a new watermark over missing data.
544
+ /// (A single whole-map msync cannot express "data before watermark": the kernel may
545
+ /// write the header page back first.)
546
+ pub fn flush_with_watermark(&self, txn: Option<u64>) -> io::Result<()> {
547
+ self.map.flush()?;
548
+ if let Some(txn) = txn {
549
+ // None must not TOUCH the watermark: a cadence barrier reading-then-rewriting it
550
+ // on a pool thread could write a stale value over a completion stamp
551
+ self.set_watermark(txn);
552
+ }
553
+ unsafe { *(self.map.as_ptr().add(H_CLEAN_SHUTDOWN) as *mut u8) = 1 };
554
+ self.map.flush_range(0, HEADER_SIZE)
555
+ }
556
+ }