|
|
@@ -0,0 +1,228 @@
|
|
|
+#include <smartbotic/database/client.hpp>
|
|
|
+
|
|
|
+#include <atomic>
|
|
|
+#include <chrono>
|
|
|
+#include <iomanip>
|
|
|
+#include <iostream>
|
|
|
+#include <sstream>
|
|
|
+#include <string>
|
|
|
+#include <thread>
|
|
|
+#include <vector>
|
|
|
+
|
|
|
+using namespace smartbotic::database;
|
|
|
+using Clock = std::chrono::steady_clock;
|
|
|
+
|
|
|
+struct Counters {
|
|
|
+ std::atomic<uint64_t> inserts{0};
|
|
|
+ std::atomic<uint64_t> insertFails{0};
|
|
|
+ std::atomic<uint64_t> resourceExhausted{0};
|
|
|
+ std::atomic<uint64_t> deadlineExceeded{0};
|
|
|
+ std::atomic<uint64_t> otherErrors{0};
|
|
|
+ std::atomic<bool> stop{false};
|
|
|
+};
|
|
|
+
|
|
|
+// Tracking for transition detection
|
|
|
+struct StatsSnapshot {
|
|
|
+ int64_t timeMs;
|
|
|
+ std::string level;
|
|
|
+ uint32_t percent;
|
|
|
+ uint64_t docs;
|
|
|
+ uint64_t evicted;
|
|
|
+ uint64_t inserts;
|
|
|
+ uint64_t fails;
|
|
|
+};
|
|
|
+
|
|
|
+int main(int argc, char* argv[]) {
|
|
|
+ int durationSec = 30;
|
|
|
+ int writers = 8;
|
|
|
+ int docBytes = 2048;
|
|
|
+ if (argc > 1) durationSec = std::stoi(argv[1]);
|
|
|
+ if (argc > 2) writers = std::stoi(argv[2]);
|
|
|
+ if (argc > 3) docBytes = std::stoi(argv[3]);
|
|
|
+
|
|
|
+ std::cout << "=== smartbotic-database v1.7.0 load test ===\n"
|
|
|
+ << "Server: localhost:9005\n"
|
|
|
+ << "Duration: " << durationSec << "s\n"
|
|
|
+ << "Writers: " << writers << "\n"
|
|
|
+ << "Doc size: " << docBytes << " bytes\n"
|
|
|
+ << "Memory cap: 32 MB (configured in server)\n"
|
|
|
+ << "\n";
|
|
|
+
|
|
|
+ Client::Config cfg;
|
|
|
+ cfg.address = "localhost:9005";
|
|
|
+ cfg.timeoutMs = 3000;
|
|
|
+ cfg.writeRetries = 5;
|
|
|
+ cfg.writeRetryBackoffMs = 50;
|
|
|
+ cfg.writeRetryMaxBackoffMs = 1000;
|
|
|
+
|
|
|
+ Client db(cfg);
|
|
|
+ if (!db.connect()) {
|
|
|
+ std::cerr << "connect failed\n";
|
|
|
+ return 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!db.createCollection("loadtest")) {
|
|
|
+ std::cout << "(collection loadtest already exists, continuing)\n";
|
|
|
+ }
|
|
|
+
|
|
|
+ Counters counters;
|
|
|
+ auto startTime = Clock::now();
|
|
|
+ std::string pad(docBytes, 'x');
|
|
|
+
|
|
|
+ // Writer threads — each with its own Client
|
|
|
+ std::vector<std::thread> writerThreads;
|
|
|
+ for (int w = 0; w < writers; w++) {
|
|
|
+ writerThreads.emplace_back([&, w]() {
|
|
|
+ Client::Config wcfg = cfg;
|
|
|
+ Client wdb(wcfg);
|
|
|
+ if (!wdb.connect()) return;
|
|
|
+
|
|
|
+ uint64_t seq = 0;
|
|
|
+ while (!counters.stop.load(std::memory_order_relaxed)) {
|
|
|
+ try {
|
|
|
+ nlohmann::json doc{
|
|
|
+ {"worker", w},
|
|
|
+ {"seq", seq},
|
|
|
+ {"pad", pad}
|
|
|
+ };
|
|
|
+ std::string id = "w" + std::to_string(w) + "-" + std::to_string(seq);
|
|
|
+ wdb.insert("loadtest", doc, id);
|
|
|
+ counters.inserts.fetch_add(1, std::memory_order_relaxed);
|
|
|
+ } catch (const std::exception& e) {
|
|
|
+ counters.insertFails.fetch_add(1, std::memory_order_relaxed);
|
|
|
+ std::string msg = e.what();
|
|
|
+ if (msg.find("RESOURCE_EXHAUSTED") != std::string::npos ||
|
|
|
+ msg.find("memory pressure") != std::string::npos) {
|
|
|
+ counters.resourceExhausted.fetch_add(1, std::memory_order_relaxed);
|
|
|
+ } else if (msg.find("Deadline") != std::string::npos ||
|
|
|
+ msg.find("DEADLINE") != std::string::npos) {
|
|
|
+ counters.deadlineExceeded.fetch_add(1, std::memory_order_relaxed);
|
|
|
+ } else {
|
|
|
+ counters.otherErrors.fetch_add(1, std::memory_order_relaxed);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ seq++;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // Stats sampler thread
|
|
|
+ std::vector<StatsSnapshot> timeline;
|
|
|
+ std::string lastLevel = "normal";
|
|
|
+ std::thread statsThread([&]() {
|
|
|
+ while (!counters.stop.load(std::memory_order_relaxed)) {
|
|
|
+ auto s = db.getMemoryStats();
|
|
|
+ auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
|
+ Clock::now() - startTime).count();
|
|
|
+
|
|
|
+ uint64_t docs = 0, evicted = 0;
|
|
|
+ for (const auto& c : s.collections) {
|
|
|
+ if (c.collection == "loadtest") {
|
|
|
+ docs = c.documentCount;
|
|
|
+ evicted = c.evictedStubCount;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ StatsSnapshot snap{elapsed, s.pressureLevel, s.pressurePercent, docs, evicted,
|
|
|
+ counters.inserts.load(), counters.insertFails.load()};
|
|
|
+ timeline.push_back(snap);
|
|
|
+
|
|
|
+ // Highlight pressure transitions
|
|
|
+ std::string marker = " ";
|
|
|
+ if (s.pressureLevel != lastLevel) {
|
|
|
+ marker = ">> ";
|
|
|
+ lastLevel = s.pressureLevel;
|
|
|
+ }
|
|
|
+
|
|
|
+ std::cout << marker << "[t=" << std::setw(5) << elapsed << "ms] "
|
|
|
+ << "pressure=" << std::setw(9) << s.pressureLevel
|
|
|
+ << " " << std::setw(3) << s.pressurePercent << "%"
|
|
|
+ << " mem=" << std::setw(5) << (s.totalMemoryBytes / 1024) << "KB"
|
|
|
+ << " live=" << std::setw(6) << docs
|
|
|
+ << " evicted=" << std::setw(6) << evicted
|
|
|
+ << " inserts=" << std::setw(7) << counters.inserts.load()
|
|
|
+ << " fails=" << std::setw(5) << counters.insertFails.load()
|
|
|
+ << "\n";
|
|
|
+ std::cout.flush();
|
|
|
+ std::this_thread::sleep_for(std::chrono::milliseconds(500));
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ std::this_thread::sleep_for(std::chrono::seconds(durationSec));
|
|
|
+ counters.stop.store(true);
|
|
|
+ for (auto& t : writerThreads) t.join();
|
|
|
+ statsThread.join();
|
|
|
+
|
|
|
+ auto endTime = Clock::now();
|
|
|
+ auto totalMs = std::chrono::duration_cast<std::chrono::milliseconds>(endTime - startTime).count();
|
|
|
+
|
|
|
+ auto finalStats = db.getMemoryStats();
|
|
|
+ uint64_t finalDocs = 0, finalEvicted = 0;
|
|
|
+ for (const auto& c : finalStats.collections) {
|
|
|
+ if (c.collection == "loadtest") {
|
|
|
+ finalDocs = c.documentCount;
|
|
|
+ finalEvicted = c.evictedStubCount;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Count pressure-level transitions
|
|
|
+ int transitions = 0;
|
|
|
+ std::string prev = "normal";
|
|
|
+ int timeInEach[4] = {0, 0, 0, 0}; // normal, soft, hard, emergency
|
|
|
+ for (size_t i = 0; i < timeline.size(); ++i) {
|
|
|
+ const auto& s = timeline[i];
|
|
|
+ if (s.level != prev) {
|
|
|
+ transitions++;
|
|
|
+ prev = s.level;
|
|
|
+ }
|
|
|
+ if (s.level == "normal") timeInEach[0]++;
|
|
|
+ else if (s.level == "soft") timeInEach[1]++;
|
|
|
+ else if (s.level == "hard") timeInEach[2]++;
|
|
|
+ else if (s.level == "emergency") timeInEach[3]++;
|
|
|
+ }
|
|
|
+
|
|
|
+ uint64_t totalInserts = counters.inserts.load();
|
|
|
+ uint64_t totalFails = counters.insertFails.load();
|
|
|
+
|
|
|
+ std::cout << "\n=== Summary ===\n";
|
|
|
+ std::cout << "Duration: " << totalMs << "ms\n";
|
|
|
+ std::cout << "Total insert attempts: " << (totalInserts + totalFails) << "\n";
|
|
|
+ std::cout << "Successful inserts: " << totalInserts
|
|
|
+ << " (" << (totalInserts * 1000 / totalMs) << " ops/s)\n";
|
|
|
+ std::cout << "Failed (after retries): " << totalFails << "\n";
|
|
|
+ std::cout << " RESOURCE_EXHAUSTED: " << counters.resourceExhausted.load() << "\n";
|
|
|
+ std::cout << " DEADLINE_EXCEEDED: " << counters.deadlineExceeded.load() << "\n";
|
|
|
+ std::cout << " Other: " << counters.otherErrors.load() << "\n";
|
|
|
+ std::cout << "\n";
|
|
|
+ std::cout << "Final state:\n";
|
|
|
+ std::cout << " Pressure: " << finalStats.pressureLevel
|
|
|
+ << " (" << finalStats.pressurePercent << "%)\n";
|
|
|
+ std::cout << " Memory: " << (finalStats.totalMemoryBytes / 1024)
|
|
|
+ << "KB / " << (finalStats.maxMemoryBytes / 1024) << "KB\n";
|
|
|
+ std::cout << " Live docs: " << finalDocs << "\n";
|
|
|
+ std::cout << " Evicted stubs: " << finalEvicted << "\n";
|
|
|
+ std::cout << " Last eviction: " << finalStats.lastEvictionDocs
|
|
|
+ << " docs, " << finalStats.lastEvictionBytesFreed << " bytes\n";
|
|
|
+ std::cout << "\n";
|
|
|
+ std::cout << "Pressure timeline (" << timeline.size() << " samples @ 500ms):\n";
|
|
|
+ std::cout << " Normal: " << timeInEach[0] << " samples ("
|
|
|
+ << (timeInEach[0] * 500) << "ms)\n";
|
|
|
+ std::cout << " Soft: " << timeInEach[1] << " samples ("
|
|
|
+ << (timeInEach[1] * 500) << "ms)\n";
|
|
|
+ std::cout << " Hard: " << timeInEach[2] << " samples ("
|
|
|
+ << (timeInEach[2] * 500) << "ms)\n";
|
|
|
+ std::cout << " Emergency: " << timeInEach[3] << " samples ("
|
|
|
+ << (timeInEach[3] * 500) << "ms)\n";
|
|
|
+ std::cout << " Transitions: " << transitions << "\n";
|
|
|
+ std::cout << "\n";
|
|
|
+ std::cout << "Verdict: ";
|
|
|
+ if (totalFails == 0) {
|
|
|
+ std::cout << "PASS — client-side retry absorbed all transient errors\n";
|
|
|
+ } else if (counters.resourceExhausted.load() > 0 && totalFails == counters.resourceExhausted.load()) {
|
|
|
+ std::cout << "PASS — only RESOURCE_EXHAUSTED errors (admission control engaged as designed)\n";
|
|
|
+ } else {
|
|
|
+ std::cout << "REVIEW — unexpected failures, check error counts\n";
|
|
|
+ }
|
|
|
+
|
|
|
+ return 0;
|
|
|
+}
|