@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/Cargo.lock +229 -0
- package/Cargo.toml +30 -0
- package/DESIGN.md +319 -0
- package/LICENSE +202 -0
- package/README.md +88 -0
- package/build.mjs +46 -0
- package/build.rs +6 -0
- package/index.d.ts +104 -0
- package/index.js +57 -0
- package/package.json +44 -0
- package/prebuilds/darwin-arm64/hnsw-plane.node +0 -0
- package/prebuilds/linux-arm64/hnsw-plane.node +0 -0
- package/prebuilds/linux-x64/hnsw-plane.node +0 -0
- package/prebuilds/win32-x64/hnsw-plane.node +0 -0
- package/src/bin/bench.rs +246 -0
- package/src/distance.rs +138 -0
- package/src/format.rs +556 -0
- package/src/graph.rs +650 -0
- package/src/insert.rs +292 -0
- package/src/lib.rs +15 -0
- package/src/napi.rs +486 -0
- package/src/search.rs +477 -0
- package/src/seqlock.rs +217 -0
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
|
+
}
|