Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 17 additions & 7 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed

- **`FirstFitBStackAllocator::new` — recovery triggered with `recovery_needed == 0` could fail spuriously under the `atomic` feature** (`alloc` + `set` features): When `new` decides to run recovery from an out-of-range `free_head` rather than from the flag itself (so the on-disk flag is still `0`), the recovery routine's final clear under the `atomic` feature performed `BStack::cas(1 → 0)` and surfaced a "recovery_needed was not set when clearing" error, making `new` return `Err` on a stack it could otherwise have repaired. Recovery now resets the flag with a direct `BStack::set` of zeros: it is authoritative and must run regardless of the prior flag value. Non-atomic behaviour is unchanged (the helper already did a plain set).
- **`FirstFitBStackAllocator::realloc` — tail-grow missing `recovery_needed` guard** (`alloc` + `set` features): A multi-step tail grow (`BStack::extend`, then zero, header, footer) without the recovery flag could leave an unrecoverable mid-arena layout on crash: for a grow delta of ≥ 24 bytes, a crash after `extend` but before the header write left the old block's valid header followed by zero bytes that recovery would read as a corrupt block (`"recovery: corrupted block header ... manual repair required"`), refusing to proceed. Tail-grow now sets `recovery_needed` before `extend` and clears it after the footer write in both atomic and non-atomic builds.

### Added

- `BStack::try_extend_zeros` (`atomic`): appends zeros only if current size matches; conditional atomic extend.
- `BStack::get_batched` (`atomic`): reads multiple byte ranges under one lock.
- `BStack::get_batched_into` (`atomic`): like `get_batched` but into caller-provided buffers.
- `BStack::get_batched_gen` (`atomic`): like `get_batched_into` but with a generator closure for dependent reads.
- `BStack::cross_exchange` (`set` + `atomic`): atomically swaps two non-overlapping byte regions.
- `BStack::copy` (`set` + `atomic`): copies a byte region to another offset under one lock.
- `BStack::eq_crds` (`set` + `atomic`): writes region B only if region A equals an expected value (compare-and-swap across two regions).
- `BStack::ne_crds` (`set` + `atomic`): like `eq_crds` but writes when region A does not match.
- `BStack::masked_eq_crds` (`set` + `atomic`): like `eq_crds` with a bitmask applied to the comparison.
- `BStack::masked_ne_crds` (`set` + `atomic`): like `ne_crds` with a bitmask applied to the comparison.
- **`CheckedSlabBStackAllocator`** (`alloc` + `set` features) *(Experimental)*: New crash-recoverable fixed-block slab allocator. Every block carries an 8-byte overhead prefix: zero when free (`data[0..8]` holds the next-free block offset, sentinel `0`), high bit set with the block count in the low bits when in use. The double-free guard reads the overhead and returns `InvalidInput` before touching the free list. Leaked blocks are recoverable by a linear scan. Constructor takes `data_size` (usable bytes per block, ≥ 8); the on-disk `block_size` is `data_size + 8`. Magic: `ALCK\x00\x01\x00\x00`. Multi-block allocations always extend the tail (the free list holds single blocks only). Crash-consistency model: block payloads are written before `free_head` is updated, and the in-use high bit is flipped last, so a crash at any step leaks at most the current block without corrupting the rest of the list.

### Changed

- **`FirstFitBStackAllocator` version bumped to 0.1.3** (`alloc` + `set` features): Magic number updated from `ALFF\x00\x01\x02\x00` to `ALFF\x00\x01\x03\x00`. This reflects the crash-recovery fix by adding the missing recovery guard around `realloc` tail-grow. This version new version of `FirstFitBStackAllocator` is also thread-safe when used with the `atomic` feature. Existing 0.1.x files remain fully compatible (only the first 6 bytes are checked on open).
- **`FirstFitBStackAllocator` version bumped to 0.1.3`** (`alloc` + `set` features): Magic number updated from `ALFF\x00\x01\x02\x00` to `ALFF\x00\x01\x03\x00`. This reflects the crash-recovery fix by adding the missing recovery guard around `realloc` tail-grow. This version new version of `FirstFitBStackAllocator` is also thread-safe when used with the `atomic` feature. Existing 0.1.x files remain fully compatible (only the first 6 bytes are checked on open).
- **`FirstFitBStackAllocator` is `Send + Sync` with the `atomic` feature** (`alloc` + `set` features): Without `atomic`, `FirstFitBStackAllocator` remains `Send` but not `Sync` — operations mutate the on-disk free list in several steps, a data race under concurrent `&self` access. With the `atomic` feature it gains `Sync`: an internal `std::sync::Mutex` serializes the two compound operations not already made atomic by `BStack`'s per-call write lock — free-list mutation and stack extension/discard — while in-place writes within an already-allocated block (in-bucket grow, same-block zeroing) stay lock-free. The `recovery_needed` flag is now updated with a compare-and-swap (no extra cost over the disk write it performs regardless), which also rejects operating on a stack left in a needs-recovery state. Unlike `LinearBStackAllocator`'s optimistic `try_extend`/`try_discard` (which reports a lost tail race as `Unsupported`), a contended `FirstFit` operation blocks on the mutex. Structurally, the `#[cfg(not(feature = "atomic"))] PhantomData<Cell<()>>` field opts out of `Sync`; under `atomic` that field is replaced by the `Mutex`, which makes the type `Sync` without an `unsafe impl`. Documentation updated across type-level docs, module overview, crate overview, and README.
- **`FirstFitBStackAllocator::alloc` and `realloc` - optimizes heap memory allocation out of the if statement and lock scope** (`alloc` + `set` features): Both methods now compute the aligned length and allocate the return buffer before acquiring the write lock, so the critical section only covers the actual stack mutation. This reduces contention and latency under concurrent access.
- **`FirstFitBStackAllocator::alloc` and `realloc` optimizes heap memory allocation out of the if statement and lock scope** (`alloc` + `set` features): Both methods now compute the aligned length and allocate the return buffer before acquiring the write lock, so the critical section only covers the actual stack mutation. This reduces contention and latency under concurrent access.
- **`FirstFitBStackAllocator::cascade_discard_free_tail` — remove unneeded recovery needed flag operations** (`alloc` + `set` features): This helper function is only called from `dealloc` when the freed block is at the tail, so the caller is already responsible for setting and clearing `recovery_needed` around the `discard` call. The helper no longer touches the flag, eliminating redundant writes and reentry into non-reentrant lock in atomic builds that is created by cas of `recovery_needed` in the loop.

### Fixed

- **`FirstFitBStackAllocator::new` — recovery triggered with `recovery_needed == 0` could fail spuriously under the `atomic` feature** (`alloc` + `set` features): When `new` decides to run recovery from an out-of-range `free_head` rather than from the flag itself (so the on-disk flag is still `0`), the recovery routine's final clear under the `atomic` feature performed `BStack::cas(1 → 0)` and surfaced a "recovery_needed was not set when clearing" error, making `new` return `Err` on a stack it could otherwise have repaired. Recovery now resets the flag with a direct `BStack::set` of zeros: it is authoritative and must run regardless of the prior flag value. Non-atomic behaviour is unchanged (the helper already did a plain set).
- **`FirstFitBStackAllocator::realloc` — tail-grow missing `recovery_needed` guard** (`alloc` + `set` features): A multi-step tail grow (`BStack::extend`, then zero, header, footer) without the recovery flag could leave an unrecoverable mid-arena layout on crash: for a grow delta of ≥ 24 bytes, a crash after `extend` but before the header write left the old block's valid header followed by zero bytes that recovery would read as a corrupt block (`"recovery: corrupted block header ... manual repair required"`), refusing to proceed. Tail-grow now sets `recovery_needed` before `extend` and clears it after the footer write in both atomic and non-atomic builds.

## [0.2.2] - 2026-05-26

### Added
Expand Down
12 changes: 12 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,18 @@ required-features = ["atomic"]
name = "atomic_race"
required-features = ["atomic"]

[[example]]
name = "move_and_cow"
required-features = ["set", "atomic"]

[[example]]
name = "linked_list"
required-features = ["atomic"]

[[example]]
name = "checksummed_cache"
required-features = ["set", "atomic"]

[dev-dependencies]
rand = "0.10.1"
criterion = { version = "0.5", features = ["html_reports"] }
Expand Down
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,8 @@ clean-zip:
rm -rf $(BUILD)/*.tar.gz $(BUILD)/*.zip

clean-data:
rm -rf **/*.bstack
@rm -rf **/*.bstack
@rm -rf *.bstack

# ── Zip ───────────────────────────────────────────────────────────────────────
zip: $(BUILD)
Expand Down
39 changes: 32 additions & 7 deletions c/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,18 @@ LIB_BYTEVEC_SET = libbstack-bytevec-set.a
TEST_BV_BIN = test_bstack_bytevec$(EXE_EXT)
TEST_BV_OBJ = test_bstack_bytevec.o

EXAMPLE_NAMES = basic buffer_reuse journal reading vec_store
EXAMPLE_BINS = $(addprefix ../examples/,$(addsuffix $(EXE_EXT),$(EXAMPLE_NAMES)))
HASHMAP_BIN = ../examples/hashmap$(EXE_EXT)
ATOMIC_BIN = ../examples/atomic_ops$(EXE_EXT)
EXAMPLE_NAMES = basic buffer_reuse journal reading vec_store
EXAMPLE_BINS = $(addprefix ../examples/,$(addsuffix $(EXE_EXT),$(EXAMPLE_NAMES)))
HASHMAP_BIN = ../examples/hashmap$(EXE_EXT)
ATOMIC_BIN = ../examples/atomic_ops$(EXE_EXT)
MOVE_COW_BIN = ../examples/move_and_cow$(EXE_EXT)
LINKED_LIST_BIN = ../examples/linked_list$(EXE_EXT)
CHECKSUMMED_CACHE_BIN = ../examples/checksummed_cache$(EXE_EXT)

.PHONY: all test test-set test-atomic test-set-atomic test-first-fit test-first-fit-atomic test-ghost-tree test-slab test-checked-slab test-bytevec leaks clean alloc \
example example-basic example-buffer_reuse example-journal \
example-reading example-vec_store example-hashmap example-atomic_ops
example-reading example-vec_store example-hashmap example-atomic_ops example-move_and_cow \
example-linked_list example-checksummed_cache

all: $(LIB)

Expand Down Expand Up @@ -241,6 +245,15 @@ endif
../examples/atomic_ops$(EXE_EXT): ../examples/atomic_ops.c $(LIB_SET_ATOMIC)
$(CC) $(CFLAGS) -DBSTACK_FEATURE_SET -DBSTACK_FEATURE_ATOMIC -I. -o $@ $< -L. -lbstack-set-atomic -lpthread

../examples/move_and_cow$(EXE_EXT): ../examples/move_and_cow.c $(LIB_SET_ATOMIC)
$(CC) $(CFLAGS) -DBSTACK_FEATURE_SET -DBSTACK_FEATURE_ATOMIC -I. -o $@ $< -L. -lbstack-set-atomic -lpthread

../examples/linked_list$(EXE_EXT): ../examples/linked_list.c $(LIB_ATOMIC)
$(CC) $(CFLAGS) -DBSTACK_FEATURE_ATOMIC -I. -o $@ $< -L. -lbstack-atomic -lpthread

../examples/checksummed_cache$(EXE_EXT): ../examples/checksummed_cache.c $(LIB_SET_ATOMIC)
$(CC) $(CFLAGS) -DBSTACK_FEATURE_SET -DBSTACK_FEATURE_ATOMIC -I. -o $@ $< -L. -lbstack-set-atomic -lpthread

example-basic: ../examples/basic$(EXE_EXT)
(cd ../examples && $(RUN)basic$(EXE_EXT))

Expand All @@ -262,14 +275,26 @@ example-hashmap: ../examples/hashmap$(EXE_EXT)
example-atomic_ops: ../examples/atomic_ops$(EXE_EXT)
(cd ../examples && $(RUN)atomic_ops$(EXE_EXT))

example: $(EXAMPLE_BINS) $(HASHMAP_BIN) $(ATOMIC_BIN)
example-move_and_cow: ../examples/move_and_cow$(EXE_EXT)
(cd ../examples && $(RUN)move_and_cow$(EXE_EXT))

example-linked_list: ../examples/linked_list$(EXE_EXT)
(cd ../examples && $(RUN)linked_list$(EXE_EXT))

example-checksummed_cache: ../examples/checksummed_cache$(EXE_EXT)
(cd ../examples && $(RUN)checksummed_cache$(EXE_EXT))

example: $(EXAMPLE_BINS) $(HASHMAP_BIN) $(ATOMIC_BIN) $(MOVE_COW_BIN) $(LINKED_LIST_BIN) $(CHECKSUMMED_CACHE_BIN)
@echo "==> basic" && (cd ../examples && $(RUN)basic$(EXE_EXT))
@echo "==> buffer_reuse" && (cd ../examples && $(RUN)buffer_reuse$(EXE_EXT))
@echo "==> journal" && (cd ../examples && $(RUN)journal$(EXE_EXT))
@echo "==> reading" && (cd ../examples && $(RUN)reading$(EXE_EXT))
@echo "==> vec_store" && (cd ../examples && $(RUN)vec_store$(EXE_EXT))
@echo "==> hashmap" && (cd ../examples && $(RUN)hashmap$(EXE_EXT))
@echo "==> atomic_ops" && (cd ../examples && $(RUN)atomic_ops$(EXE_EXT))
@echo "==> move_and_cow" && (cd ../examples && $(RUN)move_and_cow$(EXE_EXT))
@echo "==> linked_list" && (cd ../examples && $(RUN)linked_list$(EXE_EXT))
@echo "==> checksummed_cache" && (cd ../examples && $(RUN)checksummed_cache$(EXE_EXT))

clean:
rm -f $(OBJ) $(TEST_OBJ) $(OBJ_SET) $(LIB) $(LIB_SET) $(TEST_BIN) \
Expand All @@ -281,4 +306,4 @@ clean:
$(TEST_GT_OBJ) $(TEST_GT_BIN) \
$(TEST_SL_OBJ) $(TEST_SL_BIN) \
$(OBJ_BYTEVEC) $(LIB_BYTEVEC_SET) $(TEST_BV_OBJ) $(TEST_BV_BIN) \
$(EXAMPLE_BINS) $(HASHMAP_BIN) $(ATOMIC_BIN)
$(EXAMPLE_BINS) $(HASHMAP_BIN) $(ATOMIC_BIN) $(MOVE_COW_BIN) $(LINKED_LIST_BIN) $(CHECKSUMMED_CACHE_BIN)
Loading
Loading