Rust Guide#
This document shows how to assemble and use the Rust client from the current workspace.
Before You Start#
Bring MooncakeCompatibilityFacade into scope. Most store operations are exposed through that trait.
use mooncake_store_client::MooncakeCompatibilityFacade;
You also need:
a metadata backend
a transport engine
a transport factory
a
StoreClientBuilder
Build a Local Client#
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
use mooncake_store_rs_metadata::{MetadataKeyspace, RedisMetadataBackend, RedisMetadataConfig};
use mooncake_store_client::{
LocalMemoryConfig, MooncakeCompatibilityFacade, StoreClientBuilder, TentTransportFactory,
};
use mooncake_store_core::{ClientLifecycleState, CompatibilityDescriptor, Result};
use mooncake_store_rs_transport::{TentEngine, TentEngineConfig};
fn now_ms() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("time should move forward")
.as_millis() as u64
}
fn main() -> Result<()> {
let metadata = Arc::new(RedisMetadataBackend::new(
RedisMetadataConfig::new("redis://127.0.0.1:6380/0")
.keyspace(MetadataKeyspace::new("mc/store-rs/demo")),
)?);
let tent_config = TentEngineConfig::new()
.set("metadata_type", "redis")
.set("metadata_servers", "127.0.0.1:6380")
.set("redis_db_index", "0")
.set("rpc_server_hostname", "127.0.0.1")
.set("rpc_server_port", "0")
.set("log_level", "warning")
.set("transports/tcp/enable", "true")
.set("transports/shm/enable", "false")
.set("transports/rdma/enable", "false")
.set("transports/io_uring/enable", "false");
let engine = Arc::new(TentEngine::new(
&tent_config.clone().set("local_segment_name", "demo-segment"),
)?);
let factory = Arc::new(TentTransportFactory::new(tent_config));
let client = StoreClientBuilder::new(metadata, "demo-store")
.state(ClientLifecycleState::Active)
.compatibility(CompatibilityDescriptor::default())
.tenant("default")
.route_topk(2)
.local_memory(
LocalMemoryConfig::new()
.storage_bytes(128 * 1024 * 1024)
.scratch_bytes(16 * 1024 * 1024)
.location("cpu:0"),
)
.with_tent(engine)
.transport_factory(factory)
.build(now_ms() + 600_000)?;
client.register_local_memory()?;
client.put("hello", b"world")?;
assert_eq!(client.get("hello")?, b"world");
Ok(())
}
rpc_server_port is the TENT TCP data-plane port. Leaving it at 0 lets TENT choose a random local port, which is fine for single-host demos. For cross-host or cross-container deployments, set a fixed port and make sure peers can reach rpc_server_hostname:rpc_server_port.
When a transport is supplied directly, its segment_name() must match any explicit builder segment_name(...). If the builder omits segment_name(...), the client adopts the transport segment name. This keeps the lease announcement, allocator metadata, and transport registration pointed at the same local segment.
tenant(...) remains the normal way to select the default scope used for startup policy lookup and request builders. Startup policy resolution reads exact tenant-policy scopes for tenant -> tenant/domain -> tenant/domain/object_set instead of listing all tenant policies. route_topk(...), namespace_quota(...), execution_fairness(...), and bandwidth_shaping(...) are compatibility fallbacks; admin-managed tenant policy in metadata is the preferred authoring surface when those settings are tenant-scoped. route_control(...) is a cluster-level deployment setting (not per-tenant) that determines whether EmbeddedWrh or MetadataOnly route storage is used.
When tenant quota policy is present, single-object put and remove now use metadata-backed reservation/finalize semantics instead of relying only on the older runtime-local preflight check. The write call returns success only after route publication and quota finalization both succeed. Overwrites are charged on committed byte delta, and deletes refund quota when the route delete CAS becomes authoritative rather than waiting for later segment reclaim.
Transport Backends#
The Rust transport layer exposes two explicit choices:
TentEngine+TentEngineConfigClassicTransferEngine+ClassicEngineConfig
This guide uses TENT for the concrete examples because it is the default repository path, but Rust applications can construct either backend directly.
Runtime backend selection is implemented by the compatibility layer, not by StoreClientBuilder itself:
MC_STORE_RS_TRANSPORT_BACKEND=tent|classic_temooncake-store-rs-client --transport-backend tent|classic-teMooncakeDistributedStore.setup(..., transport_backend="tent"|"classic_te")
For classic_te with P2PHANDSHAKE metadata, Store-RS separates the logical segment identity from
the transport identity. Segment announcements keep the configured logical segment_name, publish
the peer RPC address in transport_endpoint, and publish the full TE RAM descriptor in
transport_segment_descriptor. Remote clients preload that descriptor into classic TE before
calling openSegment, matching the descriptor shape TE normally obtains from Redis metadata.
Redis authentication can come from URL-embedded credentials or from MC_REDIS_USERNAME / MC_REDIS_PASSWORD. Prefer environment variables when passwords contain URL-reserved characters such as @.
Check Without Native Build#
The top-level Mooncake CMake project owns the classic-TE and TENT shim targets
and links them to its existing transfer_engine and tent_shared targets.
mooncake-store-rs-transport-sys provides Rust FFI for those shims. Its build
script validates MOONCAKE_ROOT_DIR, MOONCAKE_BUILD_DIR, and the explicit
MOONCAKE_CLASSIC_SHIM_LIB_PATH / MOONCAKE_TENT_SHIM_LIB_PATH artifacts; it
does not configure or compile CMake targets. The enclosing CMake build passes
these paths, or direct Cargo users must provide absolute paths explicitly.
The enclosing CMake target builds the required native targets before Rust:
cmake -S /path/to/Mooncake -B /path/to/Mooncake-build \
-DWITH_STORE_RS=ON -DWITH_TE=ON -DUSE_TENT=ON \
-DWITH_STORE=OFF -DWITH_STORE_RUST=OFF
cmake --build /path/to/Mooncake-build --target build_store_rs
Developer machines that only need Rust type analysis can skip native artifact validation:
MOONCAKE_SKIP_NATIVE_BUILD=1 cargo check
This is intended for IDE analysis. Runtime builds require an existing CMake build tree with the TE, TENT, yalantinglibs, and shim artifacts; the enclosing CMake targets build and install those dependencies before Cargo compiles Rust.
Enable Routed Writes#
Use PlacementPlanner and routed_writes(...) when a client should route writes to storage nodes.
use mooncake_store_client::PlacementPlanner;
let planner = PlacementPlanner::new(metadata.clone()).require_label("storage", "true");
let router = StoreClientBuilder::new(metadata, "router-a")
.state(ClientLifecycleState::Active)
.label("storage", "false")
.with_tent(engine)
.transport_factory(factory)
.local_memory(local_memory)
.routed_writes(planner, 2)
.build(now_ms() + 600_000)?;
In this mode:
placement candidates come from live compatible clients
candidates are filtered by planner labels
replica owners are ranked with rendezvous hashing
Use Request Policies#
ReplicationPolicy changes placement behavior for one request.
use mooncake_store_client::ReplicationPolicy;
let policy = ReplicationPolicy::new()
.replica_count(2)
.prefer_local(false)
.preferred_storage_owner("store-b")
.with_soft_pin(true);
client.put_with_policy("replicated", b"payload", &policy)?;
Useful knobs:
replica_count(...)prefer_local(...)prefer_alloc_in_same_node(...)preferred_segment(...)preferred_segments(...)preferred_storage_owner(...)preferred_storage_owners(...)with_soft_pin(...)
Use Batch and Buffer-Oriented APIs#
The Rust client supports both object-oriented and buffer-oriented paths.
Basic and batch APIs#
put,get,removebatch_put,batch_get,batch_removeis_exist,batch_is_exist,get_size
query_route, get_size, and successful reads are treated as access signals when they find active
routes. They report route hits to the storage owners best-effort through the same CLOCK hit-report
path used by reads, so storage-owner eviction protects recently observed replicas. is_exist and
batch_is_exist stay boolean probes and do not report hits.
Registered-buffer APIs#
register_bufferunregister_bufferput_frombatch_put_frombatch_get_into
Multi-buffer APIs#
batch_put_from_multi_buffersbatch_get_into_multi_buffers
Use the buffer-oriented forms when the application already owns stable memory and wants to avoid extra copies.
Behavior boundary:
put_fromandbatch_put_fromsend remote writes from the registered source buffer directlybatch_get_intoreads remote payloads directly into registered destination buffersregistered source buffers that are not host-readable, such as CUDA buffers, are used only as direct transfer sources; they cannot satisfy local-storage writes or staged scratch writes, and the published replica route omits the CPU payload checksum for those writes
routed
batch_put_fromtreats per-key route CAS conflicts as successful cache insert races when an active route is already published, releases its own temporary reservation, and returns the published route for that key; when the CAS response omits that active route, the client does one exact bounded recheck for the conflicted key before failingReplicaRoute::offsetis the transport target address for the payload;segment_offsetis allocator bookkeeping, so writers translate allocator reservations through the segment announcement’s published storage target chunks, and local reads plus drain migration translateoffsetback through the same storage target map before copying from local storageunregistered
get_intotargets andbatch_put_from_multi_buffersstill fall back to the staged copy paths
Hugepage-Backed Local Memory#
LocalMemoryConfig can request hugepage-backed local storage and scratch memory.
let local_memory = LocalMemoryConfig::new()
.storage_bytes(128 * 1024 * 1024)
.scratch_bytes(16 * 1024 * 1024)
.use_hugepage(true)
.hugepage_size_bytes(2 * 1024 * 1024);
Supported sizes:
2 MiB1 GiB
If these fields are not set explicitly, the runtime can also use MC_STORE_USE_HUGEPAGE and MC_STORE_HUGEPAGE_SIZE.
Multi-Tenant Access#
Use tenant-aware request builders when the default tenant is not enough.
Examples:
put_in_tenant(...)get_in_tenant(...)remove_in_tenant(...)query_route_in_tenant(...)
Batch request builders also accept ObjectRef::tenant(...).
Lifecycle and Elasticity#
The compatibility facade also exposes lifecycle and memory-management operations.
Common calls:
heartbeat(expires_at_ms)enter_standby()activate()enter_draining()plan_handoff(...)list_segments()expand_local_memory(storage_bytes)drain_segment(segment)retire_segment(segment)evacuate_owned_replicas()evacuate_owned_replicas_via(writer)
evacuate_owned_replicas() drains the local client, rewrites every live route that still references it, immediately reclaims old allocations, and retires emptied local segments.
During evacuation, payload copy is pinned to the exact local replica being removed. The writer does not satisfy the migration read from another live mirror, because that would publish the wrong bytes when replicas temporarily diverge during a shrink. The migration read uses the replica’s transport target offset, not the allocator reservation offset, and maps that target coordinate through the storage target map before touching local memory.
Before returning, the draining client also mirrors any route-authority records that were only present locally to active route authorities. If its cached live-client snapshot cannot find a mirror, it refreshes membership and retries the mirror pass.
Use evacuate_owned_replicas_via(writer) when you want a separate routed client to publish replacement routes during shrink.
These operations are the current control surface for hot-upgrade, elastic growth, segment drain/retire, and full client shrink flows.
Route Inspection#
Use these methods when debugging placement or route state:
query_route(...)cas_route(...)list_segments()runtime_id()lease()
The default route control mode is EmbeddedWrh. Use route_control(RouteControlMode::MetadataOnly) if you want metadata-backed route control for bring-up or debugging.
Use route_topk(...) when you want a different WRH route-authority fanout. The runtime defaults to 2, rejects values below 2, and validates the setting against the effective route policy already stored in the active metadata keyspace: tenant override for the builder’s default tenant first, otherwise the default cluster policy.
Observability#
Tracing and metrics are part of the Rust client crate.
use mooncake_store_client::{
init_tracing, render_prometheus_metrics, start_metrics_http_server,
};
init_tracing(Some("info"))?;
let addr = start_metrics_http_server("127.0.0.1:0")?;
println!("metrics: http://{addr}/metrics");
println!("{}", render_prometheus_metrics());
Key functions:
init_tracing(...)init_tracing_from_env(...)render_prometheus_metrics()start_metrics_http_server(...)stop_metrics_http_server()
The exported metrics now include strict tenant quota counters for reservation, finalize, abort, and reconcile outcomes, in addition to the existing request, route-CAS, replication, eviction, and process families. Sparse operational counters such as tenant quota, tenant-local eviction, preferred-segment skip, rebalance, and segment lifecycle are exported with zero-valued baseline series so dashboards show an explicit zero rate during steady state instead of No data.
Full Example#
The most complete runnable example is crates/mooncake-store-e2e/src/main.rs.
It covers:
local and routed writes
request-level replication
batch put/get
strict tenant quota on single-object and routed batch writes
strict tenant quota e2e validation for Redis-backed admit/finalize/reject/delete-refund state
registered-buffer and multi-buffer paths
delete reclaim and overwrite reclaim
dynamic expansion, true client shrink, and hot-upgrade
Benchmarking#
Use mooncake-store-rs-bench for throughput measurement, correctness verification, and long-duration stability testing. Its sources live under crates/mooncake-store-rs-cli/src/bin/mooncake-store-rs-bench/ in the Rust workspace.
Quick start:
# Correctness check
# Defaults use batch_put + batch_get; override interfaces when needed.
mooncake-store-rs-bench --metadata-url redis://127.0.0.1:6379/0 verify
# 30-second mixed benchmark, 8 concurrent workers
# Defaults measure batch_put + batch_get; override interfaces when needed.
# Start storage=true daemons first, and pass the same --keyspace they use
# when you are not using the default `mc/store-rs/v1` namespace.
mooncake-store-rs-bench --metadata-url redis://127.0.0.1:6379/0 bench \
--keyspace mc/store-rs/bench-prod \
--mode mixed --concurrency 8 --duration 30
# 1-hour soak test with Redis jitter fault injection
mooncake-store-rs-bench --metadata-url redis://127.0.0.1:6379/0 soak \
--duration 3600 --fault redis-jitter:5:50 --verify-reads
See the Store-RS benchmark guide for the full CLI reference.
Next Reading#
Configuration for defaults and knobs
Deployment for local and multi-role deployment
Architecture for runtime behavior