Crafter.DB/tests/ShouldReadWhileAppending/main.cpp

94 lines
3.5 KiB
C++
Raw Normal View History

// 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<Tick>::rowSize == 24 || DB::Layout<Tick>::rowSize == 32);
}
int main() {
const fs::path dir = fs::temp_directory_path() / "crafter-db-concurrent";
fs::remove_all(dir);
DB::Table<Tick> 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<bool> stop{ false };
std::atomic<std::uint64_t> scans{ 0 };
std::atomic<std::uint64_t> badOrder{ 0 };
std::atomic<std::uint64_t> badId{ 0 };
std::atomic<std::uint64_t> 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<Tick> 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<std::jthread> readers;
for (std::int32_t i = 0; i < 4; i++) readers.emplace_back(reader);
std::vector<Tick> 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;
}