|
|
před 3 měsíci | |
|---|---|---|
| .. | ||
| migrations | před 3 měsíci | |
| README.md | před 3 měsíci | |
| config.json | před 3 měsíci | |
| config_pinned.json | před 3 měsíci | |
| config_replica_follower.json | před 3 měsíci | |
| config_replica_follower_16mb.json | před 3 měsíci | |
| config_replica_leader.json | před 3 měsíci | |
| config_replica_leader_128mb.json | před 3 měsíci | |
| load_test.cpp | před 3 měsíci | |
| load_test_crash_insert.cpp | před 3 měsíci | |
| load_test_crash_verify.cpp | před 3 měsíci | |
| load_test_integrity.cpp | před 3 měsíci | |
| load_test_mixed.cpp | před 3 měsíci | |
| load_test_pinned.cpp | před 3 měsíci | |
| load_test_replica_eviction.cpp | před 3 měsíci | |
| load_test_replication.cpp | před 3 měsíci | |
| run_crash_test.sh | před 3 měsíci | |
| run_replica_eviction_test.sh | před 3 měsíci | |
| run_replication_test.sh | před 3 měsíci | |
Standalone binaries that validate v1.7.0's chunked eviction + admission control + client retry against a real smartbotic-database instance.
Not unit tests — each one runs a real server on a configurable port, drives a workload, and reports a PASS/FAIL verdict. Not wired into CTest — these are operator/developer validation tools, not CI.
Four load-test binaries:
load_test — pressure + retry validation (the original)load_test_mixed — concurrent reads + writes under pressureload_test_integrity — byte-exact integrity of evicted + paged-in docsload_test_pinned — pinned=true collection protectionAll four talk to localhost:9005 and use Client::Config with transport-level retries. They differ only in workload shape and verdict criteria.
smartbotic-database package installed (for the server binary)libsmartbotic-db-client-dev installed (for headers + shared lib)cd tests/load_test
g++ -std=c++20 -O2 -I/usr/include load_test.cpp \
-lsmartbotic-db-client \
$(pkg-config --libs grpc++) \
-lspdlog -lfmt -pthread \
-o load_test
Uses port 9005 and data dir /tmp/smartbotic-loadtest/ to avoid conflicting with any existing instance on the default port 9004.
# 1. Start the test server with its own config (memory capped at 32 MB)
mkdir -p /tmp/smartbotic-loadtest/data /tmp/smartbotic-loadtest/files
smartbotic-database --config ./config.json > /tmp/smartbotic-loadtest/server.log 2>&1 &
SERVER_PID=$!
sleep 2
# 2. Run the load test — default: 20s, 4 writers, 2 KB docs
./load_test [duration_seconds] [writer_threads] [doc_bytes]
# Example — 30s with 8 writers:
./load_test 30 8 2048
# 3. Stop the server
kill $SERVER_PID
# 4. Inspect eviction events
grep -E "Eviction|MEMORY_" /tmp/smartbotic-loadtest/server.log
Sample output from a successful run (32 MB cap, 20s, 4 writers, 2 KB docs):
[t=18504ms] pressure= hard 84% mem=27584KB live= 5276 evicted= 41580 inserts= 46856 fails= 0
=== Summary ===
Duration: 20162ms
Successful inserts: 51444 (2551 ops/s)
Failed (after retries): 0
Pressure timeline:
normal: 1.5s
soft: 5.0s
hard: 9.0s
emergency: 2.0s
Transitions: 19
Verdict: PASS — client-side retry absorbed all transient errors
| Signal | What it means |
|---|---|
Eviction chunk: N docs evicted, X bytes freed |
T4 chunked eviction is working |
Eviction: nothing evictable this pass, backing off |
T4 hot-write floor is protecting in-flight upserts |
Emitted MEMORY_PRESSURE_HIGH |
T10 edge-triggered event fired |
Insert rejected: memory pressure emergency |
T7 admission control engaged |
Client::insert ...; retrying in Nms (attempt K/5) |
T11 client retry kicked in |
Successful inserts: N == attempts, Failed: 0 |
No writes lost under pressure |
Edit config.json:
hot_write_floor_ms to 500 ms so eviction can actually drop recent writeseviction_chunk_pause_ms to 200 msmemory_emergency_percent to 99max_memory_mb to 256 to see if the chunked approach still works at scaleload_test_mixed)Validates that reads stay responsive while writers + eviction are running. Three writer threads continuously insert 2 KB docs into a shared list of recent IDs; five reader threads pick random IDs from that list and call get(). Reads hit both live docs and evicted-stub IDs, exercising the WAL-fallback paging path.
g++ -std=c++20 -O2 -I/usr/include load_test_mixed.cpp \
-lsmartbotic-db-client $(pkg-config --libs grpc++) \
-lspdlog -lfmt -pthread -o load_test_mixed
# Same server setup as load_test:
mkdir -p /tmp/smartbotic-loadtest/data /tmp/smartbotic-loadtest/files
smartbotic-database --config ./config.json > /tmp/smartbotic-loadtest/server.log 2>&1 &
SERVER_PID=$!
sleep 2
./load_test_mixed [duration_sec=30] [writers=3] [readers=5] [doc_bytes=2048]
kill $SERVER_PID
not-found is counted separately from failures — it's a legitimate cold miss from a freshly-evicted stub where the WAL page hasn't been pulled yet, not a bug.
load_test_integrity)Writes 10,000 docs with deterministic makeContent(i) bodies (~1.5 KB each, content includes the id so misdirected reads also fail), waits 3 s for eviction to settle, then reads every doc back and compares byte-for-byte. Runs sequentially — no concurrency on verify, by design.
At 32 MB cap and ~15 MB of data plus overhead, eviction will fire; the verify phase exercises the WAL page-in path for every doc.
g++ -std=c++20 -O2 -I/usr/include load_test_integrity.cpp \
-lsmartbotic-db-client $(pkg-config --libs grpc++) \
-lspdlog -lfmt -pthread -o load_test_integrity
Uses the same config.json as the base test.
mkdir -p /tmp/smartbotic-loadtest/data /tmp/smartbotic-loadtest/files
smartbotic-database --config ./config.json > /tmp/smartbotic-loadtest/server.log 2>&1 &
SERVER_PID=$!
sleep 2
./load_test_integrity [num_docs=10000] [stabilize_sec=3]
kill $SERVER_PID
content field byte-exact matches makeContent(i)If the summary says no eviction stubs present, the budget is too loose for your dataset size — either bump num_docs or lower max_memory_mb in the config so eviction actually fires during the run.
load_test_pinned)Confirms that pinned=true collections are never evicted, even under sustained pressure on other collections. Needs a different server config because the settings collection is created pinned by a migration — createCollection via the client API doesn't expose pinned yet.
g++ -std=c++20 -O2 -I/usr/include load_test_pinned.cpp \
-lsmartbotic-db-client $(pkg-config --libs grpc++) \
-lspdlog -lfmt -pthread -o load_test_pinned
# 1. Set up data dir + migrations dir + copy the migration file.
mkdir -p /tmp/smartbotic-loadtest-pinned/{data,files,migrations}
cp migrations/001_create_pinned_settings.json /tmp/smartbotic-loadtest-pinned/migrations/
# 2. Start the server with the pinned-config. Migration auto-applies on startup.
smartbotic-database --config ./config_pinned.json > /tmp/smartbotic-loadtest-pinned/server.log 2>&1 &
SERVER_PID=$!
sleep 2
# 3. Run the test.
./load_test_pinned [duration_sec=30] [bulk_writers=4] [num_settings=200]
# 4. Stop.
kill $SERVER_PID
settings.documentCount == 200 (all pinned docs still present)settings.evictedStubCount == 0 (pinned docs never evicted)bulk.evictedStubCount > 0 (proves pressure actually fired — defensive sanity check)If bulk.evictedStubCount is 0, either the run was too short, or the bulk workload produced less than ~32 MB — raise duration_sec or bulk_writers.
run_crash_test.sh)The most complex scenario — starts a server, starts an inserter, waits until eviction has started firing, kill -9s both, restarts the server with --force-readwrite, and verifies every successfully-logged insert is still retrievable byte-exactly.
This is the one that validates v1.6.1 (snapshot durability + recovery) + v1.7.0 (eviction) working together under a hard failure mode.
cd tests/load_test
g++ -std=c++20 -O2 -I/usr/include load_test_crash_insert.cpp \
-lsmartbotic-db-client $(pkg-config --libs grpc++) \
-lspdlog -lfmt -pthread -o load_test_crash_insert
g++ -std=c++20 -O2 -I/usr/include load_test_crash_verify.cpp \
-lsmartbotic-db-client $(pkg-config --libs grpc++) \
-lspdlog -lfmt -pthread -o load_test_crash_verify
chmod +x run_crash_test.sh
./run_crash_test.sh
Exit code:
/tmp/smartbotic-loadtest-crash/, makes fresh dirsconfig_crash.json based on config.json with the crash data dirkill -9 the inserter + the server--force-readwrite (accepts auto-readonly non-trivial-recovery state)makeContent(i)/tmp/smartbotic-loadtest-crash/inserted.log is retrievableThe recovery system decided the snapshot+WAL combination was unrecoverable. run_crash_test.sh then dumps the last 30 lines of server2.log — look for RECOVERY REFUSED. Escalate manually:
smartbotic-database --config /tmp/smartbotic-loadtest-crash/config_crash.json \
--recovery-mode=snapshot_fallback --force-readwrite
run_replication_test.sh)Spawns two local server instances (leader on 9005, follower on 9006), each configured with the other as a replication peer, inserts 1000 docs on the leader, then polls the follower every 200 ms until it sees them. Reports catch-up time and content integrity.
This is a smoke test — it does NOT exercise replication at real-deployment scale. It verifies the replication mechanism still fires alongside v1.7.0's eviction / admission-control / client-retry changes. Because v1.7.0 didn't touch replication code, the expected outcome is "no regression" — if the mechanism breaks here, bisect toward something we did change (WAL ordering, store callbacks).
cd tests/load_test
g++ -std=c++20 -O2 -I/usr/include load_test_replication.cpp \
-lsmartbotic-db-client $(pkg-config --libs grpc++) \
-lspdlog -lfmt -pthread -o load_test_replication
chmod +x run_replication_test.sh
./run_replication_test.sh [num_docs=1000] [max_wait_sec=30]
config_replica_leader.json — port 9005, node_id replica-leader, replication enabled, peer = 127.0.0.1:9006, data dir /tmp/smartbotic-loadtest-replica-leader/dataconfig_replica_follower.json — port 9006, node_id replica-follower, replication enabled, peer = 127.0.0.1:9005, data dir /tmp/smartbotic-loadtest-replica-follower/dataBoth configs use symmetric multi-master peering (as the replication code supports) — each side lists the other in peers. Inserts are performed only on the leader in this test; the follower is a pure consumer, but could also accept writes.
max_wait_sec secondsExit codes:
0 — PASS (full catch-up, zero corruption)2 — PARTIAL (some docs replicated, no corruption, did not fully catch up in time)1 — FAIL (either nothing replicated OR content mismatches observed)A first run of this test against v1.7.0 produced FAIL — 200 docs replicated but zero content matches. The follower's copy of each document contained only the system fields (_id, _version, _created_at, _updated_at, etc.), with the user-supplied payload fields missing.
Root cause is in the leader-side broadcast code at service/src/database_service.cpp:515:
if (doc) entry.set_data(doc->data.dump());
This serializes only the document's data field. The follower-side apply code at service/src/database_service.cpp:612 expects the full Document envelope:
auto json = nlohmann::json::parse(entry.data());
Document doc = Document::fromJson(json); // looks for "data", "version", etc.
Document::fromJson reads j.value("data", {}) — i.e. it expects the payload nested under data, not at the top level. So the follower effectively discards the body.
git blame shows this mismatch predates v1.7.0 (commit a96c168, 2026-02-05). It is a latent bug in the push path: inserts/updates are getting replicated structurally (the system fields propagate) but with an empty body on the follower.
The pull path (GetEntriesSince → WAL) uses a different code path — it serializes the WAL entry's data field directly, so pull-based catch-up may or may not exhibit the same issue depending on WAL format. Not investigated here.
This test catches the bug but is deliberately not wired to fix it — the goal of Test 6 is v1.7.0 sanity, and the bug is unrelated to the eviction refactor. File a separate ticket to either:
doc->toJson().dump() instead of doc->data.dump(), ORentry.data() as the raw document body and construct the Document from entry's other fields (collection, document_id, node_id).Check both server logs for the peer handshake:
"Connected to peer 127.0.0.1:9006 (node: replica-follower, ...)"
"Connected to peer 127.0.0.1:9005 (node: replica-leader, ...)"
Without both lines, peering never established — likely a firewall or port conflict. ss -tlnp | grep 900[56] will confirm both are listening.
run_replica_eviction_test.sh)The follower has a deliberately tighter memory cap than the leader. The leader ingests enough data that the follower cannot hold it all in RAM, so the follower must:
RESOURCE_EXHAUSTED on the apply path — that would break sync forever)get(), paging evicted ones back in from the WALThis test catches bugs that only appear under the combined replicate + evict + page-in round trip:
replicate -> evict -> page-in cyclecd tests/load_test
g++ -std=c++20 -O2 -I/usr/include load_test_replica_eviction.cpp \
-lsmartbotic-db-client $(pkg-config --libs grpc++) \
-lspdlog -lfmt -pthread -o load_test_replica_eviction
chmod +x run_replica_eviction_test.sh
./run_replica_eviction_test.sh [num_docs=5000] [stabilize_sec=5]
config_replica_leader_128mb.json — port 9005, max_memory_mb: 128 (plenty of room), replication enabled, peer = 127.0.0.1:9006, data dir /tmp/smartbotic-loadtest-replica-eviction-leader/dataconfig_replica_follower_16mb.json — port 9006, max_memory_mb: 16 (tight — forces eviction), soft/hard/emergency = 50/70/90, replication enabled, peer = 127.0.0.1:9005, data dir /tmp/smartbotic-loadtest-replica-eviction-follower/datapad field) inserted on the leaderestimateDocumentSize) = ~25 MB on each sideevictedStubCount > 0 (proves eviction fired — if not, the test is INCONCLUSIVE, raise num_docs or lower follower cap)0 — PASS (all docs replicated + byte-exact, eviction fired on follower)1 — FAIL (missing docs or content corruption)2 — INCONCLUSIVE (eviction never fired — follower had room to spare)First run of this test against v1.7.1 produced FAIL — all 5000 docs replicated (follower shows 5000 evicted stubs), but zero readable on follower. Server log shows "Failed to recover evicted document replica_evict/repl-<i>" for every id.
Root cause is in DatabaseService::applyReplicatedEntry at service/src/database_service.cpp:623:
// Use loadDocument to bypass normal callbacks (avoid re-replication)
store_->loadDocument(entry.collection(), doc);
MemoryStore::loadDocument only inserts the doc into the in-memory map — it deliberately skips the usual callback chain so the follower does not re-broadcast. But that chain is also what appends the doc to the local WAL. So on the follower the doc lives only in RAM.
When eviction then fires on the follower, the doc is dropped to an evicted-stub. On read, the recovery callback calls persistence_->loadDocument() which scans the follower's WAL — and finds nothing, because replication never wrote it there. Result: permanent data loss the instant the follower evicts, even though the stub still reports the doc as "accounted for".
Fix options (not made in this changeset — the test documents the bug):
applyReplicatedEntry, after store_->loadDocument(...), append to the local WAL (e.g. persistence_->logInsert/logUpdate depending on op), so evicted-on-follower docs have a durable backing store.A v1.7.2 attempt added persistence_->logInsert(collection, doc) directly inside applyReplicatedEntry after the loadDocument call. The fix compiled cleanly but caused infinite replication echo amplification when run:
[23:30:14] Memory check: 1954 MB estimated (100%, pressure=emergency),
408121 docs, 30747 evicted
[23:30:14] Pulled 5000 entries from peer replica-leader-128
(seq: 211866 -> 216867)
The leader's WAL reached 216867+ entries after inserting only 5000. Root cause is at service/src/database_grpc_impl.cpp:1561 — the GetEntriesSince server handler hard-codes the response's node_id to its OWN nodeState.nodeId, not the entry's original origin. So when follower's WAL is streamed back to the leader, every entry claims to have originated on the follower. The leader then can't recognize its own original writes and re-applies them, re-logs to its WAL, re-streams them to the follower, and the cycle continues.
The proper fix requires:
nodeId field to struct WalEntry (service/src/persistence/wal.hpp:37) — WAL on-disk format change, breaks backward compat without a version bumppersistence_->logInsert/logUpdate/logDeletewalEntry.nodeId (not nodeState.nodeId) in GetEntriesSincenode_id equals the peer being sent toThis is a v1.8.0-class change that warrants its own design + format migration path. For v1.7.x the documented limitation stands: replicated docs on a follower that get evicted are lost. Size the follower's max_memory_mb to hold the working set until a proper replication overhaul ships.
Phase 2: waiting 5s for follower to stabilize...
[t+2000ms] follower pressure=emergency (100%) live=0 evicted=5000
...
Phase 3: reading all 5000 docs from follower...
not-found id=repl-0
not-found id=repl-1
...
Summary: Found: 0, Not found: 5000
Verdict: FAIL
And in the follower server log:
[warning] Failed to recover evicted document replica_evict/repl-4999
This confirms replication worked (5000 stubs exist on the follower and their IDs match what the leader inserted) — but the evicted-stub recovery path has nothing to page in from.