armdb 0.12.0

sharded bitcask key-value storage optimized for NVMe
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
use super::tests::{CountingHook, open_fixed_tree};
use crate::fixed::slot::{NO_SLOT, STATUS_OCCUPIED, pack_meta, version_of};
use crate::txlog::record::{OpKind, VersionTag};
use crate::txn::participant::{PlannedOp, TxParticipant};

/// The value is on disk at exactly the logged target → nothing to do.
#[test]
fn tx_fixed_replay_skips_an_operation_that_already_landed() {
    let dir = tempfile::tempdir().unwrap();
    let tree = open_fixed_tree(dir.path(), CountingHook::default());
    let sid = tree.shard_of(&[1u8; 8]).unwrap();

    let mut locked = tree.lock_shards(&[sid]).unwrap();
    let tags = locked
        .reserve(&[PlannedOp {
            key: &[1u8; 8],
            delete: false,
        }])
        .unwrap();
    locked.apply_put(&[1u8; 8], &[5u8; 16], tags[0]).unwrap();
    drop(
        locked
            .finish(
                &mut Vec::new(),
                &mut crate::watch::WatchWakeBatch::default(),
            )
            .unwrap(),
    );

    let mut again = tree.lock_shards(&[sid]).unwrap();
    assert!(
        !again.needs_replay(&[1u8; 8], OpKind::Put, tags[0]).unwrap(),
        "the slot already holds the committed state"
    );
    again.abort();
}

/// Nothing was applied: the slot is still free at an older version.
#[test]
fn tx_fixed_replay_replays_an_operation_that_never_landed() {
    let dir = tempfile::tempdir().unwrap();
    let tree = open_fixed_tree(dir.path(), CountingHook::default());
    let sid = tree.shard_of(&[2u8; 8]).unwrap();
    let mut locked = tree.lock_shards(&[sid]).unwrap();
    let tags = locked
        .reserve(&[PlannedOp {
            key: &[2u8; 8],
            delete: false,
        }])
        .unwrap();
    assert!(
        locked
            .needs_replay(&[2u8; 8], OpKind::Put, tags[0])
            .unwrap()
    );
    locked.abort();
}

/// Target version equals the slot's version, but the value is torn: dirty
/// recovery freed the slot while preserving the version, so equality alone
/// would wrongly skip a write that is not there.
#[test]
fn tx_fixed_replay_replays_a_torn_slot_at_the_target_version() {
    let dir = tempfile::tempdir().unwrap();
    let tree = open_fixed_tree(dir.path(), CountingHook::default());
    let sid = tree.shard_of(&[3u8; 8]).unwrap();

    let mut locked = tree.lock_shards(&[sid]).unwrap();
    let tags = locked
        .reserve(&[PlannedOp {
            key: &[3u8; 8],
            delete: false,
        }])
        .unwrap();
    locked.apply_put(&[3u8; 8], &[6u8; 16], tags[0]).unwrap();
    drop(
        locked
            .finish(
                &mut Vec::new(),
                &mut crate::watch::WatchWakeBatch::default(),
            )
            .unwrap(),
    );

    let VersionTag::Fixed {
        slot_id,
        target_meta,
    } = tags[0]
    else {
        unreachable!("Fixed participant")
    };
    // Recovery's verdict for a CRC-invalid OCCUPIED slot: FREE, same version.
    tree.tx_debug_force_slot_meta(
        sid,
        slot_id,
        pack_meta(crate::fixed::slot::STATUS_FREE, version_of(target_meta)),
    );

    let mut again = tree.lock_shards(&[sid]).unwrap();
    assert!(
        again.needs_replay(&[3u8; 8], OpKind::Put, tags[0]).unwrap(),
        "a torn slot at the target version must be replayed"
    );
    again.abort();
}

/// The slot was deleted and handed to another key, which ran the version on.
#[test]
fn tx_fixed_replay_skips_a_slot_reused_by_another_key() {
    let dir = tempfile::tempdir().unwrap();
    let tree = open_fixed_tree(dir.path(), CountingHook::default());
    let sid = tree.shard_of(&[4u8; 8]).unwrap();

    let mut locked = tree.lock_shards(&[sid]).unwrap();
    let tags = locked
        .reserve(&[PlannedOp {
            key: &[4u8; 8],
            delete: false,
        }])
        .unwrap();
    locked.apply_put(&[4u8; 8], &[1u8; 16], tags[0]).unwrap();
    drop(
        locked
            .finish(
                &mut Vec::new(),
                &mut crate::watch::WatchWakeBatch::default(),
            )
            .unwrap(),
    );

    // Delete it, then let another key take the slot back.
    tree.delete(&[4u8; 8]).unwrap();
    tree.put(&[5u8; 8], &[2u8; 16]).unwrap();

    let mut again = tree.lock_shards(&[sid]).unwrap();
    assert!(
        !again.needs_replay(&[4u8; 8], OpKind::Put, tags[0]).unwrap(),
        "the slot has moved on; replaying would clobber another key"
    );
    again.abort();
}

/// A delete whose target status is already on the slot.
#[test]
fn tx_fixed_replay_skips_a_delete_that_already_landed() {
    let dir = tempfile::tempdir().unwrap();
    let tree = open_fixed_tree(dir.path(), CountingHook::default());
    tree.put(&[6u8; 8], &[1u8; 16]).unwrap();
    let sid = tree.shard_of(&[6u8; 8]).unwrap();

    let mut locked = tree.lock_shards(&[sid]).unwrap();
    let tags = locked
        .reserve(&[PlannedOp {
            key: &[6u8; 8],
            delete: true,
        }])
        .unwrap();
    locked.apply_delete(&[6u8; 8], tags[0]).unwrap();
    drop(
        locked
            .finish(
                &mut Vec::new(),
                &mut crate::watch::WatchWakeBatch::default(),
            )
            .unwrap(),
    );

    let mut again = tree.lock_shards(&[sid]).unwrap();
    assert!(
        !again
            .needs_replay(&[6u8; 8], OpKind::Delete, tags[0])
            .unwrap()
    );
    again.abort();
}

/// A logged no-op carries no slot and is never replayed.
#[test]
fn tx_fixed_replay_skips_the_no_slot_sentinel() {
    let dir = tempfile::tempdir().unwrap();
    let tree = open_fixed_tree(dir.path(), CountingHook::default());
    let sid = tree.shard_of(&[7u8; 8]).unwrap();
    let mut locked = tree.lock_shards(&[sid]).unwrap();
    let tag = VersionTag::Fixed {
        slot_id: NO_SLOT,
        target_meta: 0,
    };
    assert!(!locked.needs_replay(&[7u8; 8], OpKind::Delete, tag).unwrap());
    locked.abort();
}

/// Beyond a lag of 2^29 the ring cannot order the pair. Guessing could
/// resurrect a deleted row or clobber a live one, so the open fails.
#[test]
fn tx_fixed_replay_fails_closed_on_an_unorderable_version() {
    let dir = tempfile::tempdir().unwrap();
    let tree = open_fixed_tree(dir.path(), CountingHook::default());
    tree.put(&[8u8; 8], &[1u8; 16]).unwrap();
    let sid = tree.shard_of(&[8u8; 8]).unwrap();

    let mut locked = tree.lock_shards(&[sid]).unwrap();
    let tags = locked
        .reserve(&[PlannedOp {
            key: &[8u8; 8],
            delete: false,
        }])
        .unwrap();
    let VersionTag::Fixed {
        slot_id,
        target_meta,
    } = tags[0]
    else {
        unreachable!("Fixed participant")
    };
    locked.abort();

    // Push the slot's version exactly 2^29 ahead of the reservation target:
    // `is_newer` is strictly below the half-ring, so neither direction orders.
    let ahead = pack_meta(STATUS_OCCUPIED, version_of(target_meta) + (1 << 29));
    tree.tx_debug_force_slot_meta(sid, slot_id, ahead);

    let mut again = tree.lock_shards(&[sid]).unwrap();
    let err = again
        .needs_replay(&[8u8; 8], OpKind::Put, tags[0])
        .unwrap_err();
    assert!(matches!(err, crate::DbError::TxFixedSlot { .. }), "{err}");
    again.abort();
}

/// The reservation's slot was recycled and now holds a different, intact row at
/// exactly the logged target version. Replaying would overwrite a live row that
/// belongs to another key, so the open fails rather than guessing.
#[test]
fn tx_fixed_replay_refuses_a_slot_owned_by_another_key() {
    let dir = tempfile::tempdir().unwrap();
    let tree = open_fixed_tree(dir.path(), CountingHook::default());
    let mine = [10u8; 8];
    let theirs = [11u8; 8];
    let sid = tree.shard_of(&mine).unwrap();

    let mut locked = tree.lock_shards(&[sid]).unwrap();
    let tags = locked
        .reserve(&[PlannedOp {
            key: &mine,
            delete: false,
        }])
        .unwrap();
    let VersionTag::Fixed {
        slot_id,
        target_meta,
    } = tags[0]
    else {
        unreachable!("Fixed participant")
    };
    // Hand the slot back — what a panic between the log point and apply used to
    // do, and what an abort before the log point legitimately does.
    locked.abort();

    // Another key takes the recycled slot and lands on the same version.
    tree.put(&theirs, &[3u8; 16]).unwrap();
    let state = tree.tx_debug_slot_state(sid, slot_id).unwrap();
    assert_eq!(
        state.meta, target_meta,
        "the recycled slot reached our exact target version"
    );
    assert!(state.crc_valid, "the other key's row is intact");

    let mut again = tree.lock_shards(&[sid]).unwrap();
    let err = again.needs_replay(&mine, OpKind::Put, tags[0]).unwrap_err();
    assert!(matches!(err, crate::DbError::TxFixedSlot { .. }), "{err}");
    again.abort();
    assert_eq!(
        tree.get(&theirs),
        Some([3u8; 16]),
        "the other key's row must be untouched"
    );
}

/// `superseded_elsewhere` takes precedence over the unorderable case: when the
/// key demonstrably lives in another slot, the orphaned reservation is skipped
/// rather than failing the open. Characterises the short-circuit added with the
/// index-slot check, which the replay table now documents.
#[test]
fn tx_fixed_replay_skips_an_unorderable_version_when_the_key_moved_on() {
    let dir = tempfile::tempdir().unwrap();
    let tree = open_fixed_tree(dir.path(), CountingHook::default());
    let key = [12u8; 8];
    let sid = tree.shard_of(&key).unwrap();

    let mut locked = tree.lock_shards(&[sid]).unwrap();
    let tags = locked
        .reserve(&[PlannedOp {
            key: &key,
            delete: false,
        }])
        .unwrap();
    let VersionTag::Fixed {
        slot_id: orphaned,
        target_meta,
    } = tags[0]
    else {
        unreachable!("Fixed participant")
    };
    // Durable but unapplied: the reservation stays, nothing is written.
    drop(
        locked
            .finish(
                &mut Vec::new(),
                &mut crate::watch::WatchWakeBatch::default(),
            )
            .unwrap(),
    );

    // The key's live incarnation lands in a different slot.
    tree.put(&key, &[2u8; 16]).unwrap();
    // And the orphaned slot drifts beyond the orderable half-ring.
    let ahead = pack_meta(STATUS_OCCUPIED, version_of(target_meta) + (1 << 29));
    tree.tx_debug_force_slot_meta(sid, orphaned, ahead);

    let mut again = tree.lock_shards(&[sid]).unwrap();
    assert!(
        !again.needs_replay(&key, OpKind::Put, tags[0]).unwrap(),
        "a superseded reservation is skipped, not failed closed"
    );
    again.abort();
    assert_eq!(tree.get(&key), Some([2u8; 16]));
}

/// A failed apply leaves the reserved slot allocated but the key out of the
/// index. A later commit for the same key then lands in a different slot. Replay
/// of the orphaned reservation must not overwrite that newer incarnation.
#[test]
fn tx_fixed_replay_skips_when_key_lands_in_a_newer_slot() {
    let dir = tempfile::tempdir().unwrap();
    let tree = open_fixed_tree(dir.path(), CountingHook::default());
    let key = [9u8; 8];
    let sid = tree.shard_of(&key).unwrap();

    let mut locked = tree.lock_shards(&[sid]).unwrap();
    let tags = locked
        .reserve(&[PlannedOp {
            key: &key,
            delete: false,
        }])
        .unwrap();
    let VersionTag::Fixed {
        slot_id: orphaned, ..
    } = tags[0]
    else {
        unreachable!("Fixed participant")
    };
    // Same as a durable-but-unapplied finish: keep the reservation, write nothing.
    drop(
        locked
            .finish(
                &mut Vec::new(),
                &mut crate::watch::WatchWakeBatch::default(),
            )
            .unwrap(),
    );

    tree.put(&key, &[2u8; 16]).unwrap();
    let live = {
        let mut locked = tree.lock_shards(&[sid]).unwrap();
        let live_tags = locked
            .reserve(&[PlannedOp {
                key: &key,
                delete: false,
            }])
            .unwrap();
        let VersionTag::Fixed {
            slot_id: live_slot, ..
        } = live_tags[0]
        else {
            unreachable!("Fixed participant")
        };
        locked.abort();
        live_slot
    };
    assert_ne!(
        orphaned, live,
        "the later write must have allocated a different slot"
    );

    let mut again = tree.lock_shards(&[sid]).unwrap();
    assert!(
        !again.needs_replay(&key, OpKind::Put, tags[0]).unwrap(),
        "a newer incarnation of the key supersedes the orphaned reservation"
    );
    again.abort();
    assert_eq!(tree.get(&key), Some([2u8; 16]));
}

/// A reserve that fails part-way must hand back the slots it already took.
///
/// `reserve_in_shard` walks the shard's operations one key at a time, and each
/// absent key takes a slot from the allocator. When a later key fails — a full
/// disk makes `grow()` return `Err` — the tags of the earlier keys never reach
/// the caller, so `TxHeld` has nothing to release on abort and those slots stay
/// marked used until the process restarts and rebuilds the bitmap from disk.
#[test]
fn tx_fixed_reserve_releases_its_slots_when_a_later_key_fails() {
    use crate::fixed::shard::FAIL_RESERVE_IN;
    use std::sync::atomic::Ordering;

    let dir = tempfile::tempdir().unwrap();
    let tree = open_fixed_tree(dir.path(), CountingHook::default());
    let sid = tree.shard_of(&[10u8; 8]).unwrap();
    let before = tree.tx_debug_occupied_slots(sid);

    let keys: [[u8; 8]; 3] = [[10u8; 8], [11u8; 8], [12u8; 8]];
    let planned: Vec<PlannedOp<'_>> = keys
        .iter()
        .map(|k| PlannedOp {
            key: k,
            delete: false,
        })
        .collect();

    // Two keys reserve a slot each, the third fails.
    FAIL_RESERVE_IN.store(2, Ordering::SeqCst);
    let mut locked = tree.lock_shards(&[sid]).unwrap();
    let err = locked.reserve(&planned).unwrap_err();
    FAIL_RESERVE_IN.store(usize::MAX, Ordering::SeqCst);
    assert!(
        format!("{err}").contains("injected reserve failure"),
        "{err}"
    );
    locked.abort();

    assert_eq!(
        tree.tx_debug_occupied_slots(sid),
        before,
        "a reserve that failed part-way must leak no slots"
    );
}