#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace yams; using namespace yams::storage; using namespace std::chrono_literals; // Helper function to generate random bytes static std::vector generateRandomBytes(size_t size) { std::vector data(size); std::random_device rd; std::mt19937 gen(rd()); std::uniform_int_distribution<> dis(0, 255); for (auto& b : data) { b = std::byte{static_cast(dis(gen))}; } return data; } struct ConcurrentStorageTest { std::unique_ptr storage; std::filesystem::path storagePath; ConcurrentStorageTest() { storagePath = std::filesystem::temp_directory_path() / "yams_stress_concurrent_test"; std::filesystem::create_directories(storagePath); StorageConfig config{.basePath = storagePath, .shardDepth = 2, .mutexPoolSize = 1024, .maxConcurrentReaders = 1000, .maxConcurrentWriters = 100}; storage = std::make_unique(std::move(config)); } ~ConcurrentStorageTest() { storage.reset(); std::filesystem::remove_all(storagePath); } }; double runPoolWorkload(yams::metadata::ConnectionPool& pool, std::size_t workers, std::size_t opsPerThread) { std::atomic errors{0}; auto start = std::chrono::steady_clock::now(); std::vector threads; threads.reserve(workers); for (std::size_t t = 0; t < workers; ++t) { threads.emplace_back([&, threadIndex = t]() { for (std::size_t op = 0; op < opsPerThread; ++op) { auto res = pool.withConnection([&](yams::metadata::Database& db) -> yams::Result { auto stmtRes = db.prepare("INSERT INTO kv(test_key, value) VALUES (?, ?);"); if (!stmtRes) return stmtRes.error(); auto stmt = std::move(stmtRes.value()); auto bindRes = stmt.bindAll(static_cast(threadIndex), std::string("payload-") + std::to_string(op)); if (!bindRes) return bindRes.error(); return stmt.execute(); }); if (!res) errors.fetch_add(1, std::memory_order_relaxed); } }); } for (auto& th : threads) th.join(); auto end = std::chrono::steady_clock::now(); auto elapsed = std::chrono::duration(end - start).count(); CHECK(errors.load() == 0U); double ops = static_cast(workers * opsPerThread); return (elapsed > 0.0) ? ops / elapsed : ops; } TEST_CASE_METHOD(ConcurrentStorageTest, "ThousandConcurrentReaders", "[stress][concurrentstoragetest]") { // Pre-store test objects with varying sizes constexpr size_t numObjects = 100; std::vector>> testObjects; // Test multiple object size categories std::vector sizeBuckets = {512, 1024, 4096, 8192, 16384}; for (size_t i = 0; i < numObjects; ++i) { size_t sizeCategory = sizeBuckets[i % sizeBuckets.size()]; auto data = generateRandomBytes(sizeCategory + (i % 100)); auto hasher = crypto::createSHA256Hasher(); auto hash = hasher->hash(data); testObjects.emplace_back(hash, data); REQUIRE(storage->store(hash, data).has_value()); } // Test with different reader counts to validate scalability std::vector readerCounts = {100, 500, 1000}; for (size_t numReaders : readerCounts) { std::cout << "\n=== Testing with " << numReaders << " concurrent readers ===" << std::endl; constexpr size_t readsPerThread = 100; std::atomic totalReads{0}; std::atomic successfulReads{0}; std::atomic hasErrors{false}; // Use C++20 barrier for synchronization std::barrier startBarrier(numReaders + 1); auto startTime = std::chrono::steady_clock::now(); std::vector readers; readers.reserve(numReaders); for (size_t i = 0; i < numReaders; ++i) { readers.emplace_back([&, threadId = i]() { std::random_device rd; std::mt19937 gen(rd() + threadId); std::uniform_int_distribution dis(0, numObjects - 1); // Wait for all threads to be ready startBarrier.arrive_and_wait(); for (size_t j = 0; j < readsPerThread; ++j) { auto& [hash, expectedData] = testObjects[dis(gen)]; totalReads.fetch_add(1); auto result = storage->retrieve(hash); if (!result.has_value()) { hasErrors.store(true); spdlog::error("Read failed for hash: {}", hash); } else if (result.value() != expectedData) { hasErrors.store(true); spdlog::error("Data mismatch for hash: {}", hash); } else { successfulReads.fetch_add(1); } } }); } // Start all threads simultaneously startBarrier.arrive_and_wait(); // Wait for all readers to complete for (auto& reader : readers) { reader.join(); } auto endTime = std::chrono::steady_clock::now(); auto duration = std::chrono::duration_cast(endTime - startTime); // Verify results INFO("Errors with " << numReaders << " readers"); CHECK_FALSE(hasErrors.load()); CHECK(successfulReads.load() == numReaders * readsPerThread); // Performance check auto readsPerSecond = (totalReads.load() * 1000) / duration.count(); spdlog::info("Completed {} reads in {} ms ({} reads/sec)", totalReads.load(), duration.count(), readsPerSecond); // Verify < 10ms latency requirement auto avgLatency = duration.count() / static_cast(readsPerThread); INFO("Average latency too high with " << numReaders << " readers"); CHECK(avgLatency < 15.0); } } TEST_CASE_METHOD(ConcurrentStorageTest, "HundredConcurrentWriters", "[stress][concurrentstoragetest]") { constexpr size_t numWriters = 100; constexpr size_t writesPerThread = 50; std::atomic totalWrites{0}; std::atomic successfulWrites{0}; std::atomic hasErrors{false}; // C++20 latch for completion tracking std::latch completionLatch(numWriters); auto startTime = std::chrono::steady_clock::now(); std::vector writers; writers.reserve(numWriters); for (size_t i = 0; i < numWriters; ++i) { writers.emplace_back([&, threadId = i]() { for (size_t j = 0; j < writesPerThread; ++j) { // Generate unique data for each write auto data = generateRandomBytes(512 + (threadId * 100 + j) % 1024); auto hasher = crypto::createSHA256Hasher(); auto hash = hasher->hash(data); totalWrites.fetch_add(1); auto result = storage->store(hash, data); if (!result.has_value()) { hasErrors.store(true); spdlog::error("Write failed for thread {} iteration {}", threadId, j); } else { successfulWrites.fetch_add(1); } } completionLatch.count_down(); }); } // Wait for all writers for (auto& writer : writers) { writer.join(); } auto endTime = std::chrono::steady_clock::now(); auto duration = std::chrono::duration_cast(endTime - startTime); // Verify results CHECK_FALSE(hasErrors.load()); CHECK(successfulWrites.load() == numWriters * writesPerThread); // Check storage statistics auto stats = storage->getStats(); CHECK(stats.writeOperations.load() == numWriters * writesPerThread); spdlog::info("Completed {} writes in {} ms ({} writes/sec)", totalWrites.load(), duration.count(), (totalWrites.load() * 1000) / duration.count()); } TEST_CASE_METHOD(ConcurrentStorageTest, "MixedReadWriteWorkload", "[stress][concurrentstoragetest]") { // Test multiple read/write ratios struct WorkloadConfig { size_t numReaders; size_t numWriters; const char* description; }; std::vector workloads = {{160, 40, "80/20 read/write (typical)"}, {180, 20, "90/10 read/write (read-heavy)"}, {100, 100, "50/50 read/write (balanced)"}, {50, 150, "25/75 read/write (write-heavy)"}}; for (const auto& workload : workloads) { spdlog::info("Testing workload: {}", workload.description); const size_t numThreads = workload.numReaders + workload.numWriters; constexpr size_t operationsPerThread = 100; // Pre-populate with some data std::vector existingHashes; for (size_t i = 0; i < 500; ++i) { auto data = generateRandomBytes(1024 + (i % 1024)); auto hasher = crypto::createSHA256Hasher(); auto hash = hasher->hash(data); existingHashes.push_back(hash); storage->store(hash, data); } std::atomic readOps{0}; std::atomic writeOps{0}; std::atomic conflicts{0}; std::barrier startBarrier(numThreads + 1); std::vector threads; // Reader threads for (size_t i = 0; i < workload.numReaders; ++i) { threads.emplace_back([&]() { std::random_device rd; std::mt19937 gen(rd()); std::uniform_int_distribution dis(0, existingHashes.size() - 1); startBarrier.arrive_and_wait(); for (size_t j = 0; j < operationsPerThread; ++j) { auto& hash = existingHashes[dis(gen)]; storage->retrieve(hash); readOps.fetch_add(1); } }); } // Writer threads for (size_t i = 0; i < workload.numWriters; ++i) { threads.emplace_back([&, threadId = i]() { startBarrier.arrive_and_wait(); for (size_t j = 0; j < operationsPerThread; ++j) { auto data = generateRandomBytes(2048); auto hasher = crypto::createSHA256Hasher(); auto hash = hasher->hash(data); auto result = storage->store(hash, data); if (result.has_value()) { writeOps.fetch_add(1); } else { conflicts.fetch_add(1); } } }); } auto startTime = std::chrono::steady_clock::now(); startBarrier.arrive_and_wait(); for (auto& thread : threads) { thread.join(); } auto endTime = std::chrono::steady_clock::now(); auto duration = std::chrono::duration_cast(endTime - startTime); spdlog::info(" Completed: {} reads, {} writes in {} ms", readOps.load(), writeOps.load(), duration.count()); INFO("Failed for workload: " << workload.description); CHECK(readOps.load() == workload.numReaders * operationsPerThread); INFO("Failed for workload: " << workload.description); CHECK(writeOps.load() == workload.numWriters * operationsPerThread); INFO("Conflicts in workload: " << workload.description); CHECK(conflicts.load() == 0u); // Cleanup for next workload existingHashes.clear(); } } TEST_CASE_METHOD(ConcurrentStorageTest, "HighContentionSameObjects", "[stress][concurrentstoragetest]") { // Test with different contention levels struct ContentionConfig { size_t numThreads; size_t numObjects; const char* description; }; std::vector configs = { {50, 10, "High contention (50 threads, 10 objects)"}, {100, 20, "Very high contention (100 threads, 20 objects)"}, {25, 50, "Moderate contention (25 threads, 50 objects)"}, {50, 100, "Low contention (50 threads, 100 objects)"}}; for (const auto& config : configs) { spdlog::info("Testing: {}", config.description); constexpr size_t opsPerThread = 200; // Create a set of objects std::vector>> objects; for (size_t i = 0; i < config.numObjects; ++i) { auto data = generateRandomBytes(4096 + (i % 1024)); auto hasher = crypto::createSHA256Hasher(); auto hash = hasher->hash(data); objects.emplace_back(hash, data); storage->store(hash, data); } std::atomic operations{0}; std::atomic dataCorruption{false}; std::atomic readOps{0}; std::atomic writeOps{0}; std::vector threads; for (size_t i = 0; i < config.numThreads; ++i) { threads.emplace_back([&]() { std::random_device rd; std::mt19937 gen(rd()); std::uniform_int_distribution objDis(0, config.numObjects - 1); std::uniform_int_distribution opDis(0, 9); for (size_t j = 0; j < opsPerThread; ++j) { auto& [hash, expectedData] = objects[objDis(gen)]; if (opDis(gen) < 8) { // 80% reads auto result = storage->retrieve(hash); if (!result.has_value() || result.value() != expectedData) { dataCorruption.store(true); } readOps.fetch_add(1); } else { // 20% re-writes (same data) storage->store(hash, expectedData); writeOps.fetch_add(1); } operations.fetch_add(1); } }); } for (auto& thread : threads) { thread.join(); } spdlog::info(" Ops: {} ({} reads, {} writes)", operations.load(), readOps.load(), writeOps.load()); INFO("Data corruption in: " << config.description); CHECK_FALSE(dataCorruption.load()); CHECK(operations.load() == config.numThreads * opsPerThread); } } TEST_CASE_METHOD(ConcurrentStorageTest, "RapidCreateDeleteCycles", "[stress][concurrentstoragetest]") { constexpr size_t numThreads = 20; constexpr size_t cyclesPerThread = 50; std::atomic creates{0}; std::atomic deletes{0}; std::atomic errors{false}; std::vector threads; for (size_t i = 0; i < numThreads; ++i) { threads.emplace_back([&, threadId = i]() { for (size_t j = 0; j < cyclesPerThread; ++j) { // Generate unique data auto data = generateRandomBytes(1024 + threadId * 100 + j); auto hasher = crypto::createSHA256Hasher(); auto hash = hasher->hash(data); // Create if (!storage->store(hash, data).has_value()) { errors.store(true); continue; } creates.fetch_add(1); // Verify auto result = storage->retrieve(hash); if (!result.has_value() || result.value() != data) { errors.store(true); continue; } // Delete if (!storage->remove(hash).has_value()) { errors.store(true); continue; } deletes.fetch_add(1); // Verify deletion auto existsRes = storage->exists(hash); if (!existsRes || existsRes.value()) { errors.store(true); } } }); } for (auto& thread : threads) { thread.join(); } CHECK_FALSE(errors.load()); CHECK(creates.load() == numThreads * cyclesPerThread); CHECK(deletes.load() == numThreads * cyclesPerThread); // Final storage should be empty auto stats = storage->getStats(); CHECK(stats.totalObjects.load() == 0u); } TEST_CASE_METHOD(ConcurrentStorageTest, "LatencyUnderLoad", "[stress][concurrentstoragetest]") { // Pre-populate storage for (size_t i = 0; i < 10000; ++i) { auto data = generateRandomBytes(512); auto hasher = crypto::createSHA256Hasher(); auto hash = hasher->hash(data); storage->store(hash, data); } // Measure individual operation latencies under concurrent load constexpr size_t numBackgroundThreads = 50; constexpr size_t numMeasurements = 1000; std::atomic stopFlag{false}; std::vector backgroundThreads; // Start background load for (size_t i = 0; i < numBackgroundThreads; ++i) { backgroundThreads.emplace_back([&]() { while (!stopFlag.load()) { auto data = generateRandomBytes(1024); auto hasher = crypto::createSHA256Hasher(); auto hash = hasher->hash(data); storage->store(hash, data); storage->retrieve(hash); } }); } // Measure latencies std::vector latencies; latencies.reserve(numMeasurements); auto data = generateRandomBytes(1024); auto hasher = crypto::createSHA256Hasher(); auto hash = hasher->hash(data); storage->store(hash, data); for (size_t i = 0; i < numMeasurements; ++i) { auto start = std::chrono::high_resolution_clock::now(); auto exists = storage->exists(hash); auto end = std::chrono::high_resolution_clock::now(); REQUIRE(exists.has_value()); auto latency = std::chrono::duration(end - start).count(); latencies.push_back(latency); } // Stop background threads stopFlag.store(true); for (auto& thread : backgroundThreads) { thread.join(); } // Calculate statistics std::sort(latencies.begin(), latencies.end()); double p50 = latencies[latencies.size() / 2]; double p95 = latencies[latencies.size() * 95 / 100]; double p99 = latencies[latencies.size() * 99 / 100]; spdlog::info( "Existence check latencies under load - P50: {:.2f}ms, P95: {:.2f}ms, P99: {:.2f}ms", p50, p95, p99); // Verify < 10ms requirement CHECK(p99 < 10.0); } TEST_CASE_METHOD(ConcurrentStorageTest, "ConnectionPoolWorkerSweep", "[stress][concurrentstoragetest]") { const auto dbPath = storagePath / "pool_bench.db"; yams::metadata::Database bootstrap; REQUIRE(bootstrap.open(dbPath.string(), yams::metadata::ConnectionMode::Create)); REQUIRE(bootstrap.execute("PRAGMA journal_mode=WAL;").has_value()); REQUIRE(bootstrap.execute("CREATE TABLE IF NOT EXISTS kv (test_key INTEGER, value TEXT);") .has_value()); bootstrap.close(); yams::metadata::ConnectionPoolConfig cfg; cfg.minConnections = 1; cfg.maxConnections = 8; cfg.busyTimeout = std::chrono::milliseconds(1000); yams::metadata::ConnectionPool pool(dbPath.string(), cfg); REQUIRE(pool.initialize()); const std::array workerSweep{1, 2, 4, 8}; const std::size_t opsPerThread = 150; double baseline = 0.0; for (std::size_t workers : workerSweep) { auto resetRes = pool.withConnection([](yams::metadata::Database& db) -> yams::Result { return db.execute("DELETE FROM kv;"); }); REQUIRE(resetRes); double throughput = runPoolWorkload(pool, workers, opsPerThread); spdlog::info("pool sweep workers={} throughput={}", workers, throughput); if (baseline == 0.0) { baseline = throughput; } else if (baseline > 0.0) { double warnThreshold = baseline * 0.9; double cautionThreshold = baseline * 0.5; double failThreshold = baseline * 0.25; if (throughput < failThreshold) { FAIL_CHECK("connection pool regression for " + std::to_string(workers) + " workers (" + std::to_string(throughput) + " < " + std::to_string(failThreshold) + ")"); } else if (throughput < cautionThreshold) { spdlog::warn( "connection pool throughput dipped below 50% for workers={} ({} vs {} base)", workers, throughput, baseline); } else if (throughput < warnThreshold) { spdlog::info( "connection pool throughput dipped below 90% for workers={} ({} vs {} base)", workers, throughput, baseline); } } } pool.shutdown(); std::filesystem::remove(dbPath); } // New comprehensive stress test: Variable object sizes under concurrent access TEST_CASE_METHOD(ConcurrentStorageTest, "VariableObjectSizesConcurrent", "[stress][concurrentstoragetest]") { spdlog::info("Testing variable object sizes under concurrent access"); struct SizeClass { size_t minSize; size_t maxSize; size_t count; const char* name; }; std::vector sizeClasses = {{100, 1024, 100, "Tiny (100B-1KB)"}, {1024, 10 * 1024, 100, "Small (1KB-10KB)"}, {10 * 1024, 100 * 1024, 50, "Medium (10KB-100KB)"}, {100 * 1024, 1024 * 1024, 20, "Large (100KB-1MB)"}}; // Pre-populate with objects of varying sizes std::vector>> allObjects; for (const auto& sizeClass : sizeClasses) { std::random_device rd; std::mt19937 gen(rd()); std::uniform_int_distribution sizeDist(sizeClass.minSize, sizeClass.maxSize); for (size_t i = 0; i < sizeClass.count; ++i) { auto data = generateRandomBytes(sizeDist(gen)); auto hasher = crypto::createSHA256Hasher(); auto hash = hasher->hash(data); allObjects.emplace_back(hash, data); REQUIRE(storage->store(hash, data).has_value()); } } spdlog::info("Pre-populated {} objects across {} size classes", allObjects.size(), sizeClasses.size()); // Concurrent access test constexpr size_t numThreads = 50; constexpr size_t opsPerThread = 100; std::atomic successfulOps{0}; std::atomic failedOps{0}; std::atomic corruptedReads{0}; std::barrier startBarrier(numThreads + 1); std::vector threads; for (size_t i = 0; i < numThreads; ++i) { threads.emplace_back([&]() { std::random_device rd; std::mt19937 gen(rd()); std::uniform_int_distribution objDist(0, allObjects.size() - 1); startBarrier.arrive_and_wait(); for (size_t j = 0; j < opsPerThread; ++j) { auto& [hash, expectedData] = allObjects[objDist(gen)]; auto result = storage->retrieve(hash); if (!result.has_value()) { failedOps.fetch_add(1); } else if (result.value() != expectedData) { corruptedReads.fetch_add(1); } else { successfulOps.fetch_add(1); } } }); } startBarrier.arrive_and_wait(); for (auto& thread : threads) { thread.join(); } spdlog::info("Variable size test: {} successful, {} failed, {} corrupted", successfulOps.load(), failedOps.load(), corruptedReads.load()); CHECK(successfulOps.load() == numThreads * opsPerThread); CHECK(failedOps.load() == 0u); CHECK(corruptedReads.load() == 0u); } // New stress test: Burst traffic patterns TEST_CASE_METHOD(ConcurrentStorageTest, "BurstTrafficPattern", "[stress][concurrentstoragetest]") { spdlog::info("Testing burst traffic patterns"); // Pre-populate std::vector>> objects; for (size_t i = 0; i < 100; ++i) { auto data = generateRandomBytes(2048); auto hasher = crypto::createSHA256Hasher(); auto hash = hasher->hash(data); objects.emplace_back(hash, data); storage->store(hash, data); } struct BurstConfig { size_t numThreads; size_t opsPerThread; std::chrono::milliseconds burstDuration; std::chrono::milliseconds idleDuration; const char* name; }; std::vector bursts = {{100, 50, 100ms, 200ms, "Quick burst"}, {200, 25, 50ms, 150ms, "Intense burst"}, {50, 100, 200ms, 100ms, "Sustained burst"}}; for (const auto& burst : bursts) { spdlog::info(" Testing: {}", burst.name); std::atomic totalOps{0}; std::atomic hasError{false}; // Burst phase std::vector threads; for (size_t i = 0; i < burst.numThreads; ++i) { threads.emplace_back([&]() { std::random_device rd; std::mt19937 gen(rd()); std::uniform_int_distribution objDist(0, objects.size() - 1); for (size_t j = 0; j < burst.opsPerThread; ++j) { auto& [hash, expectedData] = objects[objDist(gen)]; auto result = storage->retrieve(hash); if (!result.has_value() || result.value() != expectedData) { hasError.store(true); } totalOps.fetch_add(1); } }); } auto start = std::chrono::steady_clock::now(); for (auto& thread : threads) { thread.join(); } auto elapsed = std::chrono::steady_clock::now() - start; spdlog::info(" Completed {} ops in {}ms", totalOps.load(), std::chrono::duration_cast(elapsed).count()); INFO("Errors in burst: " << burst.name); CHECK_FALSE(hasError.load()); CHECK(totalOps.load() == burst.numThreads * burst.opsPerThread); // Idle phase std::this_thread::sleep_for(burst.idleDuration); } } // New stress test: Long-running sustained load TEST_CASE_METHOD(ConcurrentStorageTest, "LongRunningSustainedLoad", "[stress][concurrentstoragetest]") { spdlog::info("Testing long-running sustained load"); constexpr size_t numThreads = 20; constexpr auto testDuration = 30s; // 30 seconds of sustained load // Pre-populate std::vector>> objects; for (size_t i = 0; i < 200; ++i) { auto data = generateRandomBytes(1024 + (i % 2048)); auto hasher = crypto::createSHA256Hasher(); auto hash = hasher->hash(data); objects.emplace_back(hash, data); storage->store(hash, data); } std::atomic stopFlag{false}; std::atomic totalOperations{0}; std::atomic readOps{0}; std::atomic writeOps{0}; std::atomic errors{0}; std::vector threads; auto startTime = std::chrono::steady_clock::now(); for (size_t i = 0; i < numThreads; ++i) { threads.emplace_back([&, threadId = i]() { std::random_device rd; std::mt19937 gen(rd() + threadId); std::uniform_int_distribution objDist(0, objects.size() - 1); std::uniform_int_distribution opTypeDist(0, 99); while (!stopFlag.load()) { int opType = opTypeDist(gen); if (opType < 70) { // 70% reads auto& [hash, expectedData] = objects[objDist(gen)]; auto result = storage->retrieve(hash); if (!result.has_value() || result.value() != expectedData) { errors.fetch_add(1); } readOps.fetch_add(1); } else { // 30% writes (new objects) auto data = generateRandomBytes(1024); auto hasher = crypto::createSHA256Hasher(); auto hash = hasher->hash(data); if (storage->store(hash, data).has_value()) { writeOps.fetch_add(1); } else { errors.fetch_add(1); } } totalOperations.fetch_add(1); } }); } // Let it run for the test duration std::this_thread::sleep_for(testDuration); stopFlag.store(true); for (auto& thread : threads) { thread.join(); } auto endTime = std::chrono::steady_clock::now(); auto elapsed = std::chrono::duration_cast(endTime - startTime).count(); spdlog::info("Long-running test completed:"); spdlog::info(" Duration: {}s", elapsed); spdlog::info(" Total ops: {}", totalOperations.load()); spdlog::info(" Reads: {}, Writes: {}", readOps.load(), writeOps.load()); spdlog::info(" Ops/sec: {}", totalOperations.load() / elapsed); spdlog::info(" Errors: {}", errors.load()); CHECK(errors.load() == 0u); CHECK(totalOperations.load() > numThreads * 100); // Should do many operations }