Path sets that a `create` generator outputs to are now dynamic: created
nodes are added to the set on success and `remove_factor` removes take
nodes out of it (before sending; put back unless the outcome shows the
node is gone). The output set is the explicit `tag` from the create
generator's config, the `children_of` set of its fixed parent, or an
anonymous set when only `remove_factor` needs one; it is only populated
if some generator reads it. This allows pointing e.g. a `set` generator
at nodes produced by a `create` generator, and replaces the old
per-generator `paths_created` bookkeeping (along with the name
uniqueness enforcement, which never worked across threads anyway).
Dynamic sets are sharded by worker thread, so each thread reads and
updates only its own shard (shard mutexes still guard against response
callbacks running on client/commit threads). Since a dynamic set may lag
behind the real state, requests drawing paths from one report `ZNONODE`/
`ZNODEEXISTS` results as a separate `ignored_errors` counter instead of
failing, even without `continue_on_errors`. Generators whose input set
is currently empty decline, and another generator is picked instead.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`Stats::report` already returns early when `elapsed.elapsedSeconds()` is
zero, but `Stats::writeJSON` did not, causing `getThroughput(0)` to
produce infinity values that render invalid JSON.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two fixes:
1. Measure request latency in the response callback instead of when
`collect_request` drains the in-flight deque. Previously the
`Stopwatch` ran from submission until collection, which at high
`pipeline_depth` included time the request sat completed in the
deque waiting to be collected. Now a shared `Stopwatch` is captured
by the callback and read when the response actually arrives.
2. Compute RPS using wall-clock elapsed time (`Stopwatch`) instead of
deriving it from `sum(individual_latencies) / concurrency`. The old
formula did not account for pipelining: with `pipeline_depth=64` it
reported ~2k RPS while actual wall-clock throughput was ~140k RPS.
`getThroughput` now takes elapsed seconds directly, and the
`concurrency` parameter is removed from `report` and `writeJSON`.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Restore batched child removal in `removeRecursive` using `multi` requests
in chunks of 1000, reducing network round trips from O(N) to O(N/1000).
Add `std::lock_guard` in `Stats::report` and `Stats::writeJSON` so that
collector fields are consistently protected by `Stats::mutex` regardless
of caller context.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
`addOp` was modifying `op_collectors` without acquiring `Stats::mutex`,
unlike `addRead` and `addWrite` which both lock it. Currently safe
because callers hold `Runner::mutex`, but would race if used from the
log-replay path where only `Stats::mutex` is relied upon.
Also update the comment to reflect the actual invariant: access is
protected by `Stats::mutex`, not `Runner::mutex`.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fix two issues reported by Cursor Bugbot in `Stats::report` and
`requestFromLogExecutor`:
1. `Stats::report` called `getThroughput` unconditionally on both
collectors after only checking if both are zero. A log with only read
requests would hit `assert(requests != 0)` inside
`write_collector.getThroughput`. Now each collector's throughput is
computed only when its request count is non-zero.
2. `requestFromLogExecutor` only waited for the last request's promise.
When an executor processes requests across multiple ZooKeeper
connections (one per session_id), callbacks on earlier connections
could still be running on I/O threads after `last_request->wait()`
returned, causing a data race with `report`/`writeJSON` which read
`Stats` fields without `Stats::mutex`. Fixed by collecting all
per-request futures and waiting for all of them before returning.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- codespell flagged `Warmup` as a misspelling; added to ignore list
- clang-tidy `readability-make-member-function-const` on `getThroughput`
- Removed unused `<vector>` include from Stats.h
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add `pipeline_depth` and `queue_depth` settings to allow asynchronous
in-flight requests per consumer thread, hiding round-trip latency
- Add `warmup_seconds` to discard early stats while caches warm up
- Add `generator.seed` for reproducible workloads
- Track per-operation-type stats (Create, Remove, Get, Set, List, Multi)
in both stderr reports and JSON output
- Add `on_failure_callbacks` to clean up pending paths on failed creates
- Replace `paths_created` set with O(1) swap-and-pop vector+index structure
for random path removal
- Fix `static` → `thread_local` for RNG instances to avoid data races
- Fix `continue_on_errors` → `continue_on_error` (match actual config key)
- Validate `remove_factor` range, zero weights, and empty path getters
- Add retry limit for unique path name generation
- Simplify `removeRecursive` and deduplicate default ACL construction
- Document all new settings in README
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>