beamdb 0.17.0

BEAM — distributed graph database syncing over WebSocket, WebRTC, and multicast. Successor to rod.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
//! Thread-safe bump allocator implementing `allocator-api2`'s `Allocator` trait.
//!
//! ## Why not `bumpalo`?
//!
//! [`bumpalo::Bump`] is `!Send + !Sync` — bump allocation is inherently
//! single-threaded because the bump pointer is a plain `Cell<usize>`. BEAM's
//! tokio multi-threaded runtime sends `Arc<Put>` between worker threads via
//! channels, which requires `Put: Send`. If the `BTreeMap` inside `Put` uses
//! a `!Send` allocator, the entire `Put` becomes `!Send` and cannot cross
//! thread boundaries.
//!
//! ## Design
//!
//! `SyncBumpArena` wraps a `Mutex<SyncBumpInner>` behind an `Arc`. The mutex
//! is only contended during **allocation** (construction of BTreeMap nodes).
//! Read-only access to the BTreeMap (lookups, iteration, serialization) does
//! not touch the allocator at all — the `Allocator` trait's `allocate` and
//! `deallocate` methods are only called during insertion and drop.
//!
//! The inner state holds a chunk list (`Vec<Chunk>`) and a cursor pointing
//! into the current chunk. When the current chunk is exhausted, a new chunk
//! is allocated from the global allocator (doubling in size, starting at
//! 4 KiB). `deallocate` is a no-op — all memory is freed at once when the
//! last `Arc` reference drops.
//!
//! ## Performance
//!
//! - **Allocation**: `Mutex::lock` + pointer bump. The lock is held for
//!   nanoseconds (just advancing a cursor). In practice, contention is rare
//!   because BTreeMap construction happens in a single actor's context.
//! - **Drop**: O(chunks) — free each chunk. O(1) relative to the number of
//!   entries in the BTreeMap. This is the primary win: std's BTreeMap drop
//!   walks every node; ours just frees a few chunks.
//! - **Clone**: `Arc::clone` — one atomic increment.
//!
//! ## Safety
//!
//! The `unsafe impl Allocator` delegates to `SyncBumpInner::allocate`,
//! which returns valid, aligned memory from a chunk. The memory remains
//! valid until the `Arc<SyncBumpInner>` is dropped (i.e., until all clones
//! of the arena are gone). `deallocate` is a no-op, which is sound for bump
//! allocators.

use allocator_api2::alloc::{AllocError, Allocator, Global, Layout};
use std::ptr::NonNull;
use std::sync::{Arc, Mutex};

// ───────────────────────────────────────────────────────────────────────
// Chunk — a single contiguous block of memory
// ───────────────────────────────────────────────────────────────────────

/// A contiguous block of memory used by the bump allocator.
///
/// Memory is allocated from the global allocator and freed on drop.
struct Chunk {
    /// The backing memory, allocated via `Global`.
    /// Stored as `Vec<u8>` for automatic drop — when the `Chunk` is dropped,
    /// the `Vec` returns its memory to the global allocator.
    data: Vec<u8>,
}

impl Chunk {
    /// Allocates a new chunk of the given size.
    fn new(size: usize) -> Self {
        // Use `Layout` for proper alignment on the Vec's backing allocation.
        // We align to 16 to cover most BTreeMap node types.
        let layout = Layout::from_size_align(size, 16).expect("invalid layout");
        let ptr = Global.allocate(layout).expect("global alloc failed").cast();
        // SAFETY: we just allocated `size` bytes with alignment 16.
        let data = unsafe { Vec::from_raw_parts(ptr.as_ptr(), 0, size) };
        Self { data }
    }

    /// Returns the usable capacity of this chunk.
    #[inline]
    fn capacity(&self) -> usize {
        self.data.capacity()
    }

    /// Returns a raw pointer to the start of the chunk's unused region.
    #[inline]
    fn start(&self) -> *const u8 {
        self.data.as_ptr()
    }
}

// ───────────────────────────────────────────────────────────────────────
// SyncBumpInner — the actual bump allocator state (behind Mutex)
// ───────────────────────────────────────────────────────────────────────

/// Internal state of the bump allocator, protected by a `Mutex`.
struct SyncBumpInner {
    /// Chunks of backing memory. The last chunk is the "current" one being
    /// bumped. Previous chunks are full.
    chunks: Vec<Chunk>,
    /// Offset (in bytes) into the current chunk where the next allocation
    /// will start.
    cursor: usize,
    /// The capacity of the current (last) chunk.
    current_cap: usize,
}

impl SyncBumpInner {
    /// Creates a new empty inner state.
    fn new() -> Self {
        Self {
            chunks: Vec::new(),
            cursor: 0,
            current_cap: 0,
        }
    }

    /// Creates a new inner state with an initial chunk of the given size.
    fn with_capacity(cap: usize) -> Self {
        let chunk = Chunk::new(cap);
        let cap = chunk.capacity();
        Self {
            chunks: vec![chunk],
            cursor: 0,
            current_cap: cap,
        }
    }

    /// Allocates `layout.size()` bytes with `layout.align()` alignment from
    /// the bump arena. Returns a `NonNull<[u8]>` slice.
    ///
    /// If the current chunk doesn't have enough space, a new chunk is
    /// allocated (doubling in size, minimum 4 KiB).
    fn allocate(&mut self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
        let size = layout.size();
        let align = layout.align();

        // Handle zero-sized allocations — return a dangling but aligned pointer.
        if size == 0 {
            return Ok(NonNull::slice_from_raw_parts(NonNull::dangling(), 0));
        }

        // Try the current chunk first.
        if let Some(ptr) = self.try_alloc_in_current(align, size) {
            return Ok(NonNull::slice_from_raw_parts(
                NonNull::new(ptr).expect("non-null from valid chunk"),
                size,
            ));
        }

        // Current chunk is full — grow.
        self.grow(align, size);

        // Try again with the new chunk.
        let ptr = self
            .try_alloc_in_current(align, size)
            .expect("freshly grown chunk must have room");
        Ok(NonNull::slice_from_raw_parts(
            NonNull::new(ptr).expect("non-null from valid chunk"),
            size,
        ))
    }

    /// Attempts to allocate from the current chunk, aligning `cursor` to
    /// `align` and checking that `size` bytes fit. Returns `Some(ptr)` on
    /// success, `None` if the chunk is full.
    #[inline]
    fn try_alloc_in_current(&mut self, align: usize, size: usize) -> Option<*mut u8> {
        let chunk = self.chunks.last()?;
        let base = chunk.start() as usize;
        let offset = self.cursor;

        // Align the cursor upward.
        let aligned_offset = (base + offset + align - 1) & !(align - 1);
        let padding = aligned_offset - base - offset;
        let new_cursor = offset + padding + size;

        if new_cursor > self.current_cap {
            return None;
        }

        self.cursor = new_cursor;
        Some(aligned_offset as *mut u8)
    }

    /// Allocates a new chunk large enough for the requested allocation.
    /// Chunk size doubles each time, starting at 4 KiB.
    fn grow(&mut self, _align: usize, size: usize) {
        // Minimum chunk size is 4 KiB. Double from the last chunk, but ensure
        // we have at least `size` bytes available.
        let next_size = (self.current_cap * 2).max(4096).max(size);

        let chunk = Chunk::new(next_size);
        self.current_cap = chunk.capacity();
        self.cursor = 0;
        self.chunks.push(chunk);
    }
}

// ───────────────────────────────────────────────────────────────────────
// SyncBumpArena — the public Allocator impl (Arc<Mutex<SyncBumpInner>>)
// ───────────────────────────────────────────────────────────────────────

/// A thread-safe bump arena allocator implementing `allocator-api2`'s
/// `Allocator` trait.
///
/// Holds an `Arc<Mutex<SyncBumpInner>>` so it is `Clone + Send + Sync + 'static`.
/// All clones share the same arena — when the last clone drops, all memory is
/// freed at once (the chunks' `Vec` drop returns memory to the global allocator).
///
/// See the [module-level documentation](self) for the full design rationale
/// and safety model.
#[derive(Clone)]
pub struct SyncBumpArena {
    inner: Arc<Mutex<SyncBumpInner>>,
}

impl SyncBumpArena {
    /// Creates a new `SyncBumpArena` with no initial capacity.
    ///
    /// The first allocation will trigger a 4 KiB chunk allocation.
    #[inline]
    #[must_use]
    pub fn new() -> Self {
        Self {
            inner: Arc::new(Mutex::new(SyncBumpInner::new())),
        }
    }

    /// Creates a new `SyncBumpArena` with an initial chunk of the given size.
    ///
    /// This avoids a growth step on the first allocations if the approximate
    /// total size is known in advance.
    #[inline]
    #[must_use]
    pub fn with_capacity(capacity: usize) -> Self {
        Self {
            inner: Arc::new(Mutex::new(SyncBumpInner::with_capacity(capacity))),
        }
    }
}

impl Default for SyncBumpArena {
    #[inline]
    fn default() -> Self {
        Self::new()
    }
}

impl std::fmt::Debug for SyncBumpArena {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("SyncBumpArena")
            .field("inner", &Arc::as_ptr(&self.inner))
            .finish()
    }
}

// SAFETY: `allocate` delegates to `SyncBumpInner::allocate` which returns
// valid, aligned memory from a chunk. The memory remains valid until the
// `Arc<Mutex<SyncBumpInner>>` is dropped (when all clones of the arena are
// gone). `deallocate` is a no-op — sound for bump allocators because memory
// is freed in bulk on drop, never per-allocation.
unsafe impl Allocator for SyncBumpArena {
    #[inline]
    fn allocate(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
        let mut inner = self.inner.lock().expect("mutex poisoned");
        inner.allocate(layout)
    }

    #[inline]
    unsafe fn deallocate(&self, _ptr: NonNull<u8>, _layout: Layout) {
        // No-op: all memory is freed when the arena drops.
    }

    #[inline]
    fn allocate_zeroed(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
        let ptr = self.allocate(layout)?;
        // SAFETY: the memory is valid and we own it exclusively (just allocated).
        unsafe { ptr.cast::<u8>().as_ptr().write_bytes(0, layout.size()) };
        Ok(ptr)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use allocator_api2::alloc::Layout;

    #[test]
    fn test_allocate_basic() {
        let arena = SyncBumpArena::new();
        let layout = Layout::from_size_align(64, 8).unwrap();
        let ptr = arena.allocate(layout).unwrap();
        assert_eq!(ptr.len(), 64);
        // Verify the memory is usable
        unsafe {
            ptr.cast::<u8>().as_ptr().write_bytes(0xAB, 64);
        }
    }

    #[test]
    fn test_allocate_alignment() {
        let arena = SyncBumpArena::new();
        for &align in &[1usize, 2, 4, 8, 16, 32, 64, 128] {
            let layout = Layout::from_size_align(1, align).unwrap();
            let ptr = arena.allocate(layout).unwrap();
            let addr = ptr.cast::<u8>().as_ptr() as usize;
            assert_eq!(addr % align, 0, "alignment {} not respected", align);
        }
    }

    #[test]
    fn test_allocate_zeroed() {
        let arena = SyncBumpArena::new();
        let layout = Layout::from_size_align(128, 8).unwrap();
        let ptr = arena.allocate_zeroed(layout).unwrap();
        let slice = unsafe { ptr.as_ref() };
        assert!(
            slice.iter().all(|&b| b == 0),
            "allocate_zeroed returned non-zero memory"
        );
    }

    #[test]
    fn test_zero_size_allocation() {
        let arena = SyncBumpArena::new();
        let layout = Layout::from_size_align(0, 1).unwrap();
        let ptr = arena.allocate(layout).unwrap();
        assert_eq!(ptr.len(), 0);
    }

    #[test]
    fn test_clone_shares_arena() {
        let arena1 = SyncBumpArena::new();
        let arena2 = arena1.clone();

        let layout = Layout::from_size_align(8, 8).unwrap();
        let ptr1 = arena1.allocate(layout).unwrap();
        let ptr2 = arena2.allocate(layout).unwrap();

        // Both allocations from the same arena — distinct addresses
        let addr1 = ptr1.cast::<u8>().as_ptr() as usize;
        let addr2 = ptr2.cast::<u8>().as_ptr() as usize;
        assert_ne!(addr1, addr2);
    }

    #[test]
    fn test_deallocate_is_noop() {
        let arena = SyncBumpArena::new();
        let layout = Layout::from_size_align(32, 8).unwrap();
        let ptr = arena.allocate(layout).unwrap();

        // deallocate should not crash
        unsafe { arena.deallocate(ptr.cast(), layout) };

        // Memory is still valid after deallocate
        unsafe {
            ptr.cast::<u8>().as_ptr().write_bytes(0xCD, 32);
        }
    }

    #[test]
    fn test_chunk_growth() {
        // Start with a tiny capacity — force growth
        let arena = SyncBumpArena::with_capacity(64);

        // Allocate more than the initial capacity
        let layout = Layout::from_size_align(128, 8).unwrap();
        let ptr = arena.allocate(layout).unwrap();
        assert_eq!(ptr.len(), 128);

        // Further allocations should work after growth
        let layout2 = Layout::from_size_align(256, 8).unwrap();
        let ptr2 = arena.allocate(layout2).unwrap();
        assert_eq!(ptr2.len(), 256);
    }

    #[test]
    fn test_many_allocations() {
        let arena = SyncBumpArena::with_capacity(4096);
        let layout = Layout::from_size_align(48, 8).unwrap();

        // Allocate many times — should trigger multiple chunk growths
        let mut ptrs = Vec::new();
        for _ in 0..1000 {
            ptrs.push(arena.allocate(layout).unwrap());
        }

        // All pointers should be distinct
        let addrs: Vec<usize> = ptrs
            .iter()
            .map(|p| p.cast::<u8>().as_ptr() as usize)
            .collect();
        let unique: std::collections::HashSet<_> = addrs.iter().collect();
        assert_eq!(
            unique.len(),
            1000,
            "all 1000 allocations should be at distinct addresses"
        );
    }

    #[test]
    fn test_drop_frees_memory() {
        // Verify Arc refcounting works — dropping all clones frees the arena.
        let arena1 = SyncBumpArena::new();
        let arena2 = arena1.clone();

        drop(arena1);
        // arena2 still alive — allocation should work
        let layout = Layout::from_size_align(16, 8).unwrap();
        let _ = arena2.allocate(layout).unwrap();

        drop(arena2);
        // No way to test memory freeing directly, but no crash = success
    }

    #[test]
    fn test_with_capacity() {
        let arena = SyncBumpArena::with_capacity(8192);
        let layout = Layout::from_size_align(4096, 8).unwrap();
        let ptr = arena.allocate(layout).unwrap();
        assert_eq!(ptr.len(), 4096);
    }

    #[test]
    fn test_send_sync_bounds() {
        // Compile-time verification that SyncBumpArena is Send + Sync
        fn assert_send_sync<T: Send + Sync>() {}
        assert_send_sync::<SyncBumpArena>();
    }

    #[test]
    fn test_concurrent_allocation() {
        use std::sync::Arc as StdArc;
        use std::thread;

        let arena = StdArc::new(SyncBumpArena::with_capacity(4096 * 4));
        let layout = Layout::from_size_align(64, 8).unwrap();

        // Spawn multiple threads that allocate from the same arena
        let handles: Vec<_> = (0..4)
            .map(|_| {
                let arena = StdArc::clone(&arena);
                thread::spawn(move || {
                    let mut ptrs = Vec::new();
                    for _ in 0..100 {
                        ptrs.push(arena.allocate(layout).unwrap());
                    }
                    // Verify all allocations are distinct
                    let addrs: Vec<usize> = ptrs
                        .iter()
                        .map(|p| p.cast::<u8>().as_ptr() as usize)
                        .collect();
                    let unique: std::collections::HashSet<_> = addrs.iter().collect();
                    assert_eq!(unique.len(), 100);
                })
            })
            .collect();

        for handle in handles {
            handle.join().expect("thread should not panic");
        }
    }

    #[test]
    fn test_btreemap_with_arena() {
        use arena_btreemap::BTreeMap;

        let arena = SyncBumpArena::with_capacity(4096);
        let mut map: BTreeMap<String, i32, SyncBumpArena> = BTreeMap::new_in(arena);

        // Insert entries — each BTreeMap node is allocated from the arena
        for i in 0..100 {
            map.insert(format!("key_{:04}", i), i);
        }

        // Verify entries are correct
        for i in 0..100 {
            assert_eq!(map.get(&format!("key_{:04}", i)), Some(&i));
        }

        // Verify iteration order (sorted by key)
        let keys: Vec<_> = map.keys().take(5).collect();
        assert_eq!(keys[0], "key_0000");
        assert_eq!(keys[4], "key_0004");
    }

    #[test]
    fn test_btreemap_clone_shares_arena() {
        use arena_btreemap::BTreeMap;

        let arena = SyncBumpArena::with_capacity(4096);
        let mut map: BTreeMap<String, i32, SyncBumpArena> = BTreeMap::new_in(arena.clone());
        map.insert("a".to_string(), 1);
        map.insert("b".to_string(), 2);

        // Clone the map — should share the same arena
        let map2 = map.clone();
        assert_eq!(map2.get("a"), Some(&1));
        assert_eq!(map2.get("b"), Some(&2));

        // Original map still works
        assert_eq!(map.get("a"), Some(&1));
    }
}