// SPDX-License-Identifier: LGPL-3.0-only // SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® // Readers scanning (with tombstones present, so the per-row flag test is on) // while one writer appends and erases. Every row a scan yields must be a // published, live-at-some-point row with increasing ids, and nothing may // read past the published slots. Guards the sentinel-based iteration and the // tombstone flag against the Append race. import Crafter.DB; import std; using namespace Crafter; namespace fs = std::filesystem; namespace { int failures = 0; void Check(bool condition, std::string_view what) { if (!condition) { std::println("FAIL: {}", what); ++failures; } } // 16-byte rows: a power-of-two size, so a full extent ends exactly on a // row boundary and the slot after the last published one can be the // first byte of the unmapped reservation. struct Tick { std::uint64_t sequence; std::uint64_t value; static constexpr std::uint32_t version = 1; static constexpr auto orderedBy = &Tick::sequence; }; static_assert(DB::Layout::rowSize == 24 || DB::Layout::rowSize == 32); } int main() { const fs::path dir = fs::temp_directory_path() / "crafter-db-concurrent"; fs::remove_all(dir); DB::Table table(dir / "ticks.cdb"); table.Append({ 0, 0 }); table.Append({ 1, 1 }); table.Erase(0); // a tombstone from the start: scans run with the flag test std::atomic stop{ false }; std::atomic scans{ 0 }; std::atomic badOrder{ 0 }; std::atomic badId{ 0 }; std::atomic sawErased{ 0 }; const auto reader = [&] { while (!stop.load(std::memory_order_relaxed)) { std::uint64_t last = 0; bool first = true; const std::uint64_t slots = table.SlotCount(); for (DB::Row row : table) { const std::uint64_t id = row.Id(); if (!first && id <= last) badOrder.fetch_add(1, std::memory_order_relaxed); if (id >= table.SlotCount()) badId.fetch_add(1, std::memory_order_relaxed); if (row[&Tick::sequence] != id) badId.fetch_add(1, std::memory_order_relaxed); first = false; last = id; } if (slots > 2 && table.Rows(0, 1).Any()) sawErased.fetch_add(1, std::memory_order_relaxed); scans.fetch_add(1, std::memory_order_relaxed); } }; std::vector readers; for (std::int32_t i = 0; i < 4; i++) readers.emplace_back(reader); std::vector batch; for (std::uint64_t sequence = 2; sequence < 400'000; sequence++) { batch.push_back({ sequence, sequence }); if (batch.size() == 64) { table.Append(batch); batch.clear(); if (sequence % 1024 == 1) table.Erase(sequence - 500); } } stop.store(true); readers.clear(); Check(scans.load() > 0, "readers ran"); Check(badOrder.load() == 0, std::format("ids strictly increase within a scan ({} violations)", badOrder.load())); Check(badId.load() == 0, std::format("every yielded row is a published row ({} violations)", badId.load())); Check(sawErased.load() == 0, std::format("the erased first row never shows up ({} times)", sawErased.load())); Check(table.Verify().Ok(), "table verifies after the run"); fs::remove_all(dir); if (failures == 0) std::println("ok"); return failures == 0 ? 0 : 1; }