Mooncake Store Rolling Upgrade Guide#
Overview#
This document describes how to perform a rolling upgrade on a Mooncake Store cluster without downtime. The rolling upgrade leverages the predecessor-successor hot-upgrade mechanism to replace old versions (V1) with new versions (V2) one node at a time, keeping the cluster available throughout the process.
Prerequisites#
The new version only increments
store_api_minor_version(minor version) and does not changestore_api_version(major version)The changes in the new version are backward-compatible (e.g., adding fields, adding RPCs) and do not alter existing API semantics
Both versions were built from their own root CMake build trees using the Store-RS source-checkout quickstart
Compatibility Guarantees#
Scenario |
Behavior |
|---|---|
V1 node interoperates with V2 node |
✅ Compatible (same major version; minor is not checked) |
Data written by V2 is read by V1 |
✅ Compatible (new fields use serde default / protobuf ignores unknown fields) |
Data written by V1 is read by V2 |
✅ Compatible (missing fields use default values) |
Procedure#
Step 0: Preparation#
# Verify the Redis metadata backend is available
redis-cli -p 6380 ping
# Point each variable at its version's root CMake build tree.
export CLIENT_BIN_V1=/path/to/v1-build/mooncake-store-rs/cargo-cli/release/mooncake-store-rs-client
export CLIENT_BIN_V2=/path/to/v2-build/mooncake-store-rs/cargo-cli/release/mooncake-store-rs-client
# Verify the new binary and keep the predecessor available for rollback.
ls -la "$CLIENT_BIN_V2"
cp "$CLIENT_BIN_V1" "$CLIENT_BIN_V1.backup"
Step 1: Start the New Version Successor in Standby Mode#
Launch the new version binary with the same stable_id as the predecessor. The metadata backend allocates the next epoch automatically.
$CLIENT_BIN_V2 \
--local-hostname 127.0.0.1 \
--metadata-url "redis://127.0.0.1:6380/0" \
--storage-bytes 1048576 \
--scratch-bytes 1048576 \
--protocol tcp \
--route-control metadata-only \
--keyspace "${KEYSPACE}" \
--lease-ttl-ms 4000 \
--heartbeat-interval-ms 500 \
--label pool=pool-a \
--label storage=true \
--label route=false \
--drain-on-exit \
--stable-id client-a \
--initial-state standby \
--local-segment-name "seg-a-v2"
Key Parameters:
Parameter |
Description |
|---|---|
|
Must match the predecessor |
|
Starts in Standby mode, waiting for handoff |
|
Must differ from the predecessor’s segment name |
|
Ensures future upgrades can also trigger hot-upgrade |
Wait for the log to confirm successful startup (the assigned epoch is printed in the line):
mooncake-store-rs-client started stable_id=client-a epoch=2 initial_state=standby
If you need to observe the per-stable-id epoch high-water mark, query it directly:
redis-cli -p 6380 GET "${KEYSPACE}/state/client-epoch-hwm/client-a"
redis-cli -p 6380 PTTL "${KEYSPACE}/state/client-epoch-hwm/client-a"
The Redis high-water mark is lease-scoped metadata: heartbeats and lifecycle
state updates refresh its TTL with the client lease, including Draining
during hot upgrade. It disappears after the stable client has no live epoch
left.
Step 2: Trigger Hot-Upgrade on the Predecessor#
Send a SIGTERM signal to the old version predecessor:
# Find the predecessor's PID (match by stable_id and segment, not epoch, since
# the epoch is server-assigned)
pgrep -f "mooncake-store-rs-client.*--stable-id client-a"
# Send SIGTERM
kill -TERM <predecessor_pid>
Monitor the logs to confirm the hot-upgrade is complete:
Predecessor log (should appear in order):
mooncake-store-rs-client handoff stable_id=client-a from_epoch=1 to_runtime=client-a:2
mooncake-store-rs-client upgraded stable_id=client-a from_epoch=1 to_epoch=2
Successor log (should appear):
mooncake-store-rs-client promoted stable_id=client-a epoch=2 from_epoch=1 kind=HotUpgrade
Step 3: Verify Data Integrity#
import time
from mooncake.store import MooncakeDistributedStore
store = MooncakeDistributedStore()
assert store.setup(
"127.0.0.1", # local_hostname
"P2PHANDSHAKE", # transport_metadata_url (TE default; classic_te peer handshake)
4 * 1024 * 1024, # storage_bytes
1 * 1024 * 1024, # scratch_bytes
"tcp", # protocol
"", # rdma_devices
"redis://127.0.0.1:6380/0", # metadata_url (Store-RS metadata backend, required)
stable_id="verify-driver",
keyspace="<your-keyspace>", # replace with actual keyspace
labels={"pool": "pool-a", "storage": "false", "route": "false"},
route_control="metadata_only",
) == 0
# Wait for the route to converge to the new owner (successor's epoch)
for _ in range(100):
route = store.query_route("existing-key")
if route is not None:
owners = [r["owner"] for r in route["replicas"]]
if owners == ["client-a:2"]: # confirm owner switched to successor
break
time.sleep(0.1)
# Verify data written before the upgrade is still readable
assert store.get("existing-key") == b"expected-value"
# Verify new writes also work
assert store.put("new-key", b"new-value") == 0
assert store.get("new-key") == b"new-value"
store.close()
Step 4: Repeat Steps 1–3 for the Next Node#
Repeat Steps 1–3 for each node in the cluster until all nodes have been upgraded to the new version.
Rollback Procedure#
If the new version encounters issues, you can roll back using the same mechanism:
Start a successor using the old version binary with the same
stable_id; the metadata backend assigns the next epoch automaticallySend SIGTERM to the new version predecessor
The old version successor takes over
# Roll back client-a using the V1 binary — the successor gets the next epoch
# from the metadata backend (epoch=3 in a 1 -> 2 -> 3 rollout sequence).
$CLIENT_BIN_V1 \
--local-hostname 127.0.0.1 \
--metadata-url "redis://127.0.0.1:6380/0" \
--storage-bytes 1048576 \
--scratch-bytes 1048576 \
--protocol tcp \
--route-control metadata-only \
--keyspace "${KEYSPACE}" \
--lease-ttl-ms 4000 \
--heartbeat-interval-ms 500 \
--label pool=pool-a \
--label storage=true \
--label route=false \
--drain-on-exit \
--stable-id client-a \
--initial-state standby \
--local-segment-name "seg-a-v1-rollback"
# After the successor starts, send SIGTERM to the V2 predecessor
kill -TERM <v2_pid>
# Wait for the logs to confirm handoff completion; V1 resumes service
Important Notes#
Upgrade order does not matter: Nodes can be upgraded in any order; the cluster operates normally during the mixed-version period
Upgrade one node at a time: Avoid upgrading multiple nodes simultaneously; ensure each node’s handoff completes before proceeding to the next
Confirm handoff completion: After sending SIGTERM, wait for the predecessor log to show
upgradedand the successor log to showpromotedbefore moving onUnique segment names: Each successor must use a different
--local-segment-namethan its predecessorEpoch is server-assigned: The metadata backend allocates the next epoch on every startup; operators do not pick it
Minor version upgrades only: If the new version changes
store_api_version(major), rolling upgrade is not supported — a full cluster restart is required