nmbrs-runtime
The workload execution runtime for nmbrs. It takes a parsed workload,
compiles its Polydat bindings into a tree of scope kernels, walks the
scenario tree, runs phases as activities of concurrent fibers, dispatches ops
through adapters and wrapper stacks, and records metrics into a session. It
also defines the adapter API (DriverAdapter / OpDispenser) that protocol
drivers implement. That API is the main reason to depend on this crate
directly.
Where it sits in nmbrs
- Depends on:
nmbrs-workloadfor the workload modelnmbrs-metricsfor components, instruments, controls and metrics storagenmbrs-ratefor rate limitingnmbrs-errorhandlerfor error routingpolydat, the data generation kernel
- Depended on by:
- the
nmbrsCLI nmbrs-tuiandnmbrs-webnmbrs-optimizers, with itsruntimefeature- the adapter crates:
nmbrs-adapter-stdout,nmbrs-adapter-http,nmbrs-adapter-cql,nmbrs-adapter-testkit,nmbrs-adapter-plotter
- the
End users normally install the nmbrs CLI
(cargo install nmbrs) and run workloads with nmbrs run. This crate is for
people who write adapters, or who embed the runner in their own binary.
Writing an adapter
Adapters live in nmbrs_runtime::adapter. An adapter has two phases:
- Init time.
DriverAdapter::map_opis called once per op template, before any cycle runs. It does the expensive work: validating fields, preparing statements, building binders. It returns a boxedOpDispenser. - Cycle time.
OpDispenser::execute(cycle, ctx)is called for every cycle. It resolves that cycle's values and performs the operation.
DriverAdapter
Implement this trait on your adapter type. It is constructed once per
activity and shared across fibers through an Arc.
| Method | Required | Purpose |
|---|---|---|
name(&self) -> &str |
yes | The adapter name, e.g. "http". |
map_op(&self, template: &ParsedOp, parent: Arc<dyn Kernel>) -> MapOpFuture |
yes | Build the dispenser for one op template. parent is the phase's Polydat scope kernel. |
known_op_fields() |
no | Return Some(&[...]) to declare your op-field vocabulary. The core then rejects templates with unknown fields. None (the default) is permissive. |
known_op_params() |
no | Extra keys allowed under an op's params. |
default_status_metrics() |
no | StatusMetrics shown on the status line. |
display_preference() |
no | DisplayPreference::Off if the adapter writes to the raw terminal. The default is Auto. |
declare_controls(parent) |
no | Declare adapter-level dynamic controls on a subcomponent of the activity's nmbrs_metrics component. |
shutdown() |
no | Async teardown when a shared adapter is released. |
accessor_payload() |
no | A type-erased handle that kernel nodes can look up through the resource scope. |
If map_op returns Err, construction stops before any cycle runs. If a
dispenser binds fields through a typed API, and not by plain text
substitution, it should build a Binder and verify it against parent
(adapter::verify_binders) inside map_op. Kernel, Binder,
BinderSlot, PortType and ExecCtx are re-exported from
nmbrs_runtime::adapter, so an adapter crate doesn't need a direct polydat
dependency.
OpDispenser
| Method | Required | Purpose |
|---|---|---|
execute(&self, cycle, ctx: &ExecCtx) -> Pin<Box<dyn Future<Output = Result<OpResult, ExecutionError>> + Send>> |
yes | Run one op. |
canonical_kernel() |
no | Return Some(&kernel) when the dispenser keeps the Polydat kernel it got from map_op. The executor builds per-fiber kernels from it. |
describe() / describe_resolved(wires) |
no | One-line views of the op, as a template and as sent. Used in error diagnostics. |
adapter_metrics() |
no | Extra (family, Labels, MetricValue) samples to include in metrics snapshots. |
status_counters() |
no | Cumulative (name, count) pairs for the status line. A leading _ marks a name as internal. |
rows_per_op() |
no | The cursor stride for batch ops. The default is 1. |
inner_dispenser() |
no | Wrappers must return their inner dispenser. Leaves keep the default None. |
At cycle time, ctx.wires (a wires::WireSource) is how a dispenser reads
bound values. It resolves names against the fiber's kernel for this
dispenser. wires::resolve_op_fields_via_wires renders a list of op fields:
- A field that is exactly
{name}keeps its typed value. - References embedded in text are substituted.
- Bare strings stay literal.
The result is a ResolvedFields, which has names, typed values,
get_str, get_value and strings().
Results and errors:
- Success is an
OpResult. Itsbodyis anOption<Box<dyn ResultBody>>.TextBodyandJsonBodyare provided. ImplementResultBodyfor native result types;to_json,element_countandbyte_countare used for captures and traversal metrics. - Failure is an
ExecutionError:ExecutionError::Op(AdapterError)is a failure of this op.ExecutionError::Adapter(AdapterError)means the connection or session is degraded.AdapterErrorhas three fields.error_nameis the name that the workload'serrors:rules match against.messageis shown to the user.retryable: truemarks anOperror that thetries:wrapper may re-run.
Registration
Adapters register at link time with
inventory, by submitting an
AdapterRegistration:
namesare the names users select withadapter=<name>.known_paramsare adapter params, used for CLI validation.display_preferencedecides TUI compatibility from the params.supported_controlsis a list ofcontrol_catalog::ControlDescdescriptors.createis an async factory from params to anArc<dyn DriverAdapter>.
There are two optional registrations:
DriverImpllets one adapter have several driver implementations. The user picks one with a selector param, such ascqldriver=.SharedDriverRegistrationlets phases with the sameResourceKeyshare one adapter instance through the resource pool.
A minimal adapter that renders its op fields as text:
// Cargo.toml: nmbrs-runtime = "0.3", nmbrs-workload = "0.3",
// inventory = "0.3", serde_json = "1"
use Future;
use Pin;
use Arc;
use ;
use resolve_op_fields_via_wires;
use ParsedOp;
;
submit!
Reference implementations in the repository:
- stdout
is the smallest real adapter. It also shows
SharedDriverRegistration. - testkit injects errors deterministically.
- http and
cql are
full adapters.
cqlusesDriverImplfor itsscyllaandcassandra-cppdrivers.
Running with your adapter
The published nmbrs binary only contains the adapters it was built with. To
use your own adapter, build a binary that links your adapter crate and calls
the runner. Link the crate explicitly (for example with extern crate), so
that its inventory::submit! registration is kept:
extern crate my_adapter; // force-link for inventory registration
async
my-bench run workload=my_workload.yaml adapter=echo cycles=10
runner::run accepts run or a bare key=value / workload-file argument
list. Other nmbrs subcommands (check, report, …) are implemented in
the nmbrs crate, not here.
Execution model
- Runner (
runner).run(args)andrun_with_observer(args, observer)run the whole pipeline: parameter handling, workload resolution and parsing, adapter creation, scenario execution, metrics and the session.run_executionsruns several executions in one shared session.concurrent::run_workload_headlessruns one workload and returns anExecutionOutcome. - Scope tree (
scope_tree,scope_kernel,scope,bindings). Workload, scenario and phase bindings compile into aScopeTreeofScopeKernels. Children are bound under their parent's kernel, and op templates' kernels are bound under the phase kernel they get inmap_op. - Scenario executor. It is internal. It walks the
ScenarioNodetree at run time and evaluatesfor_each/do_while/do_untilas it goes.scene_tree::SceneTreeis the view of that walk that renderers use, with iterations unrolled into per-iteration phase nodes. - Activities and fibers (
activity). Each phase runs as anActivity, configured byActivityConfig:concurrencyfibers (tokio tasks) execute stanzas.opseqmaps cycles to ops by ratio (bucket, interval or concat sequencing).- An optional activity-level
nmbrs_rate::RateLimitergates all fibers. - Stop conditions,
throttle:andtries:settings apply per activity.
- Wrappers (
wrappers,wrapper_registry,wrapper_resolver). Op fields such astries:,if:,delay:,poll:,rate:,metrics:anderrors:select wrapper dispensers. These compose around the adapter's dispenser in a fixed innermost-to-outermost order (seewrapper_resolver::DEFAULT_ORDER). Wrappers implementadapter::WrappingDispenserand exposeinner_dispenser(). - Error routing. The outermost op wrapper applies the op's
nmbrs_errorhandler::ErrorRouterpolicy to each terminal failure. - Observers (
observer).RunObserverreceives phase and op lifecycle events. Implementations:StderrObserver, the plain-text defaultconcurrent::HeadlessObserver, which collects an outcome with no displayTuiObserver, provided bynmbrs-tui
- Metrics. Each activity registers its instruments (
ActivityMetrics) on a component in thenmbrs_metricscomponent tree. Adapter samples fromOpDispenser::adapter_metricsare added to the same snapshots. Dynamic controls such asconcurrencyandrateare declared on the activity component. Each session writes its metrics to a SQLitemetrics.dbin its session directory (session).
Cargo features
| Feature | Default | Enables |
|---|---|---|
flamegraph |
no | The profiler=flamegraph mode: in-process CPU sampling with pprof. The SVG is rendered by the external inferno-flamegraph tool when it is on PATH. Without this feature, profiler=flamegraph logs a warning and does nothing. |
The nmbrs CLI forwards its own flamegraph feature to this one.
Links
- Repository: https://github.com/nosqlbench/nmbrs
- API docs: https://docs.rs/nmbrs-runtime
- Execution engine (SRD 29): https://github.com/nosqlbench/nmbrs/blob/main/docs/SRD/29_execution_engine.md
- Adapter interface (SRD 30): https://github.com/nosqlbench/nmbrs/blob/main/docs/SRD/30_adapter_interface.md
- Dispenser-owned Polydat context (SRD 68): https://github.com/nosqlbench/nmbrs/blob/main/docs/SRD/68_dispenser_owned_polydat_context.md
- Wrappers (SRD 32): https://github.com/nosqlbench/nmbrs/blob/main/docs/SRD/32_wrappers.md
- Driver resources and sharing (SRD 35): https://github.com/nosqlbench/nmbrs/blob/main/docs/SRD/35_driver_resources.md
License
Apache-2.0