Some checks failed
Fuzz Smoke / libFuzzer smoke (push) Has been cancelled
* chore(warnings): default-initialize aggregate members left out of brace-init
GCC -Wmissing-field-initializers fired about 1300 times where tests and
src build structs with a short positional brace list. Give the omitted
members an empty default member initializer. Value-initialization from
{} is what aggregate init already did for them, so behavior is
unchanged.
Signed-off-by: trvon <me@trevon.dev>
* chore(warnings): remove or annotate unused parameters, functions and variables
- init_command, s3_signer test, request_handler_lifetime test: comment out
the unused parameter name.
- video_content_handler: path is only read under YAMS_HAVE_FFPROBE.
- vector_memory_scaling_bench: main() takes no arguments.
- daemon_status_render: delete unused neutralText helper.
- DaemonMetrics: mallocStackLoggingNoCompactEnabled is only called in the
__APPLE__ block, so define it there.
- SocketServer: einval_streak only matters in the __APPLE__ EINVAL path.
- onnx_plugin_harness test, vector_backend_engine_compare bench,
work_coordinator test: mark platform-conditional or busy-loop
variables [[maybe_unused]].
Signed-off-by: trvon <git@trevon.dev>
* chore(warnings): fix sign-compare, range-loop, nodiscard and dangling-ref in tests
- search_admission_controller: count started workers in std::uint32_t so
the comparison with the uint32 cap is same-signed.
- p2p_manager: iterate the invalid-host list by const reference.
- integrity_verifier: discard the nodiscard results the tests do not
inspect with static_cast<void>.
- post_ingest_queue: findNode/findEdge take std::string_view so no
temporary std::string is created for the call (GCC
-Wdangling-reference false positive on the returned reference).
- topology_ablation_bench: the doc comment ended a line with a backslash,
which continues the comment (-Wcomment); show the env export on its own
line.
Signed-off-by: trvon <git@trevon.dev>
* chore(warnings): drop redundant std::move of a const captured json
The coroutine lambda is not mutable, so the captured optional<json> is
const and std::move(*progressToken) already selected the copy assignment.
Drop the std::move so the code states what actually happens.
Signed-off-by: trvon <git@trevon.dev>
* chore(warnings): mark __int128 use in db_integrity_stamp as an extension
-Wpedantic flags __int128. Use a __extension__ typedef, the GCC/Clang
idiom for an intentional extension, instead of the bare keyword.
Signed-off-by: trvon <git@trevon.dev>
* chore(warnings): remove always-true unsigned >= 0 comparisons
GCC -Wtype-limits flagged 51 test assertions of the form
CHECK(unsignedValue >= 0) that can never fail, plus a dead
`totalCount.value() < 0` clause in the graph listByType handler (the
count is std::size_t). In the tests keep the evaluation as a
static_cast<void> so the accessors are still exercised; in the handler
drop the dead clause and keep the range check.
Signed-off-by: trvon <me@trevon.dev>
---------
Signed-off-by: trvon <me@trevon.dev>
Signed-off-by: trvon <git@trevon.dev>
430 lines
14 KiB
C++
430 lines
14 KiB
C++
// Catch2 stress tests for reference counter
|
|
// Migrated from GTest: reference_counter_stress_test.cpp
|
|
|
|
#include <catch2/catch_test_macros.hpp>
|
|
|
|
#include <atomic>
|
|
#include <chrono>
|
|
#include <filesystem>
|
|
#include <format>
|
|
#include <future>
|
|
#include <latch>
|
|
#include <random>
|
|
#include <thread>
|
|
#include <vector>
|
|
|
|
#include <yams/storage/reference_counter.h>
|
|
|
|
using namespace yams::storage;
|
|
using namespace std::chrono_literals;
|
|
|
|
namespace {
|
|
|
|
struct ReferenceCounterStressFixture {
|
|
ReferenceCounterStressFixture() {
|
|
testDbPath = std::filesystem::temp_directory_path() /
|
|
std::format("yams_refcount_stress_catch2_{}.db",
|
|
std::chrono::system_clock::now().time_since_epoch().count());
|
|
|
|
ReferenceCounter::Config config{.databasePath = testDbPath,
|
|
.enableWAL = true,
|
|
.enableStatistics = true,
|
|
.cacheSize = 10000,
|
|
.busyTimeout = 5000,
|
|
.enableAuditLog = false};
|
|
|
|
refCounter = std::make_unique<ReferenceCounter>(std::move(config));
|
|
}
|
|
|
|
~ReferenceCounterStressFixture() {
|
|
refCounter.reset();
|
|
std::filesystem::remove(testDbPath);
|
|
std::filesystem::remove(testDbPath.string() + "-wal");
|
|
std::filesystem::remove(testDbPath.string() + "-shm");
|
|
}
|
|
|
|
std::string generateHash(int i) { return std::format("stress_hash_{:064}", i); }
|
|
|
|
std::filesystem::path testDbPath;
|
|
std::unique_ptr<ReferenceCounter> refCounter;
|
|
};
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE_METHOD(ReferenceCounterStressFixture, "ReferenceCounter concurrent getStats",
|
|
"[storage][refcount][stress][slow][catch2]") {
|
|
constexpr int numThreads = 20;
|
|
constexpr int numIterations = 100;
|
|
constexpr int numBlocks = 50;
|
|
|
|
// Populate some data first
|
|
for (int i = 0; i < numBlocks; ++i) {
|
|
refCounter->increment(generateHash(i), 4096);
|
|
}
|
|
|
|
std::atomic<int> successCount{0};
|
|
std::atomic<int> errorCount{0};
|
|
std::vector<std::thread> threads;
|
|
std::latch startLatch(numThreads);
|
|
|
|
for (int t = 0; t < numThreads; ++t) {
|
|
threads.emplace_back([&]() {
|
|
// Wait for all threads to be ready
|
|
startLatch.arrive_and_wait();
|
|
|
|
for (int i = 0; i < numIterations; ++i) {
|
|
auto stats = refCounter->getStats();
|
|
if (stats.has_value()) {
|
|
successCount++;
|
|
// Verify stats make sense
|
|
static_cast<void>(stats.value().totalBlocks);
|
|
static_cast<void>(stats.value().totalReferences);
|
|
} else {
|
|
errorCount++;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
for (auto& t : threads) {
|
|
t.join();
|
|
}
|
|
|
|
CHECK((successCount.load() > 0));
|
|
CHECK((errorCount.load() == 0));
|
|
|
|
// Final stats check
|
|
auto finalStats = refCounter->getStats();
|
|
REQUIRE(finalStats.has_value());
|
|
CHECK((finalStats.value().totalBlocks == static_cast<uint64_t>(numBlocks)));
|
|
CHECK((finalStats.value().totalReferences == static_cast<uint64_t>(numBlocks)));
|
|
}
|
|
|
|
TEST_CASE_METHOD(ReferenceCounterStressFixture, "ReferenceCounter concurrent mixed operations",
|
|
"[storage][refcount][stress][slow][catch2]") {
|
|
constexpr int numThreads = 16;
|
|
constexpr int opsPerThread = 500;
|
|
constexpr int numHashes = 100;
|
|
|
|
std::atomic<int> successCount{0};
|
|
std::atomic<int> errorCount{0};
|
|
std::vector<std::thread> threads;
|
|
std::latch startLatch(numThreads);
|
|
|
|
for (int t = 0; t < numThreads; ++t) {
|
|
threads.emplace_back([&, threadId = t]() {
|
|
std::mt19937 rng(threadId);
|
|
std::uniform_int_distribution<int> hashDist(0, numHashes - 1);
|
|
std::uniform_int_distribution<int> opDist(0, 3);
|
|
|
|
startLatch.arrive_and_wait();
|
|
|
|
for (int i = 0; i < opsPerThread; ++i) {
|
|
auto hash = generateHash(hashDist(rng));
|
|
int op = opDist(rng);
|
|
|
|
bool success = false;
|
|
switch (op) {
|
|
case 0: // increment
|
|
success = refCounter->increment(hash, 4096).has_value();
|
|
break;
|
|
case 1: // decrement
|
|
success = refCounter->decrement(hash).has_value();
|
|
break;
|
|
case 2: // getRefCount
|
|
success = refCounter->getRefCount(hash).has_value();
|
|
break;
|
|
case 3: // getStats
|
|
success = refCounter->getStats().has_value();
|
|
break;
|
|
default:
|
|
FAIL("unexpected mixed-operation selector");
|
|
break;
|
|
}
|
|
|
|
if (success) {
|
|
successCount++;
|
|
} else {
|
|
errorCount++;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
for (auto& t : threads) {
|
|
t.join();
|
|
}
|
|
|
|
CHECK((successCount.load() > numThreads * opsPerThread * 0.99));
|
|
}
|
|
|
|
TEST_CASE_METHOD(ReferenceCounterStressFixture, "ReferenceCounter concurrent transactions",
|
|
"[storage][refcount][stress][slow][transaction][catch2]") {
|
|
constexpr int numThreads = 12;
|
|
constexpr int txnsPerThread = 50;
|
|
constexpr int opsPerTxn = 10;
|
|
|
|
std::atomic<int> commitCount{0};
|
|
std::atomic<int> rollbackCount{0};
|
|
std::atomic<int> errorCount{0};
|
|
std::vector<std::thread> threads;
|
|
std::latch startLatch(numThreads);
|
|
|
|
for (int t = 0; t < numThreads; ++t) {
|
|
threads.emplace_back([&, threadId = t]() {
|
|
std::mt19937 rng(threadId);
|
|
std::bernoulli_distribution shouldCommit(0.8); // 80% commit, 20% rollback
|
|
|
|
startLatch.arrive_and_wait();
|
|
|
|
for (int i = 0; i < txnsPerThread; ++i) {
|
|
auto txn = refCounter->beginTransaction();
|
|
if (!txn) {
|
|
errorCount++;
|
|
continue;
|
|
}
|
|
|
|
// Add operations
|
|
for (int op = 0; op < opsPerTxn; ++op) {
|
|
auto hash = generateHash(threadId * 1000 + i * 100 + op);
|
|
if (op % 2 == 0) {
|
|
txn->increment(hash, 4096);
|
|
} else {
|
|
txn->decrement(hash);
|
|
}
|
|
}
|
|
|
|
// Randomly commit or rollback
|
|
if (shouldCommit(rng)) {
|
|
auto result = txn->commit();
|
|
if (result.has_value()) {
|
|
commitCount++;
|
|
} else {
|
|
errorCount++;
|
|
}
|
|
} else {
|
|
txn->rollback();
|
|
rollbackCount++;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
for (auto& t : threads) {
|
|
t.join();
|
|
}
|
|
|
|
CHECK((commitCount.load() > 0));
|
|
CHECK((rollbackCount.load() > 0));
|
|
CHECK((errorCount.load() == 0));
|
|
CHECK((commitCount + rollbackCount == numThreads * txnsPerThread));
|
|
}
|
|
|
|
TEST_CASE_METHOD(ReferenceCounterStressFixture, "ReferenceCounter concurrent getUnreferencedBlocks",
|
|
"[storage][refcount][stress][slow][catch2]") {
|
|
constexpr int numReaderThreads = 4;
|
|
constexpr int numWriterThreads = 8;
|
|
constexpr int iterations = 100;
|
|
constexpr int numBlocks = 200;
|
|
|
|
std::atomic<bool> stopFlag{false};
|
|
std::atomic<int> readerSuccessCount{0};
|
|
std::atomic<int> writerSuccessCount{0};
|
|
std::vector<std::thread> threads;
|
|
std::latch startLatch(numReaderThreads + numWriterThreads);
|
|
|
|
// Writer threads: constantly increment/decrement while readers are active.
|
|
for (int t = 0; t < numWriterThreads; ++t) {
|
|
threads.emplace_back([&, threadId = t]() {
|
|
std::mt19937 rng(threadId);
|
|
std::uniform_int_distribution<int> blockDist(0, numBlocks - 1);
|
|
|
|
startLatch.arrive_and_wait();
|
|
while (!stopFlag.load(std::memory_order_relaxed)) {
|
|
auto hash = generateHash(blockDist(rng));
|
|
if (threadId % 2 == 0) {
|
|
if (refCounter->increment(hash, 4096).has_value()) {
|
|
writerSuccessCount++;
|
|
}
|
|
} else {
|
|
if (refCounter->decrement(hash).has_value()) {
|
|
writerSuccessCount++;
|
|
}
|
|
}
|
|
std::this_thread::yield();
|
|
}
|
|
});
|
|
}
|
|
|
|
// Reader threads: repeatedly query while writers are mutating state.
|
|
for (int t = 0; t < numReaderThreads; ++t) {
|
|
threads.emplace_back([&]() {
|
|
startLatch.arrive_and_wait();
|
|
for (int i = 0; i < iterations; ++i) {
|
|
auto blocks = refCounter->getUnreferencedBlocks(50, std::chrono::seconds(0));
|
|
if (blocks.has_value()) {
|
|
readerSuccessCount++;
|
|
}
|
|
std::this_thread::yield();
|
|
}
|
|
});
|
|
}
|
|
|
|
for (int i = 0; i < numReaderThreads; ++i) {
|
|
threads[numWriterThreads + i].join();
|
|
}
|
|
stopFlag.store(true, std::memory_order_relaxed);
|
|
for (int i = 0; i < numWriterThreads; ++i) {
|
|
threads[i].join();
|
|
}
|
|
|
|
CHECK((readerSuccessCount.load() > 0));
|
|
CHECK((writerSuccessCount.load() > 0));
|
|
}
|
|
|
|
TEST_CASE_METHOD(ReferenceCounterStressFixture, "ReferenceCounter high load batch operations",
|
|
"[storage][refcount][stress][slow][batch][catch2]") {
|
|
constexpr int numThreads = 8;
|
|
constexpr int batchesPerThread = 100;
|
|
constexpr int batchSize = 100;
|
|
|
|
std::atomic<int> successCount{0};
|
|
std::vector<std::thread> threads;
|
|
std::latch startLatch(numThreads);
|
|
|
|
for (int t = 0; t < numThreads; ++t) {
|
|
threads.emplace_back([&, threadId = t]() {
|
|
startLatch.arrive_and_wait();
|
|
|
|
for (int b = 0; b < batchesPerThread; ++b) {
|
|
std::vector<std::string> hashes;
|
|
for (int i = 0; i < batchSize; ++i) {
|
|
hashes.push_back(generateHash(threadId * 100000 + b * 1000 + i));
|
|
}
|
|
|
|
if (refCounter->incrementBatch(hashes, 4096).has_value()) {
|
|
successCount++;
|
|
}
|
|
|
|
if (refCounter->decrementBatch(hashes).has_value()) {
|
|
successCount++;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
for (auto& t : threads) {
|
|
t.join();
|
|
}
|
|
|
|
CHECK((successCount.load() == numThreads * batchesPerThread * 2));
|
|
}
|
|
|
|
TEST_CASE_METHOD(ReferenceCounterStressFixture,
|
|
"ReferenceCounter transaction isActive concurrent observer",
|
|
"[storage][refcount][stress][slow][transaction][tsan][catch2]") {
|
|
constexpr int iterations = 200;
|
|
|
|
std::atomic<int> activeTrue{0};
|
|
std::atomic<int> activeFalse{0};
|
|
|
|
for (int i = 0; i < iterations; ++i) {
|
|
auto txn = refCounter->beginTransaction();
|
|
REQUIRE((txn != nullptr));
|
|
txn->increment(generateHash(10'000 + i), 4096);
|
|
|
|
std::atomic<bool> stopObserver{false};
|
|
std::atomic<bool> observedOnce{false};
|
|
std::thread observer([&]() {
|
|
while (!stopObserver.load(std::memory_order_relaxed)) {
|
|
if (txn->isActive()) {
|
|
activeTrue.fetch_add(1, std::memory_order_relaxed);
|
|
} else {
|
|
activeFalse.fetch_add(1, std::memory_order_relaxed);
|
|
}
|
|
observedOnce.store(true, std::memory_order_release);
|
|
}
|
|
|
|
if (txn->isActive()) {
|
|
activeTrue.fetch_add(1, std::memory_order_relaxed);
|
|
} else {
|
|
activeFalse.fetch_add(1, std::memory_order_relaxed);
|
|
}
|
|
});
|
|
|
|
// Guarantee the observer logs at least one pre-commit sample so the
|
|
// activeTrue counter is non-zero independent of thread-start scheduling.
|
|
while (!observedOnce.load(std::memory_order_acquire)) {
|
|
std::this_thread::yield();
|
|
}
|
|
|
|
if ((i % 2) == 0) {
|
|
auto commitResult = txn->commit();
|
|
REQUIRE(commitResult.has_value());
|
|
} else {
|
|
txn->rollback();
|
|
}
|
|
|
|
stopObserver.store(true, std::memory_order_relaxed);
|
|
observer.join();
|
|
CHECK_FALSE(txn->isActive());
|
|
}
|
|
|
|
CHECK((activeTrue.load() > 0));
|
|
CHECK((activeFalse.load() > 0));
|
|
}
|
|
|
|
TEST_CASE_METHOD(ReferenceCounterStressFixture, "ReferenceCounter reproduce getStats crash",
|
|
"[storage][refcount][stress][slow][regression][catch2]") {
|
|
// This keeps the original daemon-metrics regression in the fast lane,
|
|
// but uses bounded concurrent work instead of a multi-second soak.
|
|
constexpr int numThreads = 10;
|
|
constexpr int workerIterations = 250;
|
|
constexpr int statsIterations = 200;
|
|
|
|
std::atomic<int> workerErrors{0};
|
|
std::vector<std::thread> threads;
|
|
std::latch startLatch(numThreads);
|
|
|
|
// Background worker threads
|
|
for (int t = 0; t < numThreads - 1; ++t) {
|
|
threads.emplace_back([&, threadId = t]() {
|
|
std::mt19937 rng(threadId);
|
|
std::uniform_int_distribution<int> hashDist(0, 49);
|
|
|
|
startLatch.arrive_and_wait();
|
|
for (int i = 0; i < workerIterations; ++i) {
|
|
auto hash = generateHash(hashDist(rng));
|
|
if (!refCounter->increment(hash, 4096).has_value()) {
|
|
workerErrors.fetch_add(1, std::memory_order_relaxed);
|
|
}
|
|
if ((i % 4) == 0) {
|
|
std::this_thread::yield();
|
|
}
|
|
if (!refCounter->decrement(hash).has_value()) {
|
|
workerErrors.fetch_add(1, std::memory_order_relaxed);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Metrics thread (reproduces DaemonMetrics::refresh calling getStats)
|
|
threads.emplace_back([&]() {
|
|
startLatch.arrive_and_wait();
|
|
for (int i = 0; i < statsIterations; ++i) {
|
|
auto stats = refCounter->getStats();
|
|
REQUIRE(stats.has_value());
|
|
|
|
// Verify stats are valid (not garbage from use-after-free)
|
|
CHECK((stats.value().totalBlocks < 1000000u));
|
|
CHECK((stats.value().totalReferences < 1000000u));
|
|
std::this_thread::yield();
|
|
}
|
|
});
|
|
|
|
for (auto& t : threads) {
|
|
t.join();
|
|
}
|
|
|
|
CHECK((workerErrors.load() == 0));
|
|
}
|