diff --git a/README.md b/README.md
index e0da1235a3..5623408399 100644
--- a/README.md
+++ b/README.md
@@ -33,46 +33,61 @@ Under real workloads, Mooncakeβs innovative architecture enables Kimi to handl
π Updates
+
+
+- **Oct 10, 2026**: TensorRT-LLM merged a native [Mooncake Store connector](https://github.com/NVIDIA/TensorRT-LLM/pull/19171) into KVCMv2, enabling content-addressed KV cache pooling and prefix reuse across serving instances.
+- **Oct 6, 2026**: [TokenSpeed v0.1.1](https://github.com/lightseekorg/tokenspeed/releases/tag/v0.1.1) includes Mooncake Store as an L3 KV cache backend beneath its host cache, alongside Mooncake Transfer Engine support for PD and EPD disaggregation.
+- **Sept 17, 2026**: vLLM added [cross-encoder embedding reuse](https://github.com/vllm-project/vllm/pull/56242) through Mooncake Store, complementing its [encoder-cache transfer connector](https://github.com/vllm-project/vllm/pull/41567) for multimodal disaggregated serving. Both capabilities are included in [vLLM v0.31.0](https://github.com/vllm-project/vllm/releases/tag/v0.31.0).
+- **Aug 28, 2026**: LMDeploy merged [Mooncake Store support](https://github.com/InternLM/lmdeploy/pull/4903), enabling shared KV cache pooling and prefix reuse across PyTorch inference instances. Included in [LMDeploy v0.18.0](https://github.com/InternLM/lmdeploy/releases/tag/v0.18.0).
- **Aug 24, 2026**: [AgentX / InferenceX v3](https://newsletter.semianalysis.com/p/agentx-inferencexv3-does-cuda-moat) highlights Mooncake's contributions to agentic inference workloads, including ROCm wheels, CI, and packaging improvements. [Mooncake optimization details](https://inferencex.semianalysis.com/agentx/optimizations/mooncake).
-- **Aug 20, 2026**: Mooncake is integrated into [Miles](https://github.com/radixark/miles) as a rollout data-transfer backend for the fragmented, heterogeneous data moving between rollout and training in disaggregated RL. Blogs: [KVCache.AI](https://kvcache.ai/blog/mooncake-rl-rollout-data-transfer/), [lmsys](https://www.lmsys.org/blog/2026-08-20-miles-mooncake-rollout-data-transfer).
-- **Aug 17, 2026**: Mooncake is integrated into [Speculators](https://github.com/vllm-project/speculators) as a distributed backend for multi-node online training, efficiently moves hidden-state between vLLM inference workers and Speculators trainers through RDMA, eliminating the need for massive hidden-state storage in offline training. [Benchmark on GB300 NVL72](https://x.com/mgoin_/status/2072785822231728363).
-- **May 7, 2026**: π [vLLM officially features Mooncake Store](https://vllm.ai/blog/mooncake-store) β a deep dive into how Mooncake's distributed KVCache engine supercharges vLLM inference with high-throughput, memory-efficient, cross-instance KV cache sharing!
-- **Apr 29, 2026**: SGLang introduces [RDMA-based P2P weight transfer for large-scale distributed RL](https://lmsys.org/blog/2026-04-29-p2p-update/) using Mooncake TransferEngine, achieving 7x faster weight updates for the 1T-parameter Kimi-K2 model (53s β 7.2s) with zero-copy RDMA transfer across thousands of GPUs.
More
+- **Aug 20, 2026**: Mooncake is integrated into [Miles](https://github.com/radixark/miles) as a rollout data-transfer backend for the fragmented, heterogeneous data moving between rollout and training in disaggregated RL. Blogs: [KVCache.AI](https://kvcache.ai/blog/mooncake-rl-rollout-data-transfer/), [lmsys](https://www.lmsys.org/blog/2026-08-20-miles-mooncake-rollout-data-transfer).
+- **Aug 17, 2026**: Mooncake is integrated into [Speculators](https://github.com/vllm-project/speculators) as a distributed backend for multi-node online training, efficiently moves hidden-state between vLLM inference workers and Speculators trainers through RDMA, eliminating the need for massive hidden-state storage in offline training. [Benchmark on GB300 NVL72](https://x.com/mgoin_/status/2072785822231728363).
+- **Aug 5, 2026**: llm-d added a [Mooncake PD deployment guide](https://github.com/llm-d/llm-d/pull/1511), complementing its [Mooncake Store tiered offloading integration](https://github.com/llm-d/llm-d/pull/1826) for vLLM serving on Kubernetes.
+- **Aug 1, 2026**: Dynamo merged an [event-driven Mooncake shared-cache index](https://github.com/ai-dynamo/dynamo/pull/11239) for SGLang HiCache routing, using Mooncake object events to track reusable KV pages in the shared pool.
+- **Jul 24, 2026**: FlexKV added [Mooncake Store as a distributed REMOTE-tier backend](https://github.com/taco-project/FlexKV/pull/231), enabling content-addressed KV cache sharing alongside its existing Mooncake Transfer Engine integration.
+- **Jul 12, 2026**: TransferQueue added [GPUDirect RDMA support for its Mooncake Store backend](https://github.com/Ascend/TransferQueue/pull/131), enabling GPU clients to exchange RL tensors with the host-memory store through GDR.
+- **May 7, 2026**: π [vLLM officially features Mooncake Store](https://vllm.ai/blog/mooncake-store) β a deep dive into how Mooncake's distributed KVCache engine supercharges vLLM inference with high-throughput, memory-efficient, cross-instance KV cache sharing!
+- **Apr 29, 2026**: SGLang introduces [RDMA-based P2P weight transfer for large-scale distributed RL](https://lmsys.org/blog/2026-04-29-p2p-update/) using Mooncake TransferEngine, achieving 7x faster weight updates for the 1T-parameter Kimi-K2 model (53s β 7.2s) with zero-copy RDMA transfer across thousands of GPUs.
+- **Apr 3, 2026**: LMCache introduced a native [Mooncake Store L2 adapter for multiprocess mode](https://github.com/LMCache/LMCache/pull/2911), connecting the LMCache MP server to the distributed KV cache pool.
+- **Apr 1, 2026**: The [TENT technical report](https://arxiv.org/abs/2604.00368) introduces declarative data-movement orchestration for heterogeneous interconnects, with evaluations in SGLang HiCache and checkpoint-engine workloads.
- **Mar 19, 2026**: [TorchSpec: Speculative Decoding Training at Scale](https://pytorch.org/blog/torchspec-speculative-decoding-training-at-scale) is [open sourced](https://github.com/torchspec-project/TorchSpec), using Mooncake to decouple inference and training via efficient hidden states management.
+- **Mar 16, 2026**: FastDeploy added [Mooncake Store cache read/write support to its PD pipeline](https://github.com/PaddlePaddle/FastDeploy/pull/6768), complementing its [global KV cache pooling](https://github.com/PaddlePaddle/FastDeploy/blob/develop/docs/features/global_cache_pooling.md) across inference instances.
- **Mar 5, 2026**: [LightX2V](https://github.com/ModelTC/LightX2V/pull/893) now supports disaggregated deployment based on Mooncake, enabling encoder/transformer service decoupling with Mooncake Transfer Engine for high-performance cross-device and cross-machine data transfer. Details in [blog](https://light-ai.top/LightX2V-BLOG/posts/Disaggregation/).
+- **Mar 3, 2026**: verl added a [Mooncake checkpoint-engine backend](https://github.com/verl-project/verl/pull/5176) for peer-to-peer weight synchronization between training and rollout workers.
- **Feb 25, 2026**: [SGLang](https://github.com/sgl-project/sglang) merged [Encoder Global Cache Manager](https://github.com/sgl-project/sglang/pull/16137), introducing a Mooncake-powered global multimodal embedding cache that enables cross-instance sharing of ViT embeddings to avoid redundant GPU computation.
- **Feb 24, 2026**: [vLLM-Omni](https://docs.vllm.ai/projects/vllm-omni/en/latest/design/feature/disaggregated_inference/) introduces disaggregated inference connectors with support for both `MooncakeStoreConnector` and `MooncakeTransferEngineConnector` for multi-node omni-modality pipelines.
- **Feb 12, 2026**: [Mooncake Joins PyTorch Ecosystem](https://pytorch.org/blog/mooncake-joins-pytorch-ecosystem/) We are thrilled to announce that Mooncake has officially joined the PyTorch Ecosystem!
- **Jan 28, 2026**: [FlexKV](https://github.com/taco-project/FlexKV), a distributed KV store and cache system from Tencent and NVIDIA in collaboration with the community, now supports [distributed KVCache reuse](https://github.com/taco-project/FlexKV/blob/main/docs/dist_reuse/README_en.md) with the Mooncake Transfer Engine.
- - **Dec 27, 2025**: Collaboration with [ROLL](https://github.com/alibaba/ROLL)! Check out the paper [here](https://arxiv.org/abs/2512.22560).
- - **Dec 23, 2025**: SGLang introduces [Encode-Prefill-Decode (EPD) Disaggregation](https://lmsys.org/blog/2026-01-12-epd/) with Mooncake as a transfer backend. This integration allows decoupling compute-intensive multimodal encoders (e.g., Vision Transformers) from language model nodes, utilizing Mooncake's RDMA engine for zero-copy transfer of large multimodal embeddings.
- - **Dec 19, 2025**: Mooncake Transfer Engine has been [integrated into TensorRT LLM](https://github.com/NVIDIA/TensorRT-LLM/tree/main/cpp/tensorrt_llm/executor/cache_transmission/mooncake_utils) for KVCache transfer in PD-disaggregated inference.
- - **Dec 19, 2025**: Mooncake Transfer Engine has been directly integrated into vLLM v1 as a [KV Connector](https://docs.vllm.ai/en/latest/features/mooncake_connector_usage/) in PD-disaggregated setups.
- - **Nov 07, 2025**: [RBG + SGLang HiCache + Mooncake](https://github.com/sgl-project/rbg/blob/main/keps/74-mooncake-integration/README.md), a role-based out-of-the-box solution for cloud native deployment, which is elastic, scalable, and high-performance.
- - **Sept 18, 2025**: Mooncake Store empowers vLLM Ascend by serving as [the distributed KV cache pool backend](https://docs.vllm.ai/projects/ascend/zh-cn/main/user_guide/feature_guide/kv_pool.html).
- - **Sept 10, 2025**: SGLang officially supports Mooncake Store as a [hierarchical KV caching storage backend](https://lmsys.org/blog/2025-09-10-sglang-hicache/). The integration extends RadixAttention with multi-tier KV cache storage across device, host, and remote storage layers.
- - **Sept 10, 2025**: The official & high-performance version of Mooncake P2P Store is open-sourced as [checkpoint-engine](https://github.com/MoonshotAI/checkpoint-engine/). It has been successfully applied in K1.5 and K2 production training, updating Kimi-K2 model (1T parameters) across thousands of GPUs in ~20s.
- - **Aug 23, 2025**: [xLLM](https://github.com/jd-opensource/xllm) high-performance inference engine builds hybrid KV cache management based on Mooncake, supporting global KV cache management with intelligent offloading and prefetching.
- - **Aug 18, 2025**: vLLM-Ascend [integrates Mooncake Transfer Engine](https://docs.vllm.ai/projects/ascend/en/latest/developer_guide/feature_guide/disaggregated_prefill.html) for KV cache register and disaggregate prefill, enabling efficient distributed inference on Ascend NPUs.
- - **Jul 20, 2025**: Mooncake powers [the deployment of Kimi K2](https://lmsys.org/blog/2025-07-20-k2-large-scale-ep/) on 128 H200 GPUs with PD disaggregation and large-scale expert parallelism, achieving 224k tokens/sec prefill throughput and 288k tokens/sec decode throughput.
- - **Jun 20, 2025**: Mooncake becomes a PD disaggregation [backend](https://kvcache-ai.github.io/Mooncake/deployment/integrations/lmdeploy.html) for LMDeploy.
- - **May 9, 2025**: NIXL officially supports Mooncake Transfer Engine as [a backend plugin](https://github.com/ai-dynamo/nixl/blob/main/src/plugins/mooncake/README.md).
- - **May 8, 2025**: [Mooncake x LMCache](https://kvcache-ai.github.io/Mooncake/deployment/integrations/lmcache/index.html) unite to pioneer KVCache-centric LLM serving system.
- - **May 5, 2025**: Supported by Mooncake Team, SGLang release guidance to deploy DeepSeek with PD Disaggregation on 96 H100 GPUs.
- - **Apr 22, 2025**: LMCache officially supports Mooncake Store as a remote connector.
- - **Apr 10, 2025**: SGLang officially supports Mooncake Transfer Engine for disaggregated prefilling and KV cache transfer.
+- **Dec 27, 2025**: Collaboration with [ROLL](https://github.com/alibaba/ROLL)! Check out the paper [here](https://arxiv.org/abs/2512.22560).
+- **Dec 23, 2025**: SGLang introduces [Encode-Prefill-Decode (EPD) Disaggregation](https://lmsys.org/blog/2026-01-12-epd/) with Mooncake as a transfer backend. This integration allows decoupling compute-intensive multimodal encoders (e.g., Vision Transformers) from language model nodes, utilizing Mooncake's RDMA engine for zero-copy transfer of large multimodal embeddings.
+- **Dec 19, 2025**: Mooncake Transfer Engine has been [integrated into TensorRT LLM](https://github.com/NVIDIA/TensorRT-LLM/tree/main/cpp/tensorrt_llm/executor/cache_transmission/mooncake_utils) for KVCache transfer in PD-disaggregated inference.
+- **Dec 19, 2025**: Mooncake Transfer Engine has been directly integrated into vLLM v1 as a [KV Connector](https://docs.vllm.ai/en/latest/features/mooncake_connector_usage/) in PD-disaggregated setups.
+- **Nov 07, 2025**: [RBG + SGLang HiCache + Mooncake](https://github.com/sgl-project/rbg/blob/main/keps/74-mooncake-integration/README.md), a role-based out-of-the-box solution for cloud native deployment, which is elastic, scalable, and high-performance.
+- **Oct 15, 2025**: SGLang introduced [Mooncake EP and a collective communication backend](https://github.com/sgl-project/sglang/pull/10423) as the foundation for fault-tolerant Elastic Expert Parallel inference.
+- **Sept 18, 2025**: Mooncake Store empowers vLLM Ascend by serving as [the distributed KV cache pool backend](https://github.com/vllm-project/vllm-ascend/blob/main/docs/source/user_guide/feature_guide/kv_cache_pool_ascend_store.md).
+- **Sept 10, 2025**: SGLang officially supports Mooncake Store as a [hierarchical KV caching storage backend](https://lmsys.org/blog/2025-09-10-sglang-hicache/). The integration extends RadixAttention with multi-tier KV cache storage across device, host, and remote storage layers.
+- **Sept 10, 2025**: The official & high-performance version of Mooncake P2P Store is open-sourced as [checkpoint-engine](https://github.com/MoonshotAI/checkpoint-engine/). It has been successfully applied in K1.5 and K2 production training, updating Kimi-K2 model (1T parameters) across thousands of GPUs in ~20s.
+- **Aug 23, 2025**: [xLLM](https://github.com/jd-opensource/xllm) high-performance inference engine builds hybrid KV cache management based on Mooncake, supporting global KV cache management with intelligent offloading and prefetching.
+- **Aug 18, 2025**: vLLM-Ascend [integrates Mooncake Transfer Engine](https://github.com/vllm-project/vllm-ascend/blob/main/docs/source/tutorials/features/pd_disaggregation_mooncake_multi_node.md) for KV cache register and disaggregate prefill, enabling efficient distributed inference on Ascend NPUs.
+- **Jul 20, 2025**: Mooncake powers [the deployment of Kimi K2](https://lmsys.org/blog/2025-07-20-k2-large-scale-ep/) on 128 H200 GPUs with PD disaggregation and large-scale expert parallelism, achieving 224k tokens/sec prefill throughput and 288k tokens/sec decode throughput.
+- **Jun 20, 2025**: Mooncake becomes a PD disaggregation [backend](https://kvcache-ai.github.io/Mooncake/deployment/integrations/lmdeploy.html) for LMDeploy.
+- **May 9, 2025**: NIXL officially supports Mooncake Transfer Engine as [a backend plugin](https://github.com/ai-dynamo/nixl/blob/main/src/plugins/mooncake/README.md).
+- **May 8, 2025**: [Mooncake x LMCache](https://kvcache-ai.github.io/Mooncake/deployment/integrations/lmcache/index.html) unite to pioneer KVCache-centric LLM serving system.
+- **May 5, 2025**: Supported by Mooncake Team, SGLang release guidance to deploy DeepSeek with PD Disaggregation on 96 H100 GPUs.
+- **Apr 22, 2025**: LMCache officially supports Mooncake Store as a remote connector.
+- **Apr 10, 2025**: SGLang officially supports Mooncake Transfer Engine for disaggregated prefilling and KV cache transfer.
- **Mar 7, 2025**: We open-sourced the Mooncake Store, a distributed KVCache based on Transfer Engine. vLLM's xPyD disaggregated prefilling & decoding based on Mooncake Store will be released soon.
- - **Feb 25, 2025**: Mooncake receives the **Best Paper Award** at **FAST 2025**!
- - **Feb 21, 2025**: The updated traces used in our FAST'25 paper have been released.
- - **Dec 16, 2024**: vLLM officially supports Mooncake Transfer Engine for disaggregated prefilling and KV cache transfer.
+- **Feb 25, 2025**: Mooncake receives the **Best Paper Award** at **FAST 2025**!
+- **Feb 21, 2025**: The updated traces used in our FAST'25 paper have been released.
+- **Dec 16, 2024**: vLLM officially supports Mooncake Transfer Engine for disaggregated prefilling and KV cache transfer.
- **Nov 28, 2024**: We open-sourced the Transfer Engine, the central component of Mooncake. We also provide two demonstrations of Transfer Engine: a P2P Store and vLLM integration.
- **July 9, 2024**: We open-sourced the trace as a JSONL file.
- - **June 27, 2024**: We present a series of Chinese blogs with more discussions on zhihu 1, 2, 3, 4, 5, 6, 7.
- - **June 26, 2024**: Initial technical report release.
+- **June 27, 2024**: We present a series of Chinese blogs with more discussions on zhihu 1, 2, 3, 4, 5, 6, 7.
+- **June 26, 2024**: Initial technical report release.
@@ -193,6 +208,48 @@ Mooncake integrates with [vLLM](https://github.com/vllm-project/vllm) to acceler
+π Ecosystem Integrations
+
+Mooncake connects inference engines, cache middleware, RL systems, and deployment tools. The directory below links to upstream guides and implementations for each integration, checked on Oct 15, 2026.
+
+
+Integration directory
+
+**TE** = Transfer Engine; **Store** = Mooncake Store; **EP/PG** = expert-parallel and process-group backends. Capabilities depend on the upstream version and configuration; consult the linked guide or implementation when deploying.
+
+| Project | Mooncake components | Integration use cases | Guide or implementation |
+| --- | --- | --- | --- |
+| SGLang | TE, Store, EP/PG | PD/EPD transfer, HiCache, multimodal embedding caching, weight synchronization, and Elastic EP | [Integration guide](https://kvcache-ai.github.io/Mooncake/deployment/integrations/sglang/index.html), [Elastic EP](https://github.com/sgl-project/sglang/pull/10423) |
+| vLLM | TE, Store | PD transfer, shared KV pooling, encoder-cache transfer, and cross-encoder embedding reuse | [PD connector](https://docs.vllm.ai/en/latest/features/mooncake_connector_usage/), [Store connector](https://docs.vllm.ai/en/latest/features/mooncake_store_connector_usage/), [encoder connector](https://docs.vllm.ai/en/latest/features/mooncake_ec_connector_usage/) |
+| TokenSpeed | TE, Store | PD/EPD transfer and Store-backed L3 beneath the host KV cache | [Server configuration](https://github.com/lightseekorg/tokenspeed/blob/main/docs/configuration/server.md) |
+| TensorRT-LLM | TE, Store | PD transfer and shared KV pooling through the KVCMv2 Store connector merged into main | [TE implementation](https://github.com/NVIDIA/TensorRT-LLM/tree/main/cpp/tensorrt_llm/executor/cache_transmission/mooncake_utils), [Store connector](https://github.com/NVIDIA/TensorRT-LLM/pull/19171) |
+| LMDeploy | TE, Store | PD transfer and cross-instance KV prefix reuse | [TE guide](https://kvcache-ai.github.io/Mooncake/deployment/integrations/lmdeploy.html), [Store connector](https://github.com/InternLM/lmdeploy/pull/4903) |
+| vLLM-Ascend | TE, Store | NPU PD transfer and KV pooling through AscendStoreConnector | [PD deployment](https://github.com/vllm-project/vllm-ascend/blob/main/docs/source/tutorials/features/pd_disaggregation_mooncake_multi_node.md), [KV pool](https://github.com/vllm-project/vllm-ascend/blob/main/docs/source/user_guide/feature_guide/kv_cache_pool_ascend_store.md) |
+| FastDeploy | Store | Global KV pooling and cache reuse in PD serving | [Global cache pooling](https://github.com/PaddlePaddle/FastDeploy/blob/develop/docs/features/global_cache_pooling.md) |
+| xLLM | TE, Store | KV transfer and distributed cache management | [KV transfer backends](https://github.com/jd-opensource/xllm/tree/main/xllm/core/kv_cache/transfer) |
+| Chitu | TE | Multi-prefill/multi-decode KV transfer over RDMA | [PD disaggregation](https://github.com/thu-pacman/chitu/blob/public-main/chitu/distributed/pd_disaggregation/README.md) |
+| LMCache | TE, Store | Remote KV storage, a native MP L2 adapter, and P2P cache sharing | [Store guide](https://docs.lmcache.ai/kv_cache/storage_backends/mooncake.html), [MP adapter](https://docs.lmcache.ai/mp/l2_storage.html#mooncake-store-mooncake-store-native-connector), [P2P backend](https://github.com/LMCache/LMCache/pull/4182) |
+| FlexKV | TE, Store | Distributed cache reuse and a content-addressed REMOTE tier | [Distributed reuse](https://github.com/taco-project/FlexKV/blob/main/docs/dist_reuse/README_en.md), [Store backend](https://github.com/taco-project/FlexKV/pull/231) |
+| NIXL | TE | Mooncake transport plugin for NIXL data movement | [Plugin guide](https://github.com/ai-dynamo/nixl/blob/main/src/plugins/mooncake/README.md) |
+| Dynamo | Store via SGLang HiCache | Shared-pool awareness and event-driven KV cache routing | [HiCache integration](https://github.com/ai-dynamo/dynamo/blob/main/docs/fern/pages/developer-guide/knowledge-base/modular-components/backends/sglang/hicache.md) |
+| llm-d | TE, Store via vLLM | Kubernetes PD serving and tiered KV cache offloading | [PD integration](https://github.com/llm-d/llm-d/pull/1511), [Tiered prefix cache](https://github.com/llm-d/llm-d/tree/main/guides/tiered-prefix-cache) |
+| RBG | TE, Store via inference engines | Role-based orchestration of Store, PD transfer, and cache reuse | [Deployment examples](https://github.com/sgl-project/rbg/tree/main/examples/inference/ecosystem/mooncake) |
+| srt-slurm | TE, Store via inference engines | Slurm deployment and Mooncake service configuration | [Mooncake services](https://github.com/NVIDIA/srt-slurm/blob/main/docs/mooncake-kv-store.md) |
+| NeMo RL | Store via TransferQueue | Rollout-to-training data exchange with optional GDR client transfers | [Data plane](https://github.com/NVIDIA-NeMo/RL/blob/main/nemo_rl/data_plane/README.md), [GDR support](https://github.com/NVIDIA-NeMo/RL/pull/3501) |
+| TransferQueue | Store | RL tensor/object storage, GDR transfers, and SSD offloading | [Store client](https://github.com/Ascend/TransferQueue/blob/main/transfer_queue/storage/clients/mooncake_client.py), [GDR guide](https://github.com/Ascend/TransferQueue/blob/main/docs/storage_backends/mooncake_gdr.md) |
+| verl | TE | Checkpoint-engine backend for trainer-to-rollout weight synchronization | [Mooncake backend](https://github.com/verl-project/verl/blob/main/verl/checkpoint_engine/mooncake_checkpoint_engine.py) |
+| checkpoint-engine | TE | Peer-to-peer model-weight distribution and updates | [P2P backend](https://github.com/MoonshotAI/checkpoint-engine) |
+| Miles | Store | Rollout-to-training data transfer | [Rollout transfer guide](https://github.com/radixark/miles/blob/main/docs/advanced/mooncake-rollout-transfer.md) |
+| TorchSpec | Store | Hidden-state exchange between inference and draft-model training | [Training architecture](https://github.com/torchspec-project/TorchSpec) |
+| Speculators | Store | Hidden-state exchange for distributed online draft-model training | [Hidden-state connectors](https://github.com/vllm-project/speculators/tree/main/hs_connectors) |
+| vLLM-Omni | TE, Store | Data exchange between multimodal pipeline stages | [Omni connectors](https://github.com/vllm-project/vllm-omni/tree/main/vllm_omni/distributed/omni_connectors/connectors) |
+| LightX2V | TE | Encoder/transformer disaggregation for video generation | [Transfer backend](https://github.com/ModelTC/LightX2V/blob/main/lightx2v/disagg/mooncake.py) |
+| slime | TE via SGLang | PD-disaggregated rollout serving | [RL serving architecture](https://github.com/THUDM/slime/blob/main/docs/en/advanced/rl-systems.md) |
+
+The Dynamo entry describes SGLang HiCache shared-cache routing. Its separate [KVBM G4 Store adapter proposal](https://github.com/ai-dynamo/dynamo/pull/9786) is still unmerged. NeMo RL's GDR path changes client-side tensor transfers; queued objects remain in Mooncake-managed host memory.
+
+
+
π₯οΈ Supported Hardware
Mooncake supports hardware backends across accelerator vendors, cloud fabrics, and standard datacenter interconnects, as listed below. See the [supported protocols](https://kvcache-ai.github.io/Mooncake/getting_started/supported-protocols.html) and [Transfer Engine design docs](https://kvcache-ai.github.io/Mooncake/design/transfer-engine/index.html) for details.
diff --git a/docs/source/_static/custom.css b/docs/source/_static/custom.css
index 79bd2082b4..3d63e2798f 100644
--- a/docs/source/_static/custom.css
+++ b/docs/source/_static/custom.css
@@ -6,3 +6,31 @@
margin: 0;
}
}
+
+.latest-blogs .sd-card {
+ height: 100%;
+ border-radius: 0.75rem;
+ transition: border-color 0.15s ease, box-shadow 0.15s ease;
+}
+
+.latest-blogs .sd-card:hover {
+ border-color: var(--pst-color-primary);
+ box-shadow: 0 4px 14px color-mix(in srgb, var(--pst-color-text-base) 8%, transparent);
+}
+
+.latest-blogs .sd-card-img-top {
+ width: 100%;
+ height: 148px;
+ object-fit: cover;
+}
+
+.latest-blogs .sd-card-title {
+ font-size: 1rem;
+ line-height: 1.4;
+}
+
+.latest-blogs .blog-card-date p {
+ margin-bottom: 0;
+ color: var(--pst-color-text-muted);
+ font-size: 0.875rem;
+}
diff --git a/docs/source/api-reference/cpp/mooncake-store.md b/docs/source/api-reference/cpp/mooncake-store.md
index 49d9ca4ed8..a2bb65cf4d 100644
--- a/docs/source/api-reference/cpp/mooncake-store.md
+++ b/docs/source/api-reference/cpp/mooncake-store.md
@@ -64,7 +64,7 @@ struct ReplicateConfig {
};
```
-`dfs_replica_num` may currently be `0` or `1`. When it is `1`, `replica_num >= 1` is required, so DFS-only placement is not supported. DFS replicas currently support only the `default` tenant and require the master and client DFS backends to be configured with the same shared root and shard layout. See the {ref}`DFS deployment documentation ` for setup and lifecycle limitations. Native C++ clients must initialize a `DistributedStorageBackend` and attach it with `SetDfsStorageBackend()` before issuing DFS reads or writes. DFS descriptors are carried by each `PutStart`, `UpsertStart`, or query response; there is no client-side descriptor cache. The Python/RealClient setup path attaches the backend through `FileStorage`.
+`dfs_replica_num` may currently be `0` or `1`. DFS-only placement (`replica_num = 0`, `nof_replica_num = 0`, `dfs_replica_num = 1`) is supported; the DFS replica is then the object's only copy, so DFS eviction or removal deletes the object. DFS replicas currently support only the `default` tenant and require the master and client DFS backends to be configured with the same shared root and shard layout. See the {ref}`DFS deployment documentation ` for setup and lifecycle limitations. Native C++ clients must initialize a `DistributedStorageBackend` and attach it with `SetDfsStorageBackend()` before issuing DFS reads or writes. DFS descriptors are carried by each `PutStart`, `UpsertStart`, or query response; there is no client-side descriptor cache. The Python/RealClient setup path attaches the backend through `FileStorage`.
Soft pinning starts when the first replica becomes readable and has a fixed
lifetime: reads do not extend it. `PRESERVE` keeps the committed deadline on an
diff --git a/docs/source/api-reference/python/mooncake-store.md b/docs/source/api-reference/python/mooncake-store.md
index 42a1419937..2024ca1dca 100644
--- a/docs/source/api-reference/python/mooncake-store.md
+++ b/docs/source/api-reference/python/mooncake-store.md
@@ -30,7 +30,7 @@ imported and accepts cpp or rs.
This page describes the shared facade and the default C++ backend. See the
[Store-RS Python API](store-rs.md) for backend-specific runtime behavior, API
coverage, and compatibility notes. The
-[source-checkout quickstart](../../getting_started/store-rs.md) shows how to
+{ref}`Build with Store-RS ` section shows how to
build the root wheel with Store-RS enabled.
## Quick Start
@@ -667,9 +667,10 @@ config.nof_replica_num = 1
**Default:** `0`
**Status:** **Work in progress; development and evaluation only.**
**Description:** Requests an additional replica in the configured shared
-distributed filesystem. The supported values are currently `0` and `1`. When
-set to `1`, `replica_num` must be at least `1`, so DFS-only placement is not
-supported. DFS replicas currently support only the `default` tenant.
+distributed filesystem. The supported values are currently `0` and `1`.
+DFS-only placement (`replica_num = 0` with `dfs_replica_num = 1`) is supported;
+the DFS replica is then the object's only copy, so DFS eviction or removal
+deletes the object. DFS replicas currently support only the `default` tenant.
```python
config = ReplicateConfig()
@@ -744,6 +745,40 @@ config = ReplicateConfig()
config.prefer_alloc_in_same_node = True
```
+#### session_ids
+**Type:** `Optional[List[str]]`
+**Default:** `None`
+
+Optional per-key write attribution. Supply one entry for each input key (one
+entry for a single-key write). Each ID must be valid UTF-8 and at most 256 bytes.
+An empty string means no new attribution. The field does not change object identity,
+replication, group membership, leases, or eviction policy.
+
+```python
+config = ReplicateConfig()
+config.session_ids = ["session-a", "session-a", "session-b"]
+store.batch_put_from(keys, buffer_ptrs, buffer_sizes, config)
+```
+
+The tag becomes committed when the first replica of the write becomes readable.
+A skipped duplicate Put leaves the original tag unchanged. Upsert with a
+non-empty ID replaces attribution at commit; an omitted or empty ID preserves
+the previous tag, including when storage is reallocated. Failed/preempted
+writes do not commit their pending tags. Committed tags survive metadata
+OpLog/snapshot recovery and standby promotion; old metadata defaults to no tag.
+
+With KV events enabled, each `stored` / `BlockStored` map carries a single
+`session_id` string, or MessagePack `nil` when untagged. Replica availability
+changes reuse the committed object tag. `removed` and `cleared` keep their
+existing schemas: they retract availability, not a particular session's
+association. This matches SGLang's single-session stored-event attribution;
+it does not enumerate every session that has reused a shared object.
+
+Old RPC readers can ignore the compatible extension, but do not persist or
+publish session tags. Upgrade the Store client and Master to enable attribution.
+A storage node's disk scan alone cannot reconstruct metadata tags after all
+Master metadata is lost; preserve the metadata recovery artifacts as well.
+
#### group_ids
**Type:** `List[str] | None`
**Default:** `None`
@@ -1619,19 +1654,51 @@ else:
---
#### batch_probe_key()
-Point-in-time existence check for multiple objects in a single batch
-operation, granting no read leases.
+Probe multiple objects, optionally selecting and leasing the last complete
+candidate. The default `"none"` policy remains a lease-free existence check.
```python
-def batch_probe_key(self, keys: List[str]) -> List[int]
+def batch_probe_key(
+ self, keys: List[str], policy: str = "none", candidate_size: int = 1
+) -> List[int]
```
**Parameters:**
- `keys` (List[str]): List of object identifiers to check
-
-**Returns:**
-- `List[int]`: List of existence results (1=existed at probe time,
-0=not exists, -1=error)
+- `policy` (str): `"none"` or `"LastHitOnly"`.
+- `candidate_size` (int): Number of consecutive keys forming each candidate.
+ Ignored for `"none"`. For a nonempty `"LastHitOnly"` request, it must be
+ positive and divide the number of keys exactly. Empty input returns `[]`.
+
+**Returns:**
+- Both policies return per-key existence results in the same length and order
+ as `keys`: 1=readable when checked, 0=missing or unreadable. Negative values
+ indicate errors, not cache misses.
+- With `"LastHitOnly"`, the last group of `candidate_size` all-1 results is
+ the selected, leased candidate. Earlier complete groups and incomplete
+ groups retain their existence results. If no group is all-1, no complete
+ candidate was selected.
+
+Candidates are ordered by the caller. A candidate is complete when every key
+has a readable replica. Earlier candidates may be missing or incomplete.
+The caller must include all required components,
+cache groups, and rank shards; Store does not infer prefix or model semantics.
+Duplicate key positions are allowed and must not alter the grouping.
+
+The Master first probes without leases, then rechecks and leases complete
+candidates in reverse order using the existing per-shard lookup. Recheck
+results replace that candidate's initial results; a candidate that loses a
+member cannot remain all-1. A failed candidate may retain partial leases
+while selection falls back to an earlier candidate. This is not an atomic
+snapshot of all keys, and unselected existence results are not lease guarantees.
+
+Selected objects receive the Master's default read-lease TTL, protecting them
+from normal eviction while the leases remain valid; soft-pin deadlines are
+unchanged. The policy does not protect against lease expiry, forced removal,
+or replica loss. Existing shared group leases (or a physical key repeated
+outside the selected candidate) may also protect unselected positions. No
+persistent group registration is added. Invalid candidate sizes return
+per-key parameter errors; unknown policy strings raise `ValueError` in Python.
**Example:**
```python
@@ -1641,6 +1708,29 @@ candidates = [key for key, exists in zip(keys, results) if exists == 1]
print("Probed candidates (unprotected from eviction):", candidates)
```
+For checkpoint selection, first use `batch_is_exist()` to protect the required
+KV data and determine its usable boundary. Pass only valid checkpoint
+candidates within that boundary, in increasing resume-position order:
+
+```python
+keys = ["checkpoint128.rank0", "checkpoint128.rank1",
+ "checkpoint256.rank0", "checkpoint256.rank1"]
+results = store.batch_probe_key(keys, policy="LastHitOnly", candidate_size=2)
+if any(result < 0 for result in results):
+ raise RuntimeError("Checkpoint lookup failed")
+selected = next((i for i in reversed(range(len(keys) // 2))
+ if all(result == 1 for result in results[i * 2:(i + 1) * 2])), None)
+# Both complete: [1, 1, 1, 1] selects 1.
+# Last incomplete: [1, 1, 1, 0] selects 0.
+# Load only the selected checkpoint, with the KV prefix it needs.
+```
+
+**Upgrade note:** the policy changes the `BatchProbeKey` Master RPC and the
+DummyClient-to-RealClient batch-probe RPC signatures. Upgrade the Master,
+client daemon, and clients together; mixed-version batch-probe calls are not
+supported. Default Python arguments preserve the old call syntax, not wire
+compatibility. `is_exist()`, `batch_is_exist()`, and `probe_key()` are unchanged.
+
---
#### get_size()
diff --git a/docs/source/api-reference/python/store-rs.md b/docs/source/api-reference/python/store-rs.md
index b16cc170a5..56aeb1009c 100644
--- a/docs/source/api-reference/python/store-rs.md
+++ b/docs/source/api-reference/python/store-rs.md
@@ -4,7 +4,7 @@ The Python Store facade selects its implementation when mooncake.store is
first imported. The shared API and default C++ backend are documented in
[Mooncake Store Python API](mooncake-store.md). This page describes Store-RS
runtime and API behavior. See the
-[source-checkout quickstart](../../getting_started/store-rs.md) to build and
+{ref}`Build with Store-RS ` section to build and
install the unified root wheel.
## What the Python Layer Provides
diff --git a/docs/source/deployment/mooncake-store-deployment-guide.md b/docs/source/deployment/mooncake-store-deployment-guide.md
index 004d50d5eb..d3fde8da15 100644
--- a/docs/source/deployment/mooncake-store-deployment-guide.md
+++ b/docs/source/deployment/mooncake-store-deployment-guide.md
@@ -361,7 +361,7 @@ With `ha_backend_type=etcd`, Mooncake Store supports a Primary-Standby HA model
HA leadership and metadata replication are configured separately:
- The HA coordinator elects the active master. Configure it with `--enable_ha`, `--ha_backend_type`, `--ha_backend_connstring`, and `--cluster_id`. For `ha_backend_type=etcd`, legacy `--etcd_endpoints` is used only when `--ha_backend_connstring` is empty.
-- The optional batch-record OpLog records metadata mutations so standby masters can catch up and later be promoted. It is the etcd Primary-Standby path. Enable it explicitly with `--enable_oplog=true`; it is disabled by default. `ha_backend_type=etcd` requires a build with `STORE_USE_ETCD`. On etcd, acknowledged batches form the durable prefix used for promotion. `ha_backend_type=redis` requires a build with `STORE_USE_REDIS` and is an experimental HA log storage method. A successful Redis write only means Redis accepted the record. Do not treat Redis persistence, including AOF `appendfsync everysec`, as a strict durability guarantee.
+- The optional batch-record OpLog records metadata mutations so standby masters can catch up and later be promoted. It is the etcd Primary-Standby path. Enable it explicitly with `--enable_oplog=true`; it is disabled by default. `ha_backend_type=etcd` requires a build with `STORE_USE_ETCD`. On etcd, acknowledged batches form the durable prefix used for promotion. `ha_backend_type=redis` requires a build with `STORE_USE_REDIS` and is an experimental HA log storage method. A successful Redis write only means Redis accepted the record. Do not treat Redis persistence, including AOF `appendfsync everysec`, as a strict durability guarantee.
- The optional standby-generated batch OpLog snapshot path is enabled with `--enable_oplog_snapshot=true` together with `--enable_oplog=true`. It uses the batch snapshot provider/coordinator and does not use the legacy catalog snapshot manager. Startup fails when the required etcd, cluster ID, object-store, or chunk configuration is invalid; a temporary upload failure leaves OpLog apply running for a later attempt.
@@ -745,6 +745,7 @@ mooncake_master \
| `--ha_backend_connstring` | empty | HA backend connection string |
| `--etcd_endpoints` | empty | Backward-compatible etcd HA endpoints, used only for `ha_backend_type=etcd` when `--ha_backend_connstring` is empty |
| `--cluster_id` | `mooncake_cluster` | Cluster ID for HA persistence |
+| `--ha_leader_lease_ttl_sec` | `5` | Leadership lease TTL in seconds for `ha_backend_type=etcd` (range `2`-`60`; the JSON config key `ha_leader_lease_ttl_sec` is also accepted). Raise it to ride out etcd apply stalls longer than the default. A longer TTL delays failover: a new leader waits one full TTL before serving, and a standby waits for a crashed leader's lease to expire. Ignored by the Redis and k8s backends |
| `--enable_oplog` | `false` | Enable the batch OpLog writer and reader; requires `enable_ha=true` and `ha_backend_type=etcd` or `redis`. etcd uses it for Primary-Standby promotion. Redis uses it as an experimental HA log store: a successful write only means Redis accepted the record |
| `--enable_oplog_snapshot` | `false` | Enable standby-generated batch OpLog snapshots; requires batch OpLog, HA/etcd, valid object-store configuration, and persistent local snapshot storage when applicable |
| `--snapshot_chunk_object_count` | `1000000` | Maximum objects per batch OpLog snapshot chunk; must be positive when the new snapshot path is enabled |
@@ -998,9 +999,9 @@ config.dfs_replica_num = 1
store.put("key", b"value", config)
```
-`dfs_replica_num` may currently be `0` or `1`. A DFS replica must be requested
-with at least one memory replica (`replica_num >= 1`), so DFS-only placement is
-not supported.
+`dfs_replica_num` may currently be `0` or `1`. DFS-only placement is supported
+by setting `replica_num = 0` and `nof_replica_num = 0`; the object then has no
+memory replica, and DFS eviction or removal deletes it.
Allocation first tries the key's hash-selected DFS shard, then tries other
ready shards if that shard has no suitable extent. This lets new shards accept
@@ -1022,7 +1023,9 @@ failed key is revoked without downgrading successful keys.
For a same-size `Upsert`, if either the existing object or the new request has
a DFS replica, the requested memory, NoF, and DFS replica counts must match the
existing topology. A different-size update releases the old placement and
-allocates a new topology.
+allocates a new topology. For example, after the memory replica of a
+memory-plus-DFS object is evicted, a same-size `Upsert` must request the
+DFS-only topology.
On reads, the master returns the readable replica list through the normal query
path, and the client selects the first complete replica. If it selects DFS, any
@@ -1032,8 +1035,7 @@ reads for that descriptor.
#### Current limitations
- Only the `default` tenant is supported.
-- `dfs_replica_num` must be `0` or `1`, and `replica_num >= 1` is required when
- it is enabled.
+- `dfs_replica_num` must be `0` or `1`.
- C and Rust clients cannot currently request or access descriptor-based DFS:
their replication configuration does not expose `dfs_replica_num`, and their
setup API cannot initialize the distributed `FileStorage` backend. Use the
diff --git a/docs/source/deployment/store-rs/index.md b/docs/source/deployment/store-rs/index.md
index 90dfec28ff..c35a4f4541 100644
--- a/docs/source/deployment/store-rs/index.md
+++ b/docs/source/deployment/store-rs/index.md
@@ -2,7 +2,7 @@
Use these guides to build the optional Store-RS component, configure a runtime,
operate tenant policy and routes, and validate deployment scenarios. The
-[source-checkout quickstart](../../getting_started/store-rs.md) starts with a
+{ref}`Build with Store-RS ` section starts with a
root CMake build and the unified Python wheel.
| Guide | Use it to |
diff --git a/docs/source/deployment/store-rs/python.md b/docs/source/deployment/store-rs/python.md
index 334a48009e..242eb45789 100644
--- a/docs/source/deployment/store-rs/python.md
+++ b/docs/source/deployment/store-rs/python.md
@@ -1,11 +1,11 @@
# Store-RS Python Operations
The root wheel installs the standalone Store-RS commands. Build and install it
-using the [source-checkout quickstart](../../getting_started/store-rs.md).
+using {ref}`Build with Store-RS `.
## Standalone Store-RS Commands
-The root CMake target build_store_rs_cli builds the client, admin, and benchmark binaries. The unified root wheel installs the standalone commands under their mooncake-store-rs-* names. Use the [source-checkout quickstart](../../getting_started/store-rs.md) to configure the root build and install the wheel.
+The root CMake target build_store_rs_cli builds the client, admin, and benchmark binaries. The unified root wheel installs the standalone commands under their mooncake-store-rs-* names. Use {ref}`Build with Store-RS ` to configure the root build and install the wheel.
Start a storage client:
diff --git a/docs/source/deployment/store-rs/rolling-upgrade.md b/docs/source/deployment/store-rs/rolling-upgrade.md
index e6984a8023..946cc43b15 100644
--- a/docs/source/deployment/store-rs/rolling-upgrade.md
+++ b/docs/source/deployment/store-rs/rolling-upgrade.md
@@ -8,7 +8,7 @@ This document describes how to perform a rolling upgrade on a Mooncake Store clu
- The new version only increments `store_api_minor_version` (minor version) and does **not** change `store_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](../../getting_started/store-rs.md)
+- Both versions were built from their own root CMake build trees using {ref}`Build with Store-RS `
### Compatibility Guarantees
diff --git a/docs/source/deployment/store-rs/route-migration.md b/docs/source/deployment/store-rs/route-migration.md
index 74c5dd5835..b15902a505 100644
--- a/docs/source/deployment/store-rs/route-migration.md
+++ b/docs/source/deployment/store-rs/route-migration.md
@@ -202,6 +202,6 @@ What this feature is not:
## 9. Related Docs
- [Python Guide](../../api-reference/python/store-rs.md)
-- [Features](../../getting_started/store-rs-features.md)
+- [Features](../../design/store/store-rs/features.md)
- [Configuration Reference](configuration.md)
- [Architecture](../../design/store/store-rs/architecture.md)
diff --git a/docs/source/design/store/ssd-offload.md b/docs/source/design/store/ssd-offload.md
index adc952b8f4..06c4b7bc43 100644
--- a/docs/source/design/store/ssd-offload.md
+++ b/docs/source/design/store/ssd-offload.md
@@ -144,6 +144,10 @@ Bucket IDs are monotonically increasing timestamps with a sequence suffix, so `b
**Grouping strategy** (`GroupOffloadingKeysByBucket`): objects are accumulated into a bucket until either `bucket_size_limit` (default 256 MB) or `bucket_keys_limit` (default 500) is reached. Objects that do not fill a complete bucket are held in `ungrouped_offloading_objects_` and retried on the next heartbeat.
+Set `MOONCAKE_OFFLOAD_FLUSH_PARTIAL_BUCKETS=1` before starting a client to write its final partial bucket in the same offload cycle. The default is `0`, preserving the existing grouping policy. The setting is read when the bucket backend configuration is created; an existing backend does not change when the environment changes.
+
+This opt-in policy avoids the deferred-task loss described in [issue #3006](https://github.com/kvcache-ai/Mooncake/issues/3006): carrying only keys and sizes across heartbeats does not preserve their offload tasks. Flushing the tail keeps eligible objects in the cycle that owns those tasks. It does not change completion notification, write-failure handling, or the on-disk format. Smaller buckets can increase file/metadata overhead and reduce write efficiency; this policy does not guarantee a disk replica if an offload fails. An alternative is to retain the tasks across cycles while waiting for full buckets, as proposed in [PR #3009](https://github.com/kvcache-ai/Mooncake/pull/3009).
+
**In-flight read tracking**: A `BucketReadGuard` RAII object increments `BucketMetadata::inflight_reads_` on construction and decrements it on destruction. This allows safe deletion of bucket files even when concurrent reads are in progress.
**Ownership contract (`storage_path`)**: LOCAL_DISK allows **one live client per `storage_path`**. `BucketStorageBackend::Init()` opens `.mooncake_local_disk.lock` under that directory and takes a non-blocking exclusive `flock`; a second live client fails Init immediately with an actionable error (`storage_path already held by another live client β¦ use standalone store or per-client directories`). Do not delete the lock file while a client is running β that would release the flock for other processes. `flock` is only meaningful on local filesystems (NFS and similar may not honor exclusive locks the same way).
diff --git a/docs/source/design/store/store-rs/components.md b/docs/source/design/store/store-rs/components.md
index 022f304781..a4d441288b 100644
--- a/docs/source/design/store/store-rs/components.md
+++ b/docs/source/design/store/store-rs/components.md
@@ -231,11 +231,11 @@ See the [benchmark guide](../../../performance/mooncake/store-rs-benchmark.md) f
## Suggested Reading Order
-1. [Source-checkout quickstart](../../../getting_started/store-rs.md)
+1. {ref}`Build with Store-RS `
2. [Deployment](../../../deployment/store-rs/deployment.md)
3. [Rust API](../../../api-reference/rust/store-rs.md)
4. [Configuration](../../../deployment/store-rs/configuration.md)
5. `crates/mooncake-store-core`
-6. [Features](../../../getting_started/store-rs-features.md)
+6. [Features](features.md)
7. [Architecture](architecture.md)
8. [Test and validation guide](../../../deployment/store-rs/testing.md)
diff --git a/docs/source/getting_started/store-rs-features.md b/docs/source/design/store/store-rs/features.md
similarity index 99%
rename from docs/source/getting_started/store-rs-features.md
rename to docs/source/design/store/store-rs/features.md
index c085ad3a0a..ac49836db7 100644
--- a/docs/source/getting_started/store-rs-features.md
+++ b/docs/source/design/store/store-rs/features.md
@@ -149,7 +149,7 @@ For simpler bring-up or debugging, the runtime can store and resolve routes dire
The current implementation also exposes admin-driven explicit route migration tasks for key-level `copy` and `move`.
-See [Route Migration Guide](../deployment/store-rs/route-migration.md) for the current
+See [Route Migration Guide](../../../deployment/store-rs/route-migration.md) for the current
operator workflow, task semantics, and request examples.
What it provides:
@@ -507,7 +507,7 @@ Both paths are covered by repository validation scripts.
The root validation entry point is `scripts/ci/run_store_rs_smoke.sh`. It
covers the installed Python facade and the dummy and routed TCP/classic-TE
-read/write paths. See the [validation guide](../deployment/store-rs/testing.md)
+read/write paths. See the [validation guide](../../../deployment/store-rs/testing.md)
for required build inputs and direct commands for additional scenarios.
Manual path-specific validators stay available as:
diff --git a/docs/source/design/store/store-rs/index.md b/docs/source/design/store/store-rs/index.md
index 016f864d6c..2d8e5e082e 100644
--- a/docs/source/design/store/store-rs/index.md
+++ b/docs/source/design/store/store-rs/index.md
@@ -13,6 +13,7 @@ public language APIs live under the Rust and Python API references.
| NoF integration boundaries | [NoF](nof) |
| Tenant control plane | [Multi-tenant admin design](multi-tenant-admin-control-plane) |
| Strict quota accounting | [Tenant quota consistency](tenant-quota-consistency) |
+| Implemented capabilities | [Features](features) |
:::{toctree}
:maxdepth: 1
@@ -23,4 +24,5 @@ cold-tier-design
nof
multi-tenant-admin-control-plane
tenant-quota-consistency
+features
:::
diff --git a/docs/source/design/tent/overview.md b/docs/source/design/tent/overview.md
index 6b37df87e7..30c5e1fdf9 100644
--- a/docs/source/design/tent/overview.md
+++ b/docs/source/design/tent/overview.md
@@ -139,4 +139,5 @@ failover
:maxdepth: 1
testing
+ub-native-validation
:::
diff --git a/docs/source/design/tent/ub-native-validation.md b/docs/source/design/tent/ub-native-validation.md
new file mode 100644
index 0000000000..c7294823b8
--- /dev/null
+++ b/docs/source/design/tent/ub-native-validation.md
@@ -0,0 +1,322 @@
+# TENT Native UB Hardware Validation Guide
+
+This guide validates the TENT-native UB transport on machines with a real
+UMDK/URMA provider. It covers build linkage, two-node READ/WRITE correctness,
+multi-rail scheduling, fault recovery, retryable shutdown, and a performance
+baseline. The repository's CI uses an injected adapter; the hardware cases in
+this guide are therefore required before declaring native UB production-ready.
+
+## 1. Record the Test Environment
+
+Run every case on the same Mooncake commit and record the following on both
+hosts:
+
+```bash
+git rev-parse HEAD
+uname -a
+gcc --version | head -1
+cmake --version | head -1
+urma_admin -l
+ldconfig -p | grep liburma
+```
+
+The two hosts must have mutually reachable EIDs and TCP reachability for the
+TENT P2P control plane. Use the vendor-supported tool to verify each active UB
+port, for example:
+
+```bash
+urma_admin -p urma0
+```
+
+If UMDK is installed outside the system search paths, export its library path
+before configuring and running Mooncake:
+
+```bash
+export LD_LIBRARY_PATH=/path/to/umdk/lib:${LD_LIBRARY_PATH}
+```
+
+Unset `MC_TENT_CONF` for the commands below unless the referenced file
+explicitly enables `transports/ub`. The environment config replaces, rather
+than merges with, the configuration assembled by `tebench`.
+
+```bash
+unset MC_TENT_CONF
+```
+
+## 2. Build and Verify Real URMA Linkage
+
+Configure a Debug build first. CMake discovers standard UMDK installations
+automatically; pass `URMA_SYSTEM_INCLUDE_DIR` and `URMA_LIBRARY` only for a
+non-standard installation.
+
+```bash
+cmake -S . -B build-ub-debug \
+ -DUSE_TENT=ON \
+ -DUSE_UB=ON \
+ -DUSE_CUDA=OFF \
+ -DBUILD_UNIT_TESTS=ON \
+ -DBUILD_BENCHMARK=ON \
+ -DCMAKE_BUILD_TYPE=Debug
+
+cmake --build build-ub-debug -j"$(nproc)" --target \
+ tent_ub_core_test \
+ tent_ub_teardown_test \
+ tent_ub_native_data_path_test \
+ tent_transport_selector_test \
+ tebench
+```
+
+Confirm that CMake found a real library and that `tebench` has a runtime
+dependency on it:
+
+```bash
+grep '^URMA_LIBRARY:' build-ub-debug/CMakeCache.txt
+ldd build-ub-debug/mooncake-transfer-engine/benchmark/tebench | grep urma
+```
+
+Both commands must show the intended UMDK installation. A header-only fallback
+is sufficient for mock tests but **not** for hardware validation.
+
+Run the deterministic mock suite before touching the devices:
+
+```bash
+ctest --test-dir build-ub-debug --output-on-failure \
+ -R 'tent_(ub_core|ub_teardown|ub_native_data_path|transport_selector)_test'
+```
+
+Expected result: all selected tests pass. In particular, the teardown test
+must demonstrate that a failed unregister/delete retains ownership and that a
+second cleanup call releases the same handle. It also verifies that a failed
+device unpublishes every old endpoint and cannot recover until every JFC has
+reported healthy in the current failure epoch.
+
+## 3. Two-Node Correctness
+
+Use `HOST_A` as the target and `HOST_B` as the initiator. The examples use a
+fixed control-plane port so peer-restart tests can reuse the same segment name.
+
+On `HOST_A`:
+
+```bash
+build-ub-debug/mooncake-transfer-engine/benchmark/tebench \
+ --backend=tent \
+ --metadata_type=p2p \
+ --rpc_server_port=18001 \
+ --xport_type=ub \
+ --seg_type=DRAM \
+ --total_buffer_size=1073741824 2>&1 | tee target-ub.log
+```
+
+Copy the printed segment name. On `HOST_B`, run a consistency-checked mixed
+transfer first:
+
+```bash
+TARGET_SEG=''
+
+build-ub-debug/mooncake-transfer-engine/benchmark/tebench \
+ --backend=tent \
+ --metadata_type=p2p \
+ --target_seg_name="${TARGET_SEG}" \
+ --xport_type=ub \
+ --tent_transport_hint=ub \
+ --seg_type=DRAM \
+ --op_type=mix \
+ --check_consistency=true \
+ --total_buffer_size=1073741824 \
+ --start_block_size=4096 \
+ --max_block_size=1048576 \
+ --start_batch_size=1 \
+ --max_batch_size=8 \
+ --start_num_threads=1 \
+ --max_num_threads=4 \
+ --duration=10 2>&1 | tee initiator-mix.log
+```
+
+Then repeat with `--op_type=read` and `--op_type=write`. All rows must finish,
+consistency checking must report no mismatch, and neither host may log a
+fallback to a non-UB transport. This covers both upward-facing TENT APIs and
+native URMA READ/WRITE without converting requests to the Classic TE path.
+
+## 4. Multi-Device and Pressure Scheduling
+
+Leave `transports/ub/device_filter` empty so every discovered active EID is
+eligible. Confirm that the startup topology lists at least two `ub::eid`
+entries on each host, then run a fixed high-concurrency case:
+
+```bash
+build-ub-debug/mooncake-transfer-engine/benchmark/tebench \
+ --backend=tent \
+ --metadata_type=p2p \
+ --target_seg_name="${TARGET_SEG}" \
+ --xport_type=ub \
+ --tent_transport_hint=ub \
+ --seg_type=DRAM \
+ --op_type=write \
+ --total_buffer_size=1073741824 \
+ --start_block_size=65536 \
+ --max_block_size=65536 \
+ --start_batch_size=16 \
+ --max_batch_size=16 \
+ --start_num_threads=16 \
+ --max_num_threads=16 \
+ --duration=60 2>&1 | tee initiator-multirail.log
+```
+
+Monitor per-device traffic with the platform's supported counters. Every
+healthy eligible rail should make progress; saturating one path must not stop
+posting on another path with free quota. Record aggregate bandwidth and the
+per-device byte deltas.
+
+## 5. Fault and Recovery Cases
+
+Run these cases with a long-lived embedding harness that keeps the initiator's
+TENT engine alive across each fault. Preserve both host logs, note exact fault
+timestamps, and record endpoint generations before and after recovery. A new
+`tebench` process may be used for basic reconnect smoke testing, but it is not
+sufficient evidence for generation isolation.
+
+### Peer restart and endpoint rebuild
+
+1. Stop `HOST_A` with `SIGINT` while transfers are active.
+2. Start it again with the same `--rpc_server_port=18001` command.
+3. In the still-running `HOST_B` harness, discover the republished segment and
+ submit READ and WRITE again without reinstalling the local TENT transport.
+
+The old endpoint generation must not be reused. The live engine must establish
+a new generation and complete new transfers; stale completions must not change
+a new task. Save both generation values as part of the PASS evidence.
+
+### Remote segment republish
+
+Using the embedding application's TENT API, unregister and re-register the
+target buffer at the same virtual address while the engines remain alive.
+Publish the returned `BufferDesc`, then submit another READ and WRITE. The new
+metadata generation must be imported and the stale imported segment must be
+released. Do not use `tebench` for this case because it registers its buffers
+only once at process startup.
+
+### UB port down/up
+
+1. Disable one UB port using the deployment's vendor-approved administration
+ procedure; do not power-cycle a host as the first test.
+2. Confirm that traffic continues on another healthy rail.
+3. Re-enable the port and wait at least the configured endpoint cooldown.
+4. Confirm that the recovered rail resumes traffic and that a new endpoint is
+ built before posting.
+
+The current recovery loop first unpublishes every endpoint backed by the failed
+device. It reactivates the context only after every provider JFC has polled
+successfully in the latest failure epoch, the cooldown has elapsed, and all old
+WRs have drained. This supports a transient port/link failure when the provider
+keeps Context/JFC handles valid. A destructive device reset that invalidates
+those handles still requires transport reinstall and must be reported as
+unsupported for in-place recovery.
+
+### Cancellation and timeout
+
+Exercise cancellation through the embedding application's TENT API while a
+large multi-slice transfer is active. For timeout testing, drop the UB path
+without stopping the control-plane TCP connection. The transfer must either
+complete on another rail or reach a terminal timeout/failure; it must not hang
+and shutdown must still drain safely.
+
+### Transport fallback
+
+The `tebench --xport_type=ub` commands above deliberately disable every other
+transport, so they validate UB path failover but **cannot** validate transport
+fallback. Use an embedding test harness with both UB and TCP (or RDMA) enabled,
+and a selector policy that prefers UB but permits the second transport. Submit
+the request with `transport_hint=UNSPEC`; do not pin it to UB.
+
+1. Verify the first consistency-checked READ and WRITE select UB.
+2. Disable every usable UB path while requests remain active.
+3. Verify the runtime reaches a terminal result for the failed UB attempt and
+ resubmits through TCP/RDMA rather than leaving the task pending.
+4. Restore UB, wait for its cooldown, and verify later policy-selected traffic
+ can use UB again.
+
+Capture selector/failover logs or transport metrics that identify both the
+failed UB attempt and the replacement transport. A successful copy without
+this evidence does not prove transport fallback.
+
+## 6. Repeated Shutdown and Sanitizers
+
+Configure a separate sanitizer build:
+
+```bash
+cmake -S . -B build-ub-asan \
+ -DUSE_TENT=ON \
+ -DUSE_UB=ON \
+ -DUSE_CUDA=OFF \
+ -DBUILD_UNIT_TESTS=ON \
+ -DBUILD_BENCHMARK=ON \
+ -DCMAKE_BUILD_TYPE=Debug \
+ -DCMAKE_CXX_FLAGS='-fsanitize=address,undefined -fno-omit-frame-pointer' \
+ -DCMAKE_EXE_LINKER_FLAGS='-fsanitize=address,undefined' \
+ -DCMAKE_SHARED_LINKER_FLAGS='-fsanitize=address,undefined'
+
+cmake --build build-ub-asan -j"$(nproc)" --target tebench
+export ASAN_OPTIONS=detect_leaks=1:halt_on_error=1
+export UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1
+```
+
+Use an embedding test harness to perform at least 100
+install-register-transfer-unregister-uninstall cycles **in one process**.
+Include ten cycles where a UB port or peer is unavailable during uninstall. If
+unregister or uninstall returns a provider `busy`/error, keep the engine alive
+and retry the same cleanup operation; the retry must act on the retained native
+handle and eventually succeed after the fault is removed. Also run 100
+target/initiator start-transfer-`SIGINT` process cycles as a supplementary
+check.
+
+A cycle is successful only if shutdown returns, the next cycle can reuse the
+same buffers and control-plane port, and ASan/UBSan reports no UAF, double free,
+or leaked native ownership graph. Record the first and retry return codes plus
+the provider log around every injected cleanup failure.
+
+## 7. Performance Baseline
+
+Use a `RelWithDebInfo` build and keep CPU affinity, NUMA placement, block size,
+batch size, concurrency, UMDK version, and device set fixed. Record:
+
+| Case | Required measurements |
+| --- | --- |
+| 4 KiB, batch 1, one thread | average, P99, and P999 latency |
+| 64 KiB and 1 MiB, one thread | single-stream bandwidth and CPU usage |
+| 64 KiB and 1 MiB, 16 threads | aggregate bandwidth and CPU usage |
+| Multi-device, 16 threads | aggregate and per-device bandwidth |
+| First transfer to a peer | endpoint connection time |
+
+Run the same matrix against Classic TE UB using the existing Classic UB
+benchmark on the same hosts. Any material regression needs a profile and an
+explanation; do not compare results from different UMDK versions or topology.
+
+## 8. Result Checklist
+
+Attach the following to the review:
+
+```text
+Mooncake commit:
+Host/OS/kernel:
+UMDK package and liburma path:
+Devices/EIDs:
+Debug USE_TENT+USE_UB build: PASS/FAIL
+Mock UB tests: PASS/FAIL
+Two-node READ: PASS/FAIL
+Two-node WRITE: PASS/FAIL
+Consistency-checked mix: PASS/FAIL
+Multi-device pressure scheduling: PASS/FAIL
+Peer restart / endpoint rebuild: PASS/FAIL
+Segment republish: PASS/FAIL
+Port down/up: PASS/FAIL
+Cancellation and timeout: PASS/FAIL
+UB to TCP/RDMA transport fallback: PASS/FAIL
+100-cycle ASan/UBSan shutdown: PASS/FAIL
+Classic UB baseline artifact:
+TENT UB baseline artifact:
+Known provider errors or unsupported cases:
+```
+
+Do not mark the hardware gate complete from bandwidth alone. Correctness,
+generation isolation, retryable resource release, and sanitizer-clean shutdown
+are independent acceptance requirements.
diff --git a/docs/source/getting_started/build.md b/docs/source/getting_started/build.md
index ec6a4deb10..371de0f038 100644
--- a/docs/source/getting_started/build.md
+++ b/docs/source/getting_started/build.md
@@ -75,6 +75,69 @@ sudo make install
`-DUSE_NOF=ON` builds the NoF registration APIs and deployment tools. Use
`-DUSE_NOF=OFF` or omit the option when the NVMe-oF SSD pool is not needed.
+(build-with-store-rs)=
+### Build with Store-RS
+
+Store-RS is an optional Rust-native Mooncake Store. The default CMake build
+leaves it disabled, and the Python Store facade uses the C++ backend. Enable
+it when building a root wheel that should include the Rust backend and its standalone commands.
+
+The build needs a root checkout with submodules, CMake, a C++ toolchain, Rust
+(`cargo` on `PATH`), and Python 3.10 or newer. A multi-client runtime also
+needs Redis or etcd.
+
+```bash
+export MOONCAKE_ROOT_DIR="$PWD"
+export MOONCAKE_STORE_RS_DIR="$MOONCAKE_ROOT_DIR/mooncake-store-rs"
+export MOONCAKE_BUILD_DIR="$MOONCAKE_ROOT_DIR/build/store-rs"
+export MOONCAKE_PYTHON_BIN="$(command -v python3)"
+
+cmake -S "$MOONCAKE_ROOT_DIR" -B "$MOONCAKE_BUILD_DIR" \
+ -DPython3_EXECUTABLE="$MOONCAKE_PYTHON_BIN" \
+ -DWITH_STORE=ON \
+ -DWITH_STORE_RUST=OFF \
+ -DWITH_STORE_RS=ON \
+ -DWITH_TE=ON \
+ -DUSE_TENT=ON \
+ -DBUILD_SHARED_LIBS=ON \
+ -DCMAKE_BUILD_TYPE=Release
+cmake --build "$MOONCAKE_BUILD_DIR" --target build_store_rs --parallel
+```
+
+This out-of-tree build provides the native artifacts and standalone binaries. For a Python wheel only, skip the separate CMake build command; the wheel builder configures and builds its own CMake tree.
+
+The root wheel owns the mooncake Python package. Build and install one wheel,
+then select its Store backend before importing mooncake.store:
+
+| Setting | Backend |
+|---------|---------|
+| Unset or cpp | C++ Store |
+| rs | Store-RS, included by WITH_STORE_RS=ON |
+
+The facade reads MOONCAKE_STORE_BACKEND when mooncake.store is first imported.
+Use exactly cpp or rs. The setting does not change the standalone command
+names.
+
+```bash
+MOONCAKE_STORE_BACKEND=rs .venv-store-rs/bin/python -c \
+ "from mooncake.store import MooncakeDistributedStore; print(MooncakeDistributedStore)"
+
+.venv-store-rs/bin/mooncake-store-rs-client --help
+.venv-store-rs/bin/mooncake-store-rs-admin --help
+.venv-store-rs/bin/mooncake-store-rs-bench --help
+```
+
+For workspace unit tests and installed-package smoke coverage, run
+`scripts/ci/run_store_rs_smoke.sh` after installing the CMake `python`
+component. The [Store-RS validation guide](../deployment/store-rs/testing.md#local-validation)
+lists the required CMake paths and the direct commands for heavier scenarios.
+
+Configuration, validation, and API details are in the
+[Store-RS deployment guides](../deployment/store-rs/index.md), the
+[Rust API](../api-reference/rust/store-rs.md), the
+[Python API](../api-reference/python/store-rs.md), and the
+[Admin HTTP API](../api-reference/http/store-rs-admin.md).
+
### RISC-V Build
Mooncake supports native 64-bit RISC-V Linux builds with `USE_RISCV` enabled.
@@ -301,6 +364,7 @@ The following options can be passed to `cmake ..`.
| `-DWITH_P2P_STORE=ON/OFF` | `OFF` | Enable Golang support and build the P2P Store component. Requires Go 1.23+. |
| `-DWITH_RUST_EXAMPLE=ON/OFF` | `OFF` | Build the Transfer Engine Rust library (`transfer_engine_rust`), tests, and sample benchmark. |
| `-DWITH_STORE_RUST=ON/OFF` | `ON` | Build Mooncake Store Rust bindings and CMake Rust targets. |
+| `-DWITH_STORE_RS=ON/OFF` | `OFF` | Build the Rust-native Mooncake Store (Store-RS), its Python extension, and the standalone commands. Requires `-DWITH_TE=ON`, `-DUSE_TENT=ON`, and `cargo`. See {ref}`Build with Store-RS `. |
| `-DWITH_EP=ON/OFF` | `OFF` | Build Mooncake EP and the native Mooncake PG core/device libraries. The Torch-facing PG adapter is compiled by the installed package at first `import mooncake.pg`. |
### Build Behavior Options
diff --git a/docs/source/getting_started/store-rs.md b/docs/source/getting_started/store-rs.md
deleted file mode 100644
index 962042e703..0000000000
--- a/docs/source/getting_started/store-rs.md
+++ /dev/null
@@ -1,88 +0,0 @@
-# Store-RS Source-Checkout Quickstart
-
-Store-RS is an optional Rust-native Mooncake Store component in the root
-repository. The normal CMake build leaves Store-RS disabled, and the Python
-Store facade uses the C++ backend by default. Enable Store-RS when building a
-root wheel that should include the Rust backend and its standalone commands.
-
-## Requirements
-
-- A Mooncake root checkout with its submodules initialized.
-- CMake, a C++ toolchain, and the Rust toolchain used by the Store-RS build.
-- Python 3.10 or newer for the Python wheel.
-- A Redis or etcd metadata service for a multi-client runtime.
-
-## Configure and build
-
-Run these commands from the Mooncake repository root. Keep the source, build,
-and Python interpreter paths explicit so CMake, the wheel, and validation
-scripts use the same checkout:
-
-```bash
-export MOONCAKE_ROOT_DIR="$PWD"
-export MOONCAKE_STORE_RS_DIR="$MOONCAKE_ROOT_DIR/mooncake-store-rs"
-export MOONCAKE_BUILD_DIR="$MOONCAKE_ROOT_DIR/build/store-rs"
-export MOONCAKE_PYTHON_BIN="$(command -v python3)"
-
-cmake -S "$MOONCAKE_ROOT_DIR" -B "$MOONCAKE_BUILD_DIR" \
- -DPython3_EXECUTABLE="$MOONCAKE_PYTHON_BIN" \
- -DWITH_STORE=ON \
- -DWITH_STORE_RUST=OFF \
- -DWITH_STORE_RS=ON \
- -DWITH_TE=ON \
- -DUSE_TENT=ON \
- -DBUILD_SHARED_LIBS=ON \
- -DCMAKE_BUILD_TYPE=Release
-cmake --build "$MOONCAKE_BUILD_DIR" --target build_store_rs --parallel
-```
-
-This out-of-tree build provides the native artifacts and standalone binaries.
-For a Python wheel only, skip the separate CMake build command; the wheel
-builder configures and builds its own CMake tree.
-
-The root Python wheel packages the C++ Store facade and, when configured with
-WITH_STORE_RS=ON, the Store-RS extension and the three standalone commands:
-
-```bash
-SKBUILD_CMAKE_ARGS="-DWITH_STORE=ON -DWITH_STORE_RUST=OFF \
- -DWITH_STORE_RS=ON -DWITH_TE=ON -DUSE_TENT=ON \
- -DBUILD_SHARED_LIBS=ON -DPython3_EXECUTABLE=$MOONCAKE_PYTHON_BIN" \
- "$MOONCAKE_PYTHON_BIN" -m pip wheel . --no-deps --wheel-dir dist/store-rs
-
-"$MOONCAKE_PYTHON_BIN" -m venv .venv-store-rs
-.venv-store-rs/bin/python -m pip install dist/store-rs/mooncake_transfer_engine-*.whl
-```
-
-The root wheel owns the mooncake Python package. Build and install one wheel,
-then select its Store backend before importing mooncake.store:
-
-| Setting | Backend |
-|---------|---------|
-| Unset or cpp | C++ Store |
-| rs | Store-RS, included by WITH_STORE_RS=ON |
-
-The facade reads MOONCAKE_STORE_BACKEND when mooncake.store is first imported.
-Use exactly cpp or rs. The setting does not change the standalone command
-names.
-
-```bash
-MOONCAKE_STORE_BACKEND=rs .venv-store-rs/bin/python -c \
- "from mooncake.store import MooncakeDistributedStore; print(MooncakeDistributedStore)"
-
-.venv-store-rs/bin/mooncake-store-rs-client --help
-.venv-store-rs/bin/mooncake-store-rs-admin --help
-.venv-store-rs/bin/mooncake-store-rs-bench --help
-```
-
-## Validate and continue
-
-For workspace unit tests and installed-package smoke coverage, run
-`scripts/ci/run_store_rs_smoke.sh` after installing the CMake `python`
-component. The [Store-RS validation guide](../deployment/store-rs/testing.md#local-validation)
-lists the required CMake paths and the direct commands for heavier scenarios.
-
-The [Store-RS deployment index](../deployment/store-rs/index.md) links to
-configuration, operator, and scenario-specific validation guides. For API
-details, see the [Rust API](../api-reference/rust/store-rs.md), the
-[Python API](../api-reference/python/store-rs.md), and the
-[Admin HTTP API](../api-reference/http/store-rs-admin.md).
diff --git a/docs/source/index.md b/docs/source/index.md
index 23fbf5be46..f65560c708 100644
--- a/docs/source/index.md
+++ b/docs/source/index.md
@@ -82,18 +82,63 @@ At the center of Mooncake is a KVCache-centric scheduler that balances effective
:::
-## Documentation
+π° Blogs
+
+::::{grid} 1 2 2 2
+:gutter: 2
+:class-container: latest-blogs
+
+:::{grid-item-card} Over One Trillion Tokens per Day: How Mooncake Powers Approaching.AIβs Token Factory
+:img-top: https://kvcache.ai/blog/mooncake-in-approaching-ai/featured_hue86b549ecdd836b0c6b3aa760df33338_1244829_2a2b63e1a97ff4921bf0e665a65e1403.webp
+:img-alt: The HiCache + Mooncake Store Architecture within a PD Group
+:link: https://kvcache.ai/blog/mooncake-in-approaching-ai/
+:link-alt: Over One Trillion Tokens per Day: How Mooncake Powers Approaching.AIβs Token Factory
+:class-body: blog-card-date
+
+September 15, 2026
+:::
+
+:::{grid-item-card} Mooncake for Miles: From Fragmented Rollout Data to Efficient Bulk I/O
+:img-top: https://kvcache.ai/blog/mooncake-rl-rollout-data-transfer/featured_hue655d0112fa862027ab204efc8ff95c7_58042_8bb94c00d58ac807a3ff4bc8f8df21e2.webp
+:img-alt: Miles rollout data transfer with Mooncake
+:link: https://kvcache.ai/blog/mooncake-rl-rollout-data-transfer/
+:link-alt: Mooncake for Miles: From Fragmented Rollout Data to Efficient Bulk I/O
+:class-body: blog-card-date
+
+August 20, 2026
+:::
+
+:::{grid-item-card} Scaling KV Cache Beyond Memory with Mooncake SSD Offloading
+:img-top: https://kvcache.ai/blog/scaling-kv-cache-beyond-memory/featured_huc52d82bb227581a966940933903d5e2c_479035_349926621d7fc55e030b6d364f46602b.webp
+:img-alt: Mooncake SSD Offloading Architecture
+:link: https://kvcache.ai/blog/scaling-kv-cache-beyond-memory/
+:link-alt: Scaling KV Cache Beyond Memory with Mooncake SSD Offloading
+:class-body: blog-card-date
+
+July 15, 2026
+:::
+
+:::{grid-item-card} How Much KV Cache Budget Do We Need for LLM Serving?
+:img-top: https://kvcache.ai/blog/calculate-kvcache-cache-budge/featured_hu137d5fd64784a367d2cae0088c081616_54073_4242cb61033db09c45fd8739c71f32e2.webp
+:img-alt: KV Cache Hit Rate Figure
+:link: https://kvcache.ai/blog/calculate-kvcache-cache-budge/
+:link-alt: How Much KV Cache Budget Do We Need for LLM Serving?
+:class-body: blog-card-date
+
+June 26, 2026
+:::
+
+::::
% How to start using Mooncake?
:::{toctree}
:caption: Getting Started
:maxdepth: 1
+:hidden:
getting_started/quick-start
getting_started/build
-getting_started/store-rs
-getting_started/store-rs-features
:::
@@ -102,6 +147,7 @@ getting_started/store-rs-features
:::{toctree}
:caption: Deployment
:maxdepth: 1
+:hidden:
deployment/mooncake-store-deployment-guide
deployment/store-rs/index
@@ -118,6 +164,7 @@ Mooncake x LMDeploy Integration
:::{toctree}
:caption: Performance
:maxdepth: 1
+:hidden:
performance/mooncake/index
performance/sglang/index
performance/vllm/index
@@ -128,6 +175,7 @@ performance/vllm/index
:::{toctree}
:caption: Design Documents
:maxdepth: 1
+:hidden:
design/architecture
design/transfer-engine/index
@@ -147,6 +195,7 @@ design/hicache-design
:::{toctree}
:caption: API Reference
:maxdepth: 1
+:hidden:
api-reference/python/index
api-reference/cpp/index
@@ -159,6 +208,7 @@ api-reference/rust/index
:::{toctree}
:caption: Troubleshooting
:maxdepth: 1
+:hidden:
troubleshooting/error-code
troubleshooting/troubleshooting
@@ -172,15 +222,7 @@ troubleshooting/store-rs-key-access
:::{toctree}
:caption: Community
:maxdepth: 1
+:hidden:
community/governance
:::
-
-% Archived content
-
-### Archived
-
-- [Guide: vLLM MooncakeStoreConnector](deployment/integrations/vllm/vllm-mooncakestoreconnector)
-- [vLLM V0 Disaggregated Serving Demo](deployment/integrations/vllm/vllm-integration-v0.2)
-- [vLLM V0 Disaggregated Serving with MooncakeStore](deployment/integrations/vllm/vllm-integration-v0.3)
-- [vLLM v1 backend Disaggregated Serving with MooncakeConnector](deployment/integrations/vllm/vllm-integration-v1.0)
diff --git a/docs/source/performance/mooncake/store-rs-benchmark.md b/docs/source/performance/mooncake/store-rs-benchmark.md
index 8ae6ff77b9..262cd2150d 100644
--- a/docs/source/performance/mooncake/store-rs-benchmark.md
+++ b/docs/source/performance/mooncake/store-rs-benchmark.md
@@ -275,7 +275,7 @@ Keyspace behavior is conditional:
## Build
-Configure the root CMake project as described in the [Store-RS source-checkout quickstart](../../getting_started/store-rs.md), then build the build_store_rs_cli target. The unified root wheel installs the client, admin, and benchmark commands for direct use from its Python environment.
+Configure the root CMake project as described in {ref}`Build with Store-RS `, then build the build_store_rs_cli target. The unified root wheel installs the client, admin, and benchmark commands for direct use from its Python environment.
## Next reading
diff --git a/docs/source/performance/mooncake/tebench.md b/docs/source/performance/mooncake/tebench.md
index 47180d8abf..e39cec0fc9 100644
--- a/docs/source/performance/mooncake/tebench.md
+++ b/docs/source/performance/mooncake/tebench.md
@@ -397,7 +397,8 @@ gpu_id + thread_id
**Transport (TENT only)**
-* `--xport_type` : `rdma | shm | mnnvl | gds | iouring`. Selects a single
+* `--xport_type` : `rdma | tcp | shm | mnnvl | nvlink | gds | iouring | ub |
+ sunrise_link`. Selects a single
transport to enable (all others are disabled). Empty means no transport
is explicitly enabled or disabled by tebench β the engine reads the
transport enable list from the `MC_TENT_CONF` config file (see Section
diff --git a/docs/source/troubleshooting/store-rs-key-access.md b/docs/source/troubleshooting/store-rs-key-access.md
index 049e1b243f..9fbe391b7b 100644
--- a/docs/source/troubleshooting/store-rs-key-access.md
+++ b/docs/source/troubleshooting/store-rs-key-access.md
@@ -1,4 +1,4 @@
-# Key Access Path Debugging
+# Store-RS Troubleshooting
This guide explains how to trace a single key through Mooncake Store RS: where a
`put` writes the object, how a `get` resolves and reads it, which route layer is
diff --git a/mooncake-common/include/environment_variables.h b/mooncake-common/include/environment_variables.h
index 1d48347201..f52c10e616 100644
--- a/mooncake-common/include/environment_variables.h
+++ b/mooncake-common/include/environment_variables.h
@@ -45,6 +45,7 @@ struct FilePerKeyEnvironmentVariables {
};
struct BucketBackendEnvironmentVariables {
+ MC_DEFINE_ENV_VAR(bool, MOONCAKE_OFFLOAD_FLUSH_PARTIAL_BUCKETS);
MC_DEFINE_ENV_VAR(int64_t, MOONCAKE_OFFLOAD_BUCKET_KEYS_LIMIT);
MC_DEFINE_ENV_VAR(int64_t, MOONCAKE_OFFLOAD_BUCKET_SIZE_LIMIT_BYTES);
MC_DEFINE_ENV_VAR(int64_t, MOONCAKE_OFFLOAD_BUCKET_MAX_TOTAL_SIZE);
diff --git a/mooncake-integration/store/store_py.cpp b/mooncake-integration/store/store_py.cpp
index 2dff85bd55..895da7a8fd 100644
--- a/mooncake-integration/store/store_py.cpp
+++ b/mooncake-integration/store/store_py.cpp
@@ -1171,11 +1171,18 @@ class MooncakeStorePyWrapper {
ReplicateConfig MakeIndexedConfig(
const ReplicateConfig &config,
const std::vector &original_indices) const {
+ ReplicateConfig indexed_config = config;
+ if (config.session_ids) {
+ indexed_config.session_ids->clear();
+ for (size_t index : original_indices) {
+ indexed_config.session_ids->push_back(
+ config.session_ids->at(index));
+ }
+ }
if (!config.group_ids.has_value()) {
- return config;
+ return indexed_config;
}
- ReplicateConfig indexed_config = config;
std::vector group_ids;
group_ids.reserve(original_indices.size());
for (size_t index : original_indices) {
@@ -1188,11 +1195,20 @@ class MooncakeStorePyWrapper {
ReplicateConfig MakeRepeatedIndexedConfig(
const ReplicateConfig &config,
const std::vector &original_indices, int repeat_count) const {
+ ReplicateConfig indexed_config = config;
+ if (config.session_ids) {
+ indexed_config.session_ids->clear();
+ for (size_t index : original_indices) {
+ for (int i = 0; i < repeat_count; ++i) {
+ indexed_config.session_ids->push_back(
+ config.session_ids->at(index));
+ }
+ }
+ }
if (!config.group_ids.has_value()) {
- return config;
+ return indexed_config;
}
- ReplicateConfig indexed_config = config;
std::vector group_ids;
group_ids.reserve(original_indices.size() *
static_cast(repeat_count));
@@ -1208,6 +1224,11 @@ class MooncakeStorePyWrapper {
std::vector ValidateGroupIdsForBatchConfig(
const ReplicateConfig &config, size_t key_count,
const char *operation_name) const {
+ if (!config.ValidSessionIds(key_count)) {
+ LOG(ERROR) << operation_name << ": invalid per-key session IDs";
+ return std::vector(key_count,
+ to_py_ret(ErrorCode::INVALID_PARAMS));
+ }
if (config.group_ids.has_value() &&
config.group_ids->size() != key_count) {
LOG(ERROR) << operation_name
@@ -1222,6 +1243,8 @@ class MooncakeStorePyWrapper {
const std::string &key, pybind11::object tensor,
const ReplicateConfig &config = ReplicateConfig{}, int tp_rank = 0,
int tp_size = 1, int split_dim = 0) {
+ if (!config.ValidSessionIds(1))
+ return to_py_ret(ErrorCode::INVALID_PARAMS);
try {
std::vector tp_keys;
tp_keys.reserve(tp_size);
@@ -1234,7 +1257,13 @@ class MooncakeStorePyWrapper {
if (!infos.has_value()) {
return to_py_ret(ErrorCode::INVALID_PARAMS);
}
- auto results = batch_put_tensor_infos_impl(tp_keys, *infos, config);
+ ReplicateConfig tp_config = config;
+ if (config.session_ids) {
+ tp_config.session_ids =
+ std::vector(tp_size, config.SessionId());
+ }
+ auto results =
+ batch_put_tensor_infos_impl(tp_keys, *infos, tp_config);
for (int ret : results) {
if (ret != 0) return ret;
}
@@ -2155,6 +2184,18 @@ PYBIND11_MODULE(_store, m) {
&ReplicateConfig::prefer_alloc_in_same_node)
.def_readwrite("data_type", &ReplicateConfig::data_type)
.def_readwrite("group_ids", &ReplicateConfig::group_ids)
+ .def_property(
+ "session_ids",
+ [](const ReplicateConfig &config)
+ -> std::optional> {
+ if (!config.session_ids) return std::nullopt;
+ return *config.session_ids;
+ },
+ [](ReplicateConfig &config,
+ std::optional> ids) {
+ config.session_ids.reset();
+ if (ids) config.session_ids = std::move(*ids);
+ })
.def("__str__", [](const ReplicateConfig &config) {
std::ostringstream oss;
oss << config;
@@ -2625,15 +2666,31 @@ PYBIND11_MODULE(_store, m) {
.def(
"batch_probe_key",
[](MooncakeStorePyWrapper &self,
- const std::vector &keys) {
+ const std::vector &keys, const std::string &policy,
+ uint64_t candidate_size) {
+ GrantLeasePolicy lease_policy;
+ if (policy == "none") {
+ lease_policy.lease_mode = ProbeLeaseMode::None;
+ } else if (policy == "LastHitOnly") {
+ lease_policy.lease_mode = ProbeLeaseMode::LastHitOnly;
+ } else {
+ throw py::value_error("Unknown batch_probe_key policy: " +
+ policy);
+ }
+ lease_policy.candidate_size = candidate_size;
+ if (!self.is_client_initialized()) {
+ return std::vector(
+ keys.size(), to_py_ret(ErrorCode::INVALID_PARAMS));
+ }
py::gil_scoped_release release;
- return self.store_->batchProbeKey(keys);
+ return self.store_->batchProbeKey(keys, lease_policy);
},
- py::arg("keys"),
- "Point-in-time existence check for multiple objects that grants "
- "no read leases. Returns list of results: 1 if existed at the "
- "time of the call, 0 if not exists, -1 if error. Objects may "
- "still be evicted before a subsequent get.")
+ py::arg("keys"), py::arg("policy") = "none",
+ py::arg("candidate_size") = 1,
+ "Per-key existence results: 1 if readable, 0 if missing, and "
+ "negative values on error. none grants no leases. LastHitOnly "
+ "leases the last all-1 group of candidate_size results; failed "
+ "candidates may retain partial leases after a concurrent eviction.")
.def(
"close",
[](MooncakeStorePyWrapper &self) {
diff --git a/mooncake-integration/store/store_py_internal.h b/mooncake-integration/store/store_py_internal.h
index e43c3df105..83a3be052a 100644
--- a/mooncake-integration/store/store_py_internal.h
+++ b/mooncake-integration/store/store_py_internal.h
@@ -867,7 +867,8 @@ bool is_default_replicate_config(const ReplicateConfig &config) {
!config.soft_pin_ttl_ms.has_value() && !config.with_hard_pin &&
config.preferred_segments.empty() &&
config.preferred_segment.empty() &&
- !config.prefer_alloc_in_same_node && !config.group_ids.has_value();
+ !config.prefer_alloc_in_same_node && !config.group_ids.has_value() &&
+ !config.session_ids.has_value();
}
std::optional parse_parallel_axis_spec(
diff --git a/mooncake-store-rs/README.md b/mooncake-store-rs/README.md
index 15b021e56e..6b0ad4909c 100644
--- a/mooncake-store-rs/README.md
+++ b/mooncake-store-rs/README.md
@@ -95,8 +95,8 @@ Python facade and does not change the standalone command behavior.
| Topic | Guide |
|-------|-------|
-| Source-checkout build and wheel | [Store-RS quickstart](../docs/source/getting_started/store-rs.md) |
-| Feature overview | [Store-RS features](../docs/source/getting_started/store-rs-features.md) |
+| Source-checkout build and wheel | [Build with Store-RS](../docs/source/getting_started/build.md#build-with-store-rs) |
+| Feature overview | [Store-RS features](../docs/source/design/store/store-rs/features.md) |
| Rust integration | [Rust API](../docs/source/api-reference/rust/store-rs.md) |
| Python facade and Store-RS semantics | [Python API](../docs/source/api-reference/python/store-rs.md) |
| Configuration and deployment | [Store-RS deployment guides](../docs/source/deployment/store-rs/index.md) |
diff --git a/mooncake-store/include/client_service.h b/mooncake-store/include/client_service.h
index d072a36686..94e203b5b3 100644
--- a/mooncake-store/include/client_service.h
+++ b/mooncake-store/include/client_service.h
@@ -468,13 +468,14 @@ class Client {
const std::vector& keys);
/**
- * @brief Point-in-time existence check for multiple objects, granting no
- * read leases
+ * @brief Probe multiple objects, optionally leasing the last complete
+ * candidate. LastHitOnly leases the last all-true candidate.
* @param keys Vector of keys to check
- * @return Vector of existence results for each key
+ * @return Per-key existence results in input order
*/
std::vector> BatchProbeKey(
- const std::vector& keys);
+ const std::vector& keys,
+ const GrantLeasePolicy& policy = {});
/**
* @brief Create a copy task to copy an object's replicas to target segments
diff --git a/mooncake-store/include/common/file_util.h b/mooncake-store/include/common/file_util.h
index 67388d640a..e94926a2f4 100644
--- a/mooncake-store/include/common/file_util.h
+++ b/mooncake-store/include/common/file_util.h
@@ -12,6 +12,11 @@ namespace fs = std::filesystem;
class FileUtil {
public:
+ // Snapshot birth times before sorting, oldest known first. Failed metadata
+ // queries and filesystems without birth times sort last, by path. Entries
+ // are retained so callers can separately check whether they are readable.
+ static void SortByCreationTime(std::vector& files);
+
static std::string ResolvePathFromKey(const std::string& key,
const std::string& root_dir,
const std::string& fsdir);
diff --git a/mooncake-store/include/config/bucket_backend_config.h b/mooncake-store/include/config/bucket_backend_config.h
index 17f04d7d5b..1007d59b62 100644
--- a/mooncake-store/include/config/bucket_backend_config.h
+++ b/mooncake-store/include/config/bucket_backend_config.h
@@ -62,6 +62,10 @@ struct BucketBackendConfig {
// max_physical_bytes > 0.
int64_t disk_scan_cache_ms = 500;
+ // Write the final partial bucket in the current offload cycle instead of
+ // carrying keys into a later cycle without their current offload tasks.
+ bool flush_partial_buckets = false;
+
bool Validate() const;
static BucketBackendConfig FromEnvironment();
diff --git a/mooncake-store/include/dummy_client.h b/mooncake-store/include/dummy_client.h
index 5a4777a030..7b922b55bd 100644
--- a/mooncake-store/include/dummy_client.h
+++ b/mooncake-store/include/dummy_client.h
@@ -185,7 +185,8 @@ class DummyClient : public PyClient {
int probeKey(const std::string &key);
- std::vector batchProbeKey(const std::vector &keys);
+ std::vector batchProbeKey(const std::vector &keys,
+ const GrantLeasePolicy &policy = {});
int64_t getSize(const std::string &key);
diff --git a/mooncake-store/include/ha/ha_types.h b/mooncake-store/include/ha/ha_types.h
index d4506fb646..0b6ca7432a 100644
--- a/mooncake-store/include/ha/ha_types.h
+++ b/mooncake-store/include/ha/ha_types.h
@@ -99,6 +99,8 @@ struct HABackendSpec {
HABackendType type = HABackendType::UNKNOWN;
std::string connstring;
ClusterNamespace cluster_namespace;
+ // Leadership lease TTL in seconds. Currently honored by the etcd backend.
+ int64_t leader_lease_ttl_sec = DEFAULT_MASTER_VIEW_LEASE_TTL_SEC;
};
struct MasterView {
diff --git a/mooncake-store/include/kv_event/kv_event_publisher.h b/mooncake-store/include/kv_event/kv_event_publisher.h
index 9e72c03e13..1d26ff5d0d 100644
--- a/mooncake-store/include/kv_event/kv_event_publisher.h
+++ b/mooncake-store/include/kv_event/kv_event_publisher.h
@@ -34,11 +34,14 @@ class KvEventPublisher {
// TenantId wrapper; the TenantId overloads preserve the existing API.
void PublishStored(const std::string& object_key, const std::string& medium,
const std::string& tenant_id = "",
- const std::string& group_id = "");
+ const std::string& group_id = "",
+ const std::string& session_id = "");
void PublishStored(const std::string& object_key, const std::string& medium,
const TenantId& tenant_id,
- const std::string& group_id = "") {
- PublishStored(object_key, medium, tenant_id.value(), group_id);
+ const std::string& group_id = "",
+ const std::string& session_id = "") {
+ PublishStored(object_key, medium, tenant_id.value(), group_id,
+ session_id);
}
void PublishRemoved(const std::string& object_key,
const std::string& medium,
@@ -61,7 +64,8 @@ class KvEventPublisher {
void PublishCommitted(const std::string& object_key,
const std::vector& current_media,
const std::string& tenant_id = "",
- const std::string& group_id = "");
+ const std::string& group_id = "",
+ const std::string& session_id = "");
// Synchronize replica availability after an internal metadata mutation.
// previous_media is the medium set captured before the mutation; media that
@@ -70,7 +74,8 @@ class KvEventPublisher {
const std::vector& current_media,
const std::string& tenant_id = "",
const std::string& group_id = "",
- const std::vector& previous_media = {});
+ const std::vector& previous_media = {},
+ const std::string& session_id = "");
// Publish removal of every medium the object had before deletion.
void PublishObjectRemoved(
@@ -95,6 +100,7 @@ class KvEventPublisher {
std::string medium;
std::string tenant_id;
std::string group_id;
+ std::string session_id{};
};
void EnqueueBatch(std::vector events);
@@ -132,19 +138,22 @@ class KvEventPublisher {
bool enabled() const { return false; }
void PublishStored(const std::string&, const std::string&,
- const std::string& = "", const std::string& = "") {}
- void PublishStored(const std::string&, const std::string&, const TenantId&,
+ const std::string& = "", const std::string& = "",
const std::string& = "") {}
+ void PublishStored(const std::string&, const std::string&, const TenantId&,
+ const std::string& = "", const std::string& = "") {}
void PublishRemoved(const std::string&, const std::string&,
const std::string& = "", const std::string& = "") {}
void PublishRemoved(const std::string&, const std::string&, const TenantId&,
const std::string& = "") {}
void PublishCleared(const std::string& = "") {}
void PublishCommitted(const std::string&, const std::vector&,
- const std::string& = "", const std::string& = "") {}
+ const std::string& = "", const std::string& = "",
+ const std::string& = "") {}
void SyncObjectState(const std::string&, const std::vector&,
const std::string& = "", const std::string& = "",
- const std::vector& = {}) {}
+ const std::vector& = {},
+ const std::string& = "") {}
void PublishObjectRemoved(const std::string&, const std::string& = "",
const std::string& = "",
const std::vector& = {}) {}
diff --git a/mooncake-store/include/master_client.h b/mooncake-store/include/master_client.h
index 008dfd9f89..aee43a476c 100644
--- a/mooncake-store/include/master_client.h
+++ b/mooncake-store/include/master_client.h
@@ -152,13 +152,14 @@ class MasterClient {
const std::vector& object_keys);
/**
- * @brief Point-in-time existence check for multiple objects, granting no
- * read leases
+ * @brief Probe multiple objects, optionally leasing the last complete
+ * candidate. LastHitOnly leases the last all-true candidate in the results.
* @param object_keys Vector of keys to query
- * @return Vector containing existence status for each key
+ * @return Per-key existence results in input order
*/
[[nodiscard]] std::vector> BatchProbeKey(
- const std::vector& object_keys);
+ const std::vector& object_keys,
+ const GrantLeasePolicy& policy = {});
/**
* @brief Calculate Store-observed cache reuse metrics
diff --git a/mooncake-store/include/master_config.h b/mooncake-store/include/master_config.h
index 6c22d8573b..489d02a616 100644
--- a/mooncake-store/include/master_config.h
+++ b/mooncake-store/include/master_config.h
@@ -121,6 +121,7 @@ struct MasterConfig {
bool enable_offload;
std::string ha_backend_type;
std::string ha_backend_connstring;
+ int64_t ha_leader_lease_ttl_sec = DEFAULT_MASTER_VIEW_LEASE_TTL_SEC;
std::string etcd_endpoints;
// OpLog store configuration
@@ -257,6 +258,7 @@ class MasterServiceSupervisorConfig {
uint32_t handler_worker_thread_num = 0;
std::string ha_backend_type = "etcd";
std::string ha_backend_connstring;
+ int64_t ha_leader_lease_ttl_sec = DEFAULT_MASTER_VIEW_LEASE_TTL_SEC;
std::string etcd_endpoints = "0.0.0.0:2379";
// OpLog store configuration
bool enable_oplog = false;
@@ -393,6 +395,7 @@ class MasterServiceSupervisorConfig {
rpc_enable_tcp_no_delay = config.rpc_enable_tcp_no_delay;
handler_worker_thread_num = config.handler_worker_thread_num;
ha_backend_type = config.ha_backend_type;
+ ha_leader_lease_ttl_sec = config.ha_leader_lease_ttl_sec;
etcd_endpoints = config.etcd_endpoints;
ha_backend_connstring = ResolveConfiguredHABackendConnstring(
ha_backend_type, config.ha_backend_connstring, etcd_endpoints);
diff --git a/mooncake-store/include/master_service.h b/mooncake-store/include/master_service.h
index 93afd41c35..c80f92b68c 100644
--- a/mooncake-store/include/master_service.h
+++ b/mooncake-store/include/master_service.h
@@ -262,7 +262,8 @@ class MasterService {
-> tl::expected;
std::vector> BatchProbeKey(
- const std::vector& keys, const TenantId& tenant_id);
+ const GrantLeasePolicy& policy, const std::vector& keys,
+ const TenantId& tenant_id);
/**
* @brief Fetch all keys for a single tenant.
diff --git a/mooncake-store/include/metadata_store.h b/mooncake-store/include/metadata_store.h
index 957d43a77b..08ff3c5598 100644
--- a/mooncake-store/include/metadata_store.h
+++ b/mooncake-store/include/metadata_store.h
@@ -39,10 +39,12 @@ struct StandbyObjectMetadata {
ObjectDataType::UNKNOWN}; // Data type classification
struct_pack::compatible hard_pinned; // Eviction protection
+ struct_pack::compatible session_id;
+
StandbyObjectMetadata() = default;
YLT_REFL(StandbyObjectMetadata, client_id, size, replicas, group_id,
- data_type, hard_pinned);
+ data_type, hard_pinned, session_id);
// Check if this metadata has valid replicas
bool HasReplicas() const { return !replicas.empty(); }
@@ -106,8 +108,10 @@ struct MetadataPayload {
struct_pack::compatible data_type; // Data type
struct_pack::compatible hard_pinned; // Hard pin state
+ struct_pack::compatible session_id;
+
YLT_REFL(MetadataPayload, client_id, size, replicas, group_id, data_type,
- hard_pinned);
+ hard_pinned, session_id);
// Convert to StandbyObjectMetadata
StandbyObjectMetadata ToStandbyMetadata() const {
@@ -116,6 +120,7 @@ struct MetadataPayload {
meta.size = size;
meta.replicas = replicas;
meta.group_id = group_id.value_or("");
+ meta.session_id = session_id.value_or("");
meta.data_type = data_type.value_or(ObjectDataType::UNKNOWN);
meta.hard_pinned = hard_pinned.value_or(false);
return meta;
diff --git a/mooncake-store/include/object_metadata.h b/mooncake-store/include/object_metadata.h
index 0846327d13..046fbee130 100644
--- a/mooncake-store/include/object_metadata.h
+++ b/mooncake-store/include/object_metadata.h
@@ -106,6 +106,42 @@ struct ObjectMetadata {
const TenantId tenant_id;
const std::string user_key;
+ // Committed attribution only; protected by the owning metadata shard lock.
+ std::string session_id;
+ struct PendingSessionTag {
+ std::string session_id;
+ std::unordered_set eligible_replica_ids;
+ };
+ std::optional pending_session_tag;
+
+ void BeginSessionTag(std::string id,
+ const std::unordered_set& replicas) {
+ pending_session_tag.reset();
+ if (!id.empty()) {
+ pending_session_tag = PendingSessionTag{std::move(id), replicas};
+ }
+ }
+
+ void CommitSessionTag(ReplicaID replica_id) {
+ if (pending_session_tag &&
+ pending_session_tag->eligible_replica_ids.contains(replica_id)) {
+ session_id = std::move(pending_session_tag->session_id);
+ pending_session_tag.reset();
+ }
+ }
+
+ void ClearSessionTagIfNoViableReplica() {
+ if (pending_session_tag && !HasReplica([this](const Replica& replica) {
+ return pending_session_tag->eligible_replica_ids.contains(
+ replica.id()) &&
+ replica.is_processing() &&
+ !replica.has_invalid_mem_handle() &&
+ !replica.has_invalid_nof_handle();
+ })) {
+ pending_session_tag.reset();
+ }
+ }
+
mutable SpinLock lock;
// Authoritative lease: ungrouped objects own one; grouped objects share
// the group's. Never null after construction.
diff --git a/mooncake-store/include/pyclient.h b/mooncake-store/include/pyclient.h
index 6fe9d3dff8..283dd6ef6a 100644
--- a/mooncake-store/include/pyclient.h
+++ b/mooncake-store/include/pyclient.h
@@ -538,7 +538,8 @@ class PyClient {
virtual int probeKey(const std::string &key) = 0;
virtual std::vector batchProbeKey(
- const std::vector &keys) = 0;
+ const std::vector &keys,
+ const GrantLeasePolicy &policy = {}) = 0;
virtual int64_t getSize(const std::string &key) = 0;
diff --git a/mooncake-store/include/real_client.h b/mooncake-store/include/real_client.h
index 6e5e2ff8e9..19bbc9d7fc 100644
--- a/mooncake-store/include/real_client.h
+++ b/mooncake-store/include/real_client.h
@@ -387,13 +387,14 @@ class RealClient : public PyClient {
int probeKey(const std::string &key);
/**
- * @brief Point-in-time existence check for multiple objects, granting
- * no read leases
+ * @brief Probe multiple objects, optionally leasing the last complete
+ * candidate.
* @param keys Vector of keys to check
- * @return Vector of existence results: 1 if exists, 0 if not exists, -1
- * if error
+ * @return Per-key existence results. LastHitOnly leases the last all-1
+ * candidate. Negative values indicate errors.
*/
- std::vector batchProbeKey(const std::vector &keys);
+ std::vector batchProbeKey(const std::vector &keys,
+ const GrantLeasePolicy &policy = {});
/**
* @brief Get the size of an object
@@ -801,7 +802,7 @@ class RealClient : public PyClient {
tl::expected probeKey_internal(const std::string &key);
std::vector> batchProbeKey_internal(
- const std::vector &keys);
+ const std::vector &keys, const GrantLeasePolicy &policy);
tl::expected getSize_internal(const std::string &key);
diff --git a/mooncake-store/include/replica.h b/mooncake-store/include/replica.h
index eb1bcadafe..f35b82fbd4 100644
--- a/mooncake-store/include/replica.h
+++ b/mooncake-store/include/replica.h
@@ -3,6 +3,10 @@
#include
#include
+#include
+
+#include
+#include
#include
#include
@@ -125,13 +129,44 @@ struct ReplicateConfig {
// always hash(tenant, key) and is decoupled from groups.
std::optional> group_ids{};
- ReplicateConfig ForSingleKey(size_t key_index) const {
- ReplicateConfig key_config = *this;
- if (group_ids.has_value()) {
- key_config.group_ids =
- std::vector{group_ids->at(key_index)};
+ // Per-key write attribution, independent of key identity and eviction
+ // groups. Compatible field keeps pre-session RPC payloads readable in both
+ // directions.
+ struct_pack::compatible, 1> session_ids;
+
+ bool ValidSessionIds(size_t key_count) const {
+ return !session_ids ||
+ (session_ids->size() == key_count &&
+ std::all_of(session_ids->begin(), session_ids->end(),
+ [](const auto& id) {
+ if (id.size() > 256) return false;
+ auto it = id.begin();
+ while (it != id.end()) {
+ namespace utf = boost::locale::utf;
+ if (!utf::is_valid_codepoint(
+ utf::utf_traits::decode(
+ it, id.end()))) {
+ return false;
+ }
+ }
+ return true;
+ }));
+ }
+
+ std::string SessionId() const {
+ return session_ids && !session_ids->empty() ? session_ids->front() : "";
+ }
+
+ // Reuse a copy of the batch config; select IDs from the original batch.
+ void SetKeyIds(const ReplicateConfig& batch_config, size_t key_index) {
+ if (batch_config.group_ids.has_value()) {
+ group_ids =
+ std::vector{batch_config.group_ids->at(key_index)};
+ }
+ if (batch_config.session_ids) {
+ session_ids = std::vector{
+ batch_config.session_ids->at(key_index)};
}
- return key_config;
}
friend std::ostream& operator<<(std::ostream& os,
diff --git a/mooncake-store/include/rpc_service.h b/mooncake-store/include/rpc_service.h
index c334157923..ba326e63ed 100644
--- a/mooncake-store/include/rpc_service.h
+++ b/mooncake-store/include/rpc_service.h
@@ -54,7 +54,7 @@ class WrappedMasterService {
const std::string& tenant_id = "default");
std::vector> BatchProbeKey(
- const std::vector& keys,
+ const GrantLeasePolicy& policy, const std::vector& keys,
const std::string& tenant_id = "default");
tl::expected<
diff --git a/mooncake-store/include/types.h b/mooncake-store/include/types.h
index 0536bd7cab..d26a6126f6 100644
--- a/mooncake-store/include/types.h
+++ b/mooncake-store/include/types.h
@@ -20,6 +20,15 @@
namespace mooncake {
+enum class ProbeLeaseMode : uint8_t { None = 0, LastHitOnly = 1 };
+
+// Candidates are consecutive groups of candidate_size keys in caller order.
+// None preserves the lease-free existence probe and ignores candidate_size.
+struct GrantLeasePolicy {
+ ProbeLeaseMode lease_mode{ProbeLeaseMode::None};
+ uint64_t candidate_size{1};
+};
+
// Constants
static constexpr uint64_t WRONG_VERSION = 0;
static constexpr uint64_t DEFAULT_VALUE = UINT64_MAX;
diff --git a/mooncake-store/src/client_service.cpp b/mooncake-store/src/client_service.cpp
index 6a8be15706..0a9473c0a7 100644
--- a/mooncake-store/src/client_service.cpp
+++ b/mooncake-store/src/client_service.cpp
@@ -2517,6 +2517,8 @@ void Client::healDanglingLocalDiskBatchStarts(
std::vector retry_keys;
std::vector> retry_slices;
std::vector retry_positions;
+ ReplicateConfig retry_config = config;
+ if (retry_config.session_ids) retry_config.session_ids->clear();
for (size_t j = 0; j < evict_keys.size(); ++j) {
if (!evicted[j]) {
LOG(WARNING) << "Failed to evict dangling LOCAL_DISK replica for "
@@ -2530,12 +2532,16 @@ void Client::healDanglingLocalDiskBatchStarts(
retry_keys.emplace_back(keys[already_exists[evict_positions[j]]]);
retry_slices.emplace_back(
slice_lengths[already_exists[evict_positions[j]]]);
+ if (config.session_ids) {
+ retry_config.session_ids->push_back(
+ config.session_ids->at(already_exists[evict_positions[j]]));
+ }
}
if (retry_keys.empty()) {
return;
}
auto retry_responses =
- master_client_.BatchPutStart(retry_keys, retry_slices, config);
+ master_client_.BatchPutStart(retry_keys, retry_slices, retry_config);
if (retry_responses.size() != retry_keys.size()) {
return;
}
@@ -2568,6 +2574,8 @@ void Client::StartBatchPut(std::vector& ops,
std::vector keys;
std::vector> slice_lengths;
std::vector active_indices;
+ ReplicateConfig active_config = config;
+ if (active_config.session_ids) active_config.session_ids->clear();
keys.reserve(ops.size());
slice_lengths.reserve(ops.size());
@@ -2587,6 +2595,9 @@ void Client::StartBatchPut(std::vector& ops,
}
active_indices.emplace_back(i);
keys.emplace_back(op.key);
+ if (config.session_ids) {
+ active_config.session_ids->push_back(config.session_ids->at(i));
+ }
std::vector slice_sizes;
slice_sizes.reserve(op.slices.size());
@@ -2601,7 +2612,7 @@ void Client::StartBatchPut(std::vector& ops,
}
auto start_responses =
- master_client_.BatchPutStart(keys, slice_lengths, config);
+ master_client_.BatchPutStart(keys, slice_lengths, active_config);
// Ensure response size matches request size
if (start_responses.size() != active_indices.size()) {
@@ -2651,7 +2662,7 @@ void Client::StartBatchPut(std::vector& ops,
}
healDanglingLocalDiskBatchStarts(ops, active_indices, already_exists, keys,
- slice_lengths, config);
+ slice_lengths, active_config);
for (size_t i : already_exists) {
auto& op = ops[active_indices[i]];
if (!op.IsResolved() && op.replicas.empty()) {
@@ -2667,6 +2678,8 @@ void Client::StartBatchUpsert(std::vector& ops,
std::vector keys;
std::vector> slice_lengths;
std::vector active_indices;
+ ReplicateConfig active_config = config;
+ if (active_config.session_ids) active_config.session_ids->clear();
keys.reserve(ops.size());
slice_lengths.reserve(ops.size());
@@ -2686,6 +2699,9 @@ void Client::StartBatchUpsert(std::vector& ops,
}
active_indices.emplace_back(i);
keys.emplace_back(op.key);
+ if (config.session_ids) {
+ active_config.session_ids->push_back(config.session_ids->at(i));
+ }
std::vector slice_sizes;
slice_sizes.reserve(op.slices.size());
@@ -2700,7 +2716,7 @@ void Client::StartBatchUpsert(std::vector& ops,
}
auto start_responses =
- master_client_.BatchUpsertStart(keys, slice_lengths, config);
+ master_client_.BatchUpsertStart(keys, slice_lengths, active_config);
// Ensure response size matches request size
if (start_responses.size() != active_indices.size()) {
@@ -3581,6 +3597,10 @@ std::vector> Client::BatchPut(
const std::vector& keys,
std::vector>& batched_slices,
const ReplicateConfig& config, const WriteBufferStager& stager) {
+ if (!config.ValidSessionIds(keys.size())) {
+ return std::vector>(
+ keys.size(), tl::unexpected(ErrorCode::INVALID_PARAMS));
+ }
ReplicateConfig client_cfg = AttachHostId(config);
if (protocol_ == "cxl") {
client_cfg.preferred_segment = local_hostname_;
@@ -3627,6 +3647,10 @@ std::vector> Client::BatchUpsert(
const std::vector& keys,
std::vector>& batched_slices,
const ReplicateConfig& config, const WriteBufferStager& stager) {
+ if (!config.ValidSessionIds(keys.size())) {
+ return std::vector>(
+ keys.size(), tl::unexpected(ErrorCode::INVALID_PARAMS));
+ }
ReplicateConfig client_cfg = AttachHostId(config);
if (protocol_ == "cxl") {
client_cfg.preferred_segment = local_hostname_;
@@ -3666,6 +3690,9 @@ Client::StartBatchPutForSizes(const std::vector& keys,
const ReplicateConfig& config) {
std::vector, ErrorCode>>
results(keys.size(), tl::unexpected(ErrorCode::INVALID_PARAMS));
+ if (!config.ValidSessionIds(keys.size())) {
+ return results;
+ }
if (keys.size() != object_sizes.size()) {
LOG(ERROR) << "StartBatchPutForSizes size mismatch: keys="
<< keys.size() << ", sizes=" << object_sizes.size();
@@ -4125,8 +4152,8 @@ std::vector> Client::BatchIsExist(
}
std::vector> Client::BatchProbeKey(
- const std::vector& keys) {
- auto response = master_client_.BatchProbeKey(keys);
+ const std::vector& keys, const GrantLeasePolicy& policy) {
+ auto response = master_client_.BatchProbeKey(keys, policy);
// Check if we got the expected number of responses
if (response.size() != keys.size()) {
diff --git a/mooncake-store/src/common/file_util.cpp b/mooncake-store/src/common/file_util.cpp
index be10319982..cc8840d2de 100644
--- a/mooncake-store/src/common/file_util.cpp
+++ b/mooncake-store/src/common/file_util.cpp
@@ -1,10 +1,16 @@
#include "common/file_util.h"
#include
+#include
#include
#include
#include
#include
#include
+#include
+#include
+
+#include
+#include
namespace mooncake {
namespace {
@@ -65,6 +71,49 @@ tl::expected WriteChunks(std::ofstream &file,
} // namespace
+void FileUtil::SortByCreationTime(std::vector &files) {
+ using TimeKey = std::tuple;
+ std::vector> timed_files;
+ timed_files.reserve(files.size());
+ for (const auto &entry : files) {
+ bool has_birth_time = false;
+ int64_t seconds = 0;
+ uint32_t nanoseconds = 0;
+#if defined(__linux__)
+ struct statx info{};
+ has_birth_time =
+ statx(AT_FDCWD, entry.path().c_str(), 0, STATX_BTIME, &info) == 0 &&
+ (info.stx_mask & STATX_BTIME) != 0;
+ if (has_birth_time) {
+ seconds = info.stx_btime.tv_sec;
+ nanoseconds = info.stx_btime.tv_nsec;
+ }
+#elif defined(__APPLE__)
+ struct stat info{};
+ has_birth_time = stat(entry.path().c_str(), &info) == 0;
+ if (has_birth_time) {
+ seconds = info.st_birthtimespec.tv_sec;
+ nanoseconds = info.st_birthtimespec.tv_nsec;
+ }
+#endif
+ timed_files.emplace_back(TimeKey{!has_birth_time, seconds, nanoseconds},
+ entry);
+ }
+
+ // No filesystem operations in the comparator: a disappearing file must
+ // not become equivalent to every other entry or change a comparison key.
+ std::sort(timed_files.begin(), timed_files.end(),
+ [](const auto &a, const auto &b) {
+ if (a.first != b.first) {
+ return a.first < b.first;
+ }
+ return a.second.path() < b.second.path();
+ });
+ for (size_t i = 0; i < files.size(); ++i) {
+ files[i] = std::move(timed_files[i].second);
+ }
+}
+
std::string FileUtil::ResolvePathFromKey(const std::string &key,
const std::string &root_dir,
const std::string &fsdir) {
diff --git a/mooncake-store/src/config/bucket_backend_config.cpp b/mooncake-store/src/config/bucket_backend_config.cpp
index 4417d596fc..f6c84a8c29 100644
--- a/mooncake-store/src/config/bucket_backend_config.cpp
+++ b/mooncake-store/src/config/bucket_backend_config.cpp
@@ -25,6 +25,10 @@ BucketBackendConfig BucketBackendConfig::FromEnvironment() {
BucketBackendConfig config;
using Variables = BucketBackendEnvironmentVariables;
+ config.flush_partial_buckets =
+ Environ::ReadOr(Variables::MOONCAKE_OFFLOAD_FLUSH_PARTIAL_BUCKETS,
+ config.flush_partial_buckets);
+
config.bucket_keys_limit =
Environ::ReadOr(Variables::MOONCAKE_OFFLOAD_BUCKET_KEYS_LIMIT,
config.bucket_keys_limit);
diff --git a/mooncake-store/src/dummy_client.cpp b/mooncake-store/src/dummy_client.cpp
index e17d4e4901..3667ee3cf5 100644
--- a/mooncake-store/src/dummy_client.cpp
+++ b/mooncake-store/src/dummy_client.cpp
@@ -1333,10 +1333,10 @@ int DummyClient::probeKey(const std::string& key) {
}
std::vector DummyClient::batchProbeKey(
- const std::vector& keys) {
+ const std::vector& keys, const GrantLeasePolicy& policy) {
auto internal_results =
- invoke_batch_rpc<&RealClient::batchProbeKey_internal, bool>(keys.size(),
- keys);
+ invoke_batch_rpc<&RealClient::batchProbeKey_internal, bool>(
+ keys.size(), keys, policy);
std::vector results;
results.reserve(internal_results.size());
diff --git a/mooncake-store/src/ha/leadership/backends/etcd/etcd_leader_coordinator.cpp b/mooncake-store/src/ha/leadership/backends/etcd/etcd_leader_coordinator.cpp
index 40a875baf7..2e4e7073bd 100644
--- a/mooncake-store/src/ha/leadership/backends/etcd/etcd_leader_coordinator.cpp
+++ b/mooncake-store/src/ha/leadership/backends/etcd/etcd_leader_coordinator.cpp
@@ -213,8 +213,11 @@ EtcdLeaderCoordinator::TryAcquireLeadership(const std::string& leader_address) {
return tl::make_unexpected(err);
}
+ const int64_t lease_ttl_sec = spec_.leader_lease_ttl_sec > 0
+ ? spec_.leader_lease_ttl_sec
+ : DEFAULT_MASTER_VIEW_LEASE_TTL_SEC;
EtcdLeaseId lease_id = 0;
- err = EtcdHelper::GrantLease(DEFAULT_MASTER_VIEW_LEASE_TTL_SEC, lease_id);
+ err = EtcdHelper::GrantLease(lease_ttl_sec, lease_id);
if (err != ErrorCode::OK) {
(void)ResetConnection();
return tl::make_unexpected(err);
@@ -257,7 +260,7 @@ EtcdLeaderCoordinator::TryAcquireLeadership(const std::string& leader_address) {
.view = MasterView{.leader_address = leader_address,
.view_version = view_version},
.owner_token = owner_token,
- .lease_ttl = std::chrono::seconds(DEFAULT_MASTER_VIEW_LEASE_TTL_SEC),
+ .lease_ttl = std::chrono::seconds(lease_ttl_sec),
};
{
diff --git a/mooncake-store/src/ha/leadership/master_service_supervisor.cpp b/mooncake-store/src/ha/leadership/master_service_supervisor.cpp
index 7a4e1a8c0c..a6c766bfe7 100644
--- a/mooncake-store/src/ha/leadership/master_service_supervisor.cpp
+++ b/mooncake-store/src/ha/leadership/master_service_supervisor.cpp
@@ -82,6 +82,7 @@ tl::expected BuildHABackendSpec(
.type = backend_type.value(),
.connstring = connstring,
.cluster_namespace = config.cluster_id,
+ .leader_lease_ttl_sec = config.ha_leader_lease_ttl_sec,
};
}
diff --git a/mooncake-store/src/kv_event/kv_event_publisher.cpp b/mooncake-store/src/kv_event/kv_event_publisher.cpp
index f680153a7c..47d20f8a08 100644
--- a/mooncake-store/src/kv_event/kv_event_publisher.cpp
+++ b/mooncake-store/src/kv_event/kv_event_publisher.cpp
@@ -54,7 +54,7 @@ size_t ComputeEventMapSize(bool is_stored, bool is_cleared, bool emit_legacy,
map_size += 1; // object_key
}
if (is_stored) {
- map_size += 3; // base_block_idx, parent_hash, token_ids
+ map_size += 4; // base_block_idx, parent_hash, token_ids, session_id
if (emit_legacy) {
map_size += 1; // parent_block_hash
}
@@ -174,7 +174,8 @@ KvEventPublisher::~KvEventPublisher() {
void KvEventPublisher::PublishStored(const std::string& object_key,
const std::string& medium,
const std::string& tenant_id,
- const std::string& group_id) {
+ const std::string& group_id,
+ const std::string& session_id) {
if (!config_.enabled) {
return;
}
@@ -185,7 +186,8 @@ void KvEventPublisher::PublishStored(const std::string& object_key,
}
std::vector events;
events.push_back(PendingEvent{EventKind::kStored, object_key, medium,
- NormalizeTenant(tenant_id), group_id});
+ NormalizeTenant(tenant_id), group_id,
+ session_id});
EnqueueBatch(std::move(events));
}
@@ -220,7 +222,7 @@ void KvEventPublisher::PublishCleared(const std::string& tenant_id) {
void KvEventPublisher::PublishCommitted(
const std::string& object_key,
const std::vector& current_media, const std::string& tenant_id,
- const std::string& group_id) {
+ const std::string& group_id, const std::string& session_id) {
if (!config_.enabled) {
return;
}
@@ -232,7 +234,7 @@ void KvEventPublisher::PublishCommitted(
// the previous contents.
for (const auto& medium : SortedMedia(NormalizeMedia(current_media))) {
events.push_back(PendingEvent{EventKind::kStored, object_key, medium,
- normalized_tenant, group_id});
+ normalized_tenant, group_id, session_id});
}
EnqueueBatch(std::move(events));
}
@@ -256,8 +258,8 @@ void KvEventPublisher::PublishObjectRemoved(
void KvEventPublisher::SyncObjectState(
const std::string& object_key,
const std::vector& current_media, const std::string& tenant_id,
- const std::string& group_id,
- const std::vector& previous_media) {
+ const std::string& group_id, const std::vector& previous_media,
+ const std::string& session_id) {
if (!config_.enabled) {
return;
}
@@ -275,7 +277,8 @@ void KvEventPublisher::SyncObjectState(
for (const auto& medium : SortedMedia(new_set)) {
if (!previous_set.contains(medium)) {
events.push_back(PendingEvent{EventKind::kStored, object_key,
- medium, normalized_tenant, group_id});
+ medium, normalized_tenant, group_id,
+ session_id});
}
}
EnqueueBatch(std::move(events));
@@ -446,6 +449,8 @@ void KvEventPublisher::PublishBatch(const std::vector& batch) {
}
if (is_stored) {
+ packer.pack("session_id");
+ PackOptionalString(packer, item.pending.session_id);
packer.pack("base_block_idx");
packer.pack_nil();
packer.pack("parent_hash");
diff --git a/mooncake-store/src/master.cpp b/mooncake-store/src/master.cpp
index 8f2446fc8b..65aa1adf42 100644
--- a/mooncake-store/src/master.cpp
+++ b/mooncake-store/src/master.cpp
@@ -51,6 +51,9 @@ static_assert(mooncake::DEFAULT_MAX_KV_SOFT_PIN_TTL_MS == 24 * 60 * 60 * 1000,
constexpr char kDefaultKvLeaseTtlFlagValue[] = "10000";
constexpr char kDefaultKvSoftPinTtlFlagValue[] = "1800000";
constexpr char kDefaultMaxKvSoftPinTtlFlagValue[] = "86400000";
+// Allowed range of --ha_leader_lease_ttl_sec.
+constexpr int64_t kMinHaLeaderLeaseTtlSec = 2;
+constexpr int64_t kMaxHaLeaderLeaseTtlSec = 60;
namespace {
@@ -322,6 +325,20 @@ DEFINE_uint32(kv_events_queue_capacity, 65536,
"the queue is full (0 = unbounded)");
DEFINE_string(ha_backend_type, "etcd",
"HA backend type, e.g. etcd | redis | k8s");
+DEFINE_int64(ha_leader_lease_ttl_sec,
+ mooncake::DEFAULT_MASTER_VIEW_LEASE_TTL_SEC,
+ "Leadership lease TTL in seconds for the etcd HA backend. A "
+ "longer TTL tolerates longer etcd apply stalls but delays "
+ "failover after a leader crash (range 2..60)");
+DEFINE_validator(ha_leader_lease_ttl_sec, [](const char* flagname,
+ int64_t value) {
+ if (value < kMinHaLeaderLeaseTtlSec || value > kMaxHaLeaderLeaseTtlSec) {
+ LOG(ERROR) << flagname << " must be in [" << kMinHaLeaderLeaseTtlSec
+ << ", " << kMaxHaLeaderLeaseTtlSec << "], got " << value;
+ return false;
+ }
+ return true;
+});
DEFINE_string(ha_backend_connstring, "",
"HA backend connection string. If unset, only backend_type=etcd "
"falls back to etcd_endpoints for backward compatibility");
@@ -655,6 +672,9 @@ void InitMasterConf(const mooncake::DefaultConfig& default_config,
default_config.GetString("ha_backend_connstring",
&master_config.ha_backend_connstring,
FLAGS_ha_backend_connstring);
+ default_config.GetInt64("ha_leader_lease_ttl_sec",
+ &master_config.ha_leader_lease_ttl_sec,
+ FLAGS_ha_leader_lease_ttl_sec);
default_config.GetString("etcd_endpoints", &master_config.etcd_endpoints,
FLAGS_etcd_endpoints);
default_config.GetString("cluster_id", &master_config.cluster_id,
@@ -1085,6 +1105,17 @@ void LoadConfigFromCmdline(mooncake::MasterConfig& master_config,
!conf_set) {
master_config.ha_backend_connstring = FLAGS_ha_backend_connstring;
}
+ if ((google::GetCommandLineFlagInfo("ha_leader_lease_ttl_sec", &info) &&
+ !info.is_default) ||
+ !conf_set) {
+ master_config.ha_leader_lease_ttl_sec = FLAGS_ha_leader_lease_ttl_sec;
+ }
+ if (master_config.ha_leader_lease_ttl_sec < kMinHaLeaderLeaseTtlSec ||
+ master_config.ha_leader_lease_ttl_sec > kMaxHaLeaderLeaseTtlSec) {
+ LOG(FATAL) << "ha_leader_lease_ttl_sec must be in ["
+ << kMinHaLeaderLeaseTtlSec << ", " << kMaxHaLeaderLeaseTtlSec
+ << "], got " << master_config.ha_leader_lease_ttl_sec;
+ }
if ((google::GetCommandLineFlagInfo("etcd_endpoints", &info) &&
!info.is_default) ||
!conf_set) {
@@ -1569,6 +1600,7 @@ int main(int argc, char* argv[]) {
<< ", offload_cap_ratio=" << master_config.offload_cap_ratio
<< ", ha_backend_type=" << master_config.ha_backend_type
<< ", ha_backend_connstring=" << ha_backend_connstring
+ << ", ha_leader_lease_ttl_sec=" << master_config.ha_leader_lease_ttl_sec
<< ", etcd_endpoints=" << master_config.etcd_endpoints
<< ", client_active_ttl_sec=" << master_config.client_active_ttl_sec
<< ", client_suspicion_ttl_sec="
diff --git a/mooncake-store/src/master_client.cpp b/mooncake-store/src/master_client.cpp
index bf8a1a92dd..4bdcd2181b 100644
--- a/mooncake-store/src/master_client.cpp
+++ b/mooncake-store/src/master_client.cpp
@@ -565,12 +565,13 @@ tl::expected MasterClient::ProbeKey(
}
std::vector> MasterClient::BatchProbeKey(
- const std::vector& object_keys) {
+ const std::vector& object_keys,
+ const GrantLeasePolicy& policy) {
ScopedVLogTimer timer(1, "MasterClient::BatchProbeKey");
timer.LogRequest("keys_count=", object_keys.size());
auto result = invoke_batch_rpc<&WrappedMasterService::BatchProbeKey, bool>(
- object_keys.size(), object_keys, tenant_id_.value());
+ object_keys.size(), policy, object_keys, tenant_id_.value());
timer.LogResponse("result=", result.size(), " keys");
return result;
}
diff --git a/mooncake-store/src/master_service.cpp b/mooncake-store/src/master_service.cpp
index 2d10751fa5..6deb936492 100644
--- a/mooncake-store/src/master_service.cpp
+++ b/mooncake-store/src/master_service.cpp
@@ -3023,8 +3023,55 @@ std::vector> MasterService::BatchExistKey(
}
std::vector> MasterService::BatchProbeKey(
- const std::vector& keys, const TenantId& tenant_id) {
- return BatchExistKeyImpl(keys, tenant_id, /*grant_lease=*/false);
+ const GrantLeasePolicy& policy, const std::vector& keys,
+ const TenantId& tenant_id) {
+ if (keys.empty()) return {};
+ std::string_view invalid_reason;
+ if (policy.lease_mode != ProbeLeaseMode::None &&
+ policy.lease_mode != ProbeLeaseMode::LastHitOnly) {
+ invalid_reason = "unknown lease mode";
+ } else if (policy.lease_mode == ProbeLeaseMode::LastHitOnly) {
+ if (policy.candidate_size == 0) {
+ invalid_reason = "candidate_size is zero";
+ } else if (keys.size() % policy.candidate_size != 0) {
+ invalid_reason = "key count is not divisible by candidate_size";
+ }
+ }
+ if (!invalid_reason.empty()) {
+ LOG(WARNING) << "BatchProbeKey: " << invalid_reason
+ << ", lease_mode=" << static_cast(policy.lease_mode)
+ << ", candidate_size=" << policy.candidate_size
+ << ", key_count=" << keys.size();
+ return std::vector>(
+ keys.size(), tl::make_unexpected(ErrorCode::INVALID_PARAMS));
+ }
+
+ auto results = BatchExistKeyImpl(keys, tenant_id, /*grant_lease=*/false);
+ if (policy.lease_mode == ProbeLeaseMode::None) return results;
+
+ const auto is_readable = [](const auto& result) {
+ return result.has_value() && result.value();
+ };
+ for (size_t end = keys.size(); end > 0; end -= policy.candidate_size) {
+ const size_t begin = end - policy.candidate_size;
+ if (!std::all_of(results.begin() + begin, results.begin() + end,
+ is_readable)) {
+ continue;
+ }
+
+ const std::vector candidate(keys.begin() + begin,
+ keys.begin() + end);
+ auto leased =
+ BatchExistKeyImpl(candidate, tenant_id, /*grant_lease=*/true);
+ // Recheck under the existing per-shard locks. Publish the fresh
+ // results so a candidate lost since probing cannot remain all-true.
+ // Failed candidates may retain partial leases until their TTL expires.
+ std::copy(leased.begin(), leased.end(), results.begin() + begin);
+ if (std::all_of(leased.begin(), leased.end(), is_readable)) {
+ break;
+ }
+ }
+ return results;
}
std::vector> MasterService::BatchExistKeyImpl(
@@ -3617,6 +3664,8 @@ tl::expected MasterService::InstallLegacyStandbyObjects(
standby_meta.data_type, standby_meta.group_id,
object.tenant_id, object.user_key));
(void)inserted;
+ it->second.metadata.session_id =
+ standby_meta.session_id.value_or("");
if (!standby_meta.group_id.empty()) {
it->second.metadata.lease_ = RegisterGroupMember(
object.tenant_id, object.user_key, standby_meta.group_id);
@@ -4086,6 +4135,8 @@ tl::expected MasterService::RestoreFromStandbyState(
standby_meta.data_type, standby_meta.group_id,
object.tenant_id, object.user_key));
(void)inserted;
+ it->second.metadata.session_id =
+ standby_meta.session_id.value_or("");
if (!standby_meta.group_id.empty()) {
it->second.metadata.lease_ =
RegisterGroupMember(object.tenant_id, object.user_key,
@@ -5336,6 +5387,8 @@ auto MasterService::InsertMetadata(
return tl::make_unexpected(ErrorCode::INTERNAL_ERROR);
}
}
+ it->second.metadata.BeginSessionTag(config.SessionId(),
+ eligible_replica_ids);
it->second.metadata.BeginSoftPinAction(soft_pin_request,
std::move(eligible_replica_ids));
if (deadline_to_index) {
@@ -5413,11 +5466,10 @@ auto MasterService::PutStart(const UUID& client_id, const std::string& key,
<< ", key_size=" << key.size() << ", error=invalid_params";
return tl::make_unexpected(ErrorCode::INVALID_PARAMS);
}
- if (config.dfs_replica_num > 1 ||
- (config.dfs_replica_num > 0 && config.replica_num == 0)) {
- LOG(ERROR) << "key=" << key << ", replica_num=" << config.replica_num
+ if (config.dfs_replica_num > 1) {
+ LOG(ERROR) << "key=" << key
<< ", dfs_replica_num=" << config.dfs_replica_num
- << ", error=invalid_dfs_replica_config";
+ << ", error=dfs_replica_num_must_be_0_or_1";
return tl::make_unexpected(ErrorCode::INVALID_PARAMS);
}
if (config.dfs_replica_num > 0 && !object_id.tenant_id.IsDefault()) {
@@ -5472,6 +5524,9 @@ auto MasterService::PutStart(const UUID& client_id, const std::string& key,
VLOG(1) << "key=" << key << ", value_length=" << slice_length
<< ", config=" << config << ", action=put_start_begin";
+ if (!config.ValidSessionIds(1)) {
+ return tl::make_unexpected(ErrorCode::INVALID_PARAMS);
+ }
auto group_id_result = GetGroupIdForKey(config, 1, 0);
if (!group_id_result) {
return tl::make_unexpected(group_id_result.error());
@@ -5692,6 +5747,7 @@ auto MasterService::PutEnd(const UUID& client_id, const ObjectMeta& object_meta,
metadata.PendingSoftPinOwnsReplica(replica.id())) {
completed_pending_replica = true;
}
+ metadata.CommitSessionTag(replica.id());
replica.mark_complete();
if (replica.is_dfs_replica() && dfs_allocator_) {
const auto& desc = replica.get_dfs_descriptor();
@@ -5966,6 +6022,7 @@ auto MasterService::PutRevoke(const UUID& client_id, const std::string& key,
r.mark_removed();
});
metadata.ClearPendingSoftPinIfNoViableReplica();
+ metadata.ClearSessionTagIfNoViableReplica();
tl::expected persist_result;
if (remaining.empty()) {
@@ -5998,6 +6055,7 @@ auto MasterService::PutRevoke(const UUID& client_id, const std::string& key,
const uint64_t before_charge = CompletedMemoryQuotaCharge(metadata);
EraseReplicasWithCacheTotalAccounting(metadata, target_pred);
metadata.ClearPendingSoftPinIfNoViableReplica();
+ metadata.ClearSessionTagIfNoViableReplica();
const uint64_t after_charge = CompletedMemoryQuotaCharge(metadata);
if (enable_multi_tenants_ && before_charge > after_charge) {
auto release_result = metadata.quota_ledger.ReleaseCommitted(
@@ -6096,11 +6154,10 @@ auto MasterService::UpsertStart(const UUID& client_id, const std::string& key,
<< ", key_size=" << key.size() << ", error=invalid_params";
return tl::make_unexpected(ErrorCode::INVALID_PARAMS);
}
- if (config.dfs_replica_num > 1 ||
- (config.dfs_replica_num > 0 && config.replica_num == 0)) {
- LOG(ERROR) << "key=" << key << ", replica_num=" << config.replica_num
+ if (config.dfs_replica_num > 1) {
+ LOG(ERROR) << "key=" << key
<< ", dfs_replica_num=" << config.dfs_replica_num
- << ", error=invalid_dfs_replica_config";
+ << ", error=dfs_replica_num_must_be_0_or_1";
return tl::make_unexpected(ErrorCode::INVALID_PARAMS);
}
if (config.dfs_replica_num > 0 && !object_id.tenant_id.IsDefault()) {
@@ -6158,6 +6215,9 @@ auto MasterService::UpsertStart(const UUID& client_id, const std::string& key,
VLOG(1) << "key=" << key << ", value_length=" << slice_length
<< ", config=" << config << ", action=upsert_start_begin";
+ if (!config.ValidSessionIds(1)) {
+ return tl::make_unexpected(ErrorCode::INVALID_PARAMS);
+ }
auto group_id_result = GetGroupIdForKey(config, 1, 0);
if (!group_id_result) {
return tl::make_unexpected(group_id_result.error());
@@ -6169,6 +6229,7 @@ auto MasterService::UpsertStart(const UUID& client_id, const std::string& key,
bool dfs_allocation_failed = false;
ReplicateConfig allocation_config = config;
std::string allocation_group_id = group_id;
+ std::string allocation_session_id;
std::optional
allocation_committed_soft_pin_timeout;
auto admit =
@@ -6293,6 +6354,7 @@ auto MasterService::UpsertStart(const UUID& client_id, const std::string& key,
auto processing_replicas =
metadata.PopReplicas(&Replica::fn_is_processing);
metadata.ClearPendingSoftPinAction();
+ metadata.pending_session_tag.reset();
if (!processing_replicas.empty()) {
FreeDfsReplicas(key, processing_replicas);
std::lock_guard lock(discarded_replicas_mutex_);
@@ -6305,6 +6367,7 @@ auto MasterService::UpsertStart(const UUID& client_id, const std::string& key,
// If no COMPLETE replicas survive the preemption, this key
// effectively does not exist β fall through to Case A.
if (!metadata.HasReplica(&Replica::fn_is_completed)) {
+ allocation_session_id = metadata.session_id;
allocation_committed_soft_pin_timeout =
metadata.GetCommittedSoftPinTimeout();
if (allocation_committed_soft_pin_timeout &&
@@ -6329,12 +6392,17 @@ auto MasterService::UpsertStart(const UUID& client_id, const std::string& key,
// routed by hash(tenant, key); group_id does not affect routing.
if (it == tenant_state.metadata.end()) {
VLOG(1) << "key=" << key << ", action=upsert_start_case_a";
- return AllocateAndInsertMetadata(
+ auto result = AllocateAndInsertMetadata(
shard, client_id, key, slice_length, allocation_config,
writer_host_id, allocation_group_id, object_id.tenant_id,
now, *soft_pin_request,
allocation_committed_soft_pin_timeout,
&dfs_allocation_failed);
+ if (result) {
+ tenant_state.metadata.at(key).metadata.session_id =
+ allocation_session_id;
+ }
+ return result;
} else {
// --- Step 2: key exists with COMPLETE replicas β Case B or C
// ---
@@ -6417,6 +6485,8 @@ auto MasterService::UpsertStart(const UUID& client_id, const std::string& key,
eligible_replica_ids.insert(replica.id());
replica.mark_processing();
});
+ metadata.BeginSessionTag(config.SessionId(),
+ eligible_replica_ids);
metadata.BeginSoftPinAction(
*soft_pin_request, std::move(eligible_replica_ids));
SyncCacheTotalAccounting(metadata);
@@ -6460,6 +6530,7 @@ auto MasterService::UpsertStart(const UUID& client_id, const std::string& key,
}
allocation_group_id = metadata.group_id;
+ allocation_session_id = metadata.session_id;
const auto previous_kv_media = KvMediaSnapshot(metadata);
TenantQuotaLedger replacement_charge;
auto* quota_account = GetBoundTenantQuotaHandle(tenant_state);
@@ -6567,6 +6638,9 @@ auto MasterService::UpsertStart(const UUID& client_id, const std::string& key,
return allocate_result;
}
auto new_it = tenant_state.metadata.find(key);
+ if (new_it != tenant_state.metadata.end()) {
+ new_it->second.metadata.session_id = allocation_session_id;
+ }
if (has_replacement_charge) {
if (new_it == tenant_state.metadata.end()) {
auto rollback_result =
@@ -6669,6 +6743,9 @@ MasterService::BatchUpsertStart(const UUID& client_id,
tl::expected, ErrorCode>>(
keys.size(), tl::make_unexpected(ErrorCode::INVALID_PARAMS));
}
+ if (!config.ValidSessionIds(keys.size())) {
+ return {keys.size(), tl::make_unexpected(ErrorCode::INVALID_PARAMS)};
+ }
if (config.group_ids.has_value() &&
config.group_ids->size() != keys.size()) {
LOG(ERROR) << "BatchUpsertStart: group_ids.size()="
@@ -6681,8 +6758,9 @@ MasterService::BatchUpsertStart(const UUID& client_id,
std::vector, ErrorCode>>
results;
results.reserve(keys.size());
+ ReplicateConfig key_config = config;
for (size_t i = 0; i < keys.size(); ++i) {
- auto key_config = config.ForSingleKey(i);
+ key_config.SetKeyIds(config, i);
results.emplace_back(UpsertStart(client_id, keys[i], tenant_id,
slice_lengths[i], key_config));
}
@@ -10380,6 +10458,7 @@ void MasterService::DiscardExpiredProcessingReplicas(
if (!metadata.IsValid() ||
metadata.AllReplicas(&Replica::fn_is_completed)) {
metadata.ClearPendingSoftPinIfNoViableReplica();
+ metadata.ClearSessionTagIfNoViableReplica();
if (!metadata.IsValid()) {
EraseMetadata(tenant_state, it, tenant_it->first,
QuotaEraseMode::kFull, &shard);
@@ -10447,6 +10526,7 @@ void MasterService::DiscardExpiredProcessingReplicas(
auto replicas =
metadata.PopReplicas(&Replica::fn_is_processing);
metadata.ClearPendingSoftPinIfNoViableReplica();
+ metadata.ClearSessionTagIfNoViableReplica();
if (!replicas.empty()) {
FreeDfsReplicas(it->first, replicas);
discarded_replicas.emplace_back(std::move(replicas), ttl);
@@ -13333,7 +13413,8 @@ void MasterService::PublishKvStored(const std::string& key,
return;
}
kv_event_publisher_->PublishCommitted(key, KvMediaForMetadata(metadata),
- tenant_id.value(), metadata.group_id);
+ tenant_id.value(), metadata.group_id,
+ metadata.session_id);
}
void MasterService::SyncKvObjectState(
@@ -13356,8 +13437,8 @@ void MasterService::SyncKvObjectState(
return;
}
kv_event_publisher_->SyncObjectState(key, current_media, tenant_id.value(),
- metadata.group_id,
- previous_media_hint);
+ metadata.group_id, previous_media_hint,
+ metadata.session_id);
}
void MasterService::PublishKvRemoved(
@@ -13558,6 +13639,7 @@ std::string MasterService::SerializeMetadataForOpLog(
payload.client_id = metadata.client_id;
payload.size = metadata.TotalLength();
payload.group_id = metadata.group_id;
+ payload.session_id = metadata.session_id;
payload.data_type = metadata.data_type;
payload.hard_pinned = metadata.IsHardPinned();
@@ -13584,6 +13666,7 @@ std::string MasterService::SerializeMetadataForOpLogWithoutMemReplicas(
payload.client_id = metadata.client_id;
payload.size = metadata.TotalLength();
payload.group_id = metadata.group_id;
+ payload.session_id = metadata.session_id;
payload.data_type = metadata.data_type;
payload.hard_pinned = metadata.IsHardPinned();
@@ -13608,6 +13691,7 @@ std::string MasterService::SerializeMetadataForOpLogFromReplicaDescriptors(
payload.size = metadata.TotalLength();
payload.replicas = replicas;
payload.group_id = metadata.group_id;
+ payload.session_id = metadata.session_id;
payload.data_type = metadata.data_type;
payload.hard_pinned = metadata.IsHardPinned();
auto result = struct_pack::serialize(payload);
diff --git a/mooncake-store/src/real_client.cpp b/mooncake-store/src/real_client.cpp
index b8ecba6204..937d38d9cb 100644
--- a/mooncake-store/src/real_client.cpp
+++ b/mooncake-store/src/real_client.cpp
@@ -2884,9 +2884,9 @@ int RealClient::probeKey(const std::string &key) {
}
}
-std::vector RealClient::batchProbeKey(
- const std::vector &keys) {
- auto internal_results = batchProbeKey_internal(keys);
+std::vector RealClient::batchProbeKey(const std::vector &keys,
+ const GrantLeasePolicy &policy) {
+ auto internal_results = batchProbeKey_internal(keys, policy);
std::vector results;
results.reserve(internal_results.size());
@@ -6725,7 +6725,7 @@ tl::expected RealClient::probeKey_internal(
}
std::vector> RealClient::batchProbeKey_internal(
- const std::vector &keys) {
+ const std::vector &keys, const GrantLeasePolicy &policy) {
if (!client_) {
LOG(ERROR) << "Client is not initialized";
return std::vector>(
@@ -6738,7 +6738,7 @@ std::vector> RealClient::batchProbeKey_internal(
}
// Call client BatchProbeKey and return the vector directly
- return client_->BatchProbeKey(keys);
+ return client_->BatchProbeKey(keys, policy);
}
int RealClient::put_from_with_metadata(const std::string &key, void *buffer,
@@ -7490,6 +7490,9 @@ std::vector RealClient::batch_put_session_start(
return std::vector(
keys.size(), static_cast(toInt(ErrorCode::INVALID_PARAMS)));
}
+ if (!config.ValidSessionIds(keys.size())) {
+ return std::vector(keys.size(), toInt(ErrorCode::INVALID_PARAMS));
+ }
if (config.group_ids.has_value() &&
config.group_ids->size() != keys.size()) {
LOG(ERROR) << "batch_put_session_start: group_ids.size()="
@@ -7541,6 +7544,15 @@ std::vector RealClient::batch_put_session_start(
start_config.group_ids = std::move(filtered_group_ids);
}
+ if (start_config.session_ids) {
+ std::vector ids;
+ ids.reserve(start_indices.size());
+ for (size_t idx : start_indices) {
+ ids.push_back(start_config.session_ids->at(idx));
+ }
+ start_config.session_ids = std::move(ids);
+ }
+
auto start_responses =
client_->StartBatchPutForSizes(start_keys, start_sizes, start_config);
std::vector keys_to_revoke;
diff --git a/mooncake-store/src/rpc_service.cpp b/mooncake-store/src/rpc_service.cpp
index a702b709bf..dfced05598 100644
--- a/mooncake-store/src/rpc_service.cpp
+++ b/mooncake-store/src/rpc_service.cpp
@@ -181,7 +181,8 @@ tl::expected WrappedMasterService::ProbeKey(
}
std::vector> WrappedMasterService::BatchProbeKey(
- const std::vector& keys, const std::string& tenant_id) {
+ const GrantLeasePolicy& policy, const std::vector& keys,
+ const std::string& tenant_id) {
ScopedVLogTimer timer(1, "BatchProbeKey");
timer.LogRequest("keys_count=", keys.size());
@@ -189,7 +190,8 @@ std::vector> WrappedMasterService::BatchProbeKey(
master_service_.IsTenantQuotaEnabled() ? std::string_view(tenant_id)
: TenantId::kDefaultValue,
keys.size(), [&](const TenantId& resolved_tenant_id) {
- return master_service_.BatchProbeKey(keys, resolved_tenant_id);
+ return master_service_.BatchProbeKey(policy, keys,
+ resolved_tenant_id);
});
}
@@ -517,6 +519,9 @@ WrappedMasterService::BatchPutStart(const UUID& client_id,
<< " != slice_lengths.size()=" << slice_lengths.size();
results.assign(keys.size(),
tl::make_unexpected(ErrorCode::INVALID_PARAMS));
+ } else if (!config.ValidSessionIds(keys.size())) {
+ results.assign(keys.size(),
+ tl::make_unexpected(ErrorCode::INVALID_PARAMS));
} else if (config.group_ids.has_value() &&
config.group_ids->size() != keys.size()) {
LOG(ERROR) << "BatchPutStart: group_ids.size()="
@@ -530,10 +535,10 @@ WrappedMasterService::BatchPutStart(const UUID& client_id,
} else if (config.prefer_alloc_in_same_node) {
ReplicateConfig new_config = config;
for (size_t i = 0; i < keys.size(); ++i) {
- auto key_config = new_config.ForSingleKey(i);
+ new_config.SetKeyIds(config, i);
auto result = master_service_.PutStart(
client_id, keys[i], resolved_tenant_id.value(),
- slice_lengths[i], key_config);
+ slice_lengths[i], new_config);
results.emplace_back(result);
if ((i == 0) && result.has_value()) {
std::string preferred_segment;
@@ -552,8 +557,9 @@ WrappedMasterService::BatchPutStart(const UUID& client_id,
}
}
} else {
+ ReplicateConfig key_config = config;
for (size_t i = 0; i < keys.size(); ++i) {
- auto key_config = config.ForSingleKey(i);
+ key_config.SetKeyIds(config, i);
results.emplace_back(master_service_.PutStart(
client_id, keys[i], resolved_tenant_id.value(),
slice_lengths[i], key_config));
@@ -772,6 +778,9 @@ WrappedMasterService::BatchUpsertStart(
<< " != slice_lengths.size()=" << slice_lengths.size();
results.assign(keys.size(),
tl::make_unexpected(ErrorCode::INVALID_PARAMS));
+ } else if (!config.ValidSessionIds(keys.size())) {
+ results.assign(keys.size(),
+ tl::make_unexpected(ErrorCode::INVALID_PARAMS));
} else if (config.group_ids.has_value() &&
config.group_ids->size() != keys.size()) {
LOG(ERROR) << "BatchUpsertStart: group_ids.size()="
diff --git a/mooncake-store/src/storage/distributed/distributed_storage_backend.cpp b/mooncake-store/src/storage/distributed/distributed_storage_backend.cpp
index 68549fd188..f436638874 100644
--- a/mooncake-store/src/storage/distributed/distributed_storage_backend.cpp
+++ b/mooncake-store/src/storage/distributed/distributed_storage_backend.cpp
@@ -216,6 +216,24 @@ tl::expected DistributedStorageBackend::Init() {
tl::expected
DistributedStorageBackend::GetOrOpenShard(
const DistributedFSDescriptor& descriptor) {
+ if (descriptor.shard_idx < 0) {
+ return tl::make_unexpected(ErrorCode::INVALID_PARAMS);
+ }
+
+ ShardFile* shard = nullptr;
+ {
+ std::lock_guard cache_lock(shard_files_mutex_);
+ auto existing = shard_files_.find(descriptor.shard_idx);
+ if (existing != shard_files_.end()) shard = existing->second.get();
+ }
+ if (shard) {
+ std::lock_guard shard_lock(shard->mutex);
+ // Only the exact path validated when this fd was opened can bypass
+ // normalization. Aliases and changed descriptors take the full
+ // validation path below. Cache entries live until backend teardown.
+ if (shard->fd >= 0 && shard->path == descriptor.file_path) return shard;
+ }
+
const auto path =
std::filesystem::path(descriptor.file_path).lexically_normal();
auto root = std::filesystem::path(root_dir_).lexically_normal();
@@ -223,13 +241,11 @@ DistributedStorageBackend::GetOrOpenShard(
auto parent = path.parent_path();
if (parent.empty()) parent = ".";
const auto index = ParseShardFileName(path.filename().string());
- if (descriptor.shard_idx < 0 || !index || *index != descriptor.shard_idx ||
- parent != root) {
+ if (!index || *index != descriptor.shard_idx || parent != root) {
return tl::make_unexpected(ErrorCode::INVALID_PARAMS);
}
- ShardFile* shard;
- {
+ if (!shard) {
std::lock_guard cache_lock(shard_files_mutex_);
auto existing = shard_files_.find(descriptor.shard_idx);
if (existing == shard_files_.end()) {
diff --git a/mooncake-store/src/storage_backend.cpp b/mooncake-store/src/storage_backend.cpp
index bde6e254e9..b296893fcd 100644
--- a/mooncake-store/src/storage_backend.cpp
+++ b/mooncake-store/src/storage_backend.cpp
@@ -136,23 +136,7 @@ tl::expected StorageBackend::Init(uint64_t quota_bytes = 0) {
<< e.what();
return tl::make_unexpected(ErrorCode::INTERNAL_ERROR);
}
- std::sort(existing_files.begin(), existing_files.end(),
- [](const auto& a, const auto& b) {
- struct statx stx_a, stx_b;
- bool success_a = (statx(AT_FDCWD, a.path().c_str(), 0,
- STATX_BTIME, &stx_a) == 0);
- bool success_b = (statx(AT_FDCWD, b.path().c_str(), 0,
- STATX_BTIME, &stx_b) == 0);
-
- if (!success_a || !success_b) {
- return false;
- }
-
- if (stx_a.stx_btime.tv_sec == stx_b.stx_btime.tv_sec) {
- return stx_a.stx_btime.tv_nsec < stx_b.stx_btime.tv_nsec;
- }
- return stx_a.stx_btime.tv_sec < stx_b.stx_btime.tv_sec;
- });
+ FileUtil::SortByCreationTime(existing_files);
bool eviction_needed = false;
{
std::unique_lock space_lock(space_mutex_);
@@ -2347,6 +2331,14 @@ tl::expected BucketStorageBackend::GroupOffloadingKeysByBucket(
while (static_cast(bucket_keys.size()) <
bucket_backend_config_.bucket_keys_limit) {
if (it == offloading_objects.cend()) {
+ if (bucket_backend_config_.flush_partial_buckets) {
+ // Keep these keys in the current cycle, while FileStorage
+ // still has their tasks and source replicas are pinned.
+ if (!bucket_keys.empty()) {
+ buckets_keys.push_back(std::move(bucket_keys));
+ }
+ return {};
+ }
for (const auto& bucket_object : bucket_objects) {
ungrouped_offloading_objects.emplace(bucket_object.first,
bucket_object.second);
diff --git a/mooncake-store/tests/CMakeLists.txt b/mooncake-store/tests/CMakeLists.txt
index 5c0d82a325..911c0db38e 100644
--- a/mooncake-store/tests/CMakeLists.txt
+++ b/mooncake-store/tests/CMakeLists.txt
@@ -68,6 +68,7 @@ set_tests_properties(replica_selection_env_opt_in_test
add_store_test(eviction_strategy_test eviction_strategy_test.cpp)
add_store_test(deadline_scheduler_test deadline_scheduler_test.cpp)
add_store_test(background_worker_test background_worker_test.cpp)
+add_store_test(session_tag_test session_tag_test.cpp)
add_store_test(kv_event_publisher_test kv_event_publisher_test.cpp)
if(ENABLE_KV_EVENTS)
target_compile_definitions(kv_event_publisher_test
@@ -77,6 +78,8 @@ endif()
add_subdirectory(master_service)
add_store_test(master_service_test master_service_test.cpp)
+add_store_test(master_service_probe_policy_test
+ master_service_probe_policy_test.cpp)
add_store_test(master_metadata_config_test master_metadata_config_test.cpp)
add_store_test(dfs_enablement_config_test dfs_enablement_config_test.cpp)
add_store_test(master_service_weight_management_test
@@ -154,7 +157,7 @@ add_test(
NAME object_checksum_client_test
COMMAND
client_integration_test
- "--gtest_filter=ObjectChecksumTest.*:ClientIntegrationTest.ObjectChecksumRejectsCorruptedObject:ClientIntegrationTest.BatchPutPreservesObjectChecksumPairing"
+ "--gtest_filter=ObjectChecksumTest.*:ClientIntegrationTest.ObjectChecksumRejectsCorruptedObject:ClientIntegrationTest.BatchPutPreservesObjectChecksumPairing:ClientIntegrationTest.TaggedBatchWritesSkipChecksumFailures"
)
set_tests_properties(object_checksum_client_test
PROPERTIES ENVIRONMENT "MOONCAKE_STORE_CHECKSUM=1")
diff --git a/mooncake-store/tests/bucket_backend_config_test.cpp b/mooncake-store/tests/bucket_backend_config_test.cpp
index 3991508657..04a6bec8b3 100644
--- a/mooncake-store/tests/bucket_backend_config_test.cpp
+++ b/mooncake-store/tests/bucket_backend_config_test.cpp
@@ -49,6 +49,8 @@ struct BucketBackendEnvironment {
"MOONCAKE_OFFLOAD_BUCKET_DISK_SCAN_CACHE_MS"};
ScopedEnvVar eviction_policy{"MOONCAKE_OFFLOAD_BUCKET_EVICTION_POLICY"};
ScopedEnvVar legacy_eviction_policy{"MOONCAKE_BUCKET_EVICTION_POLICY"};
+ ScopedEnvVar flush_partial_buckets{
+ "MOONCAKE_OFFLOAD_FLUSH_PARTIAL_BUCKETS"};
};
class BucketBackendConfigTest : public ::testing::Test {
@@ -65,6 +67,33 @@ TEST_F(BucketBackendConfigTest, UsesExistingDefaultsWhenEnvironmentIsUnset) {
EXPECT_EQ(config.max_physical_bytes, 0);
EXPECT_EQ(config.disk_scan_cache_ms, 500);
EXPECT_EQ(config.eviction_policy, BucketEvictionPolicy::FIFO);
+ EXPECT_FALSE(config.flush_partial_buckets);
+}
+
+TEST_F(BucketBackendConfigTest, ReadsPartialFlushBooleanValues) {
+ struct Case {
+ const char* value;
+ bool enabled;
+ };
+ for (const auto& entry :
+ {Case{"1", true}, Case{"true", true}, Case{"0", false},
+ Case{"FALSE", false}, Case{"invalid", false}}) {
+ SCOPED_TRACE(entry.value);
+ env.flush_partial_buckets.Set(entry.value);
+ EXPECT_EQ(BucketBackendConfig::FromEnvironment().flush_partial_buckets,
+ entry.enabled);
+ }
+}
+
+TEST_F(BucketBackendConfigTest, PartialFlushIsReadPerConfiguration) {
+ env.flush_partial_buckets.Set("1");
+ const auto enabled = BucketBackendConfig::FromEnvironment();
+ EXPECT_TRUE(enabled.flush_partial_buckets);
+ env.flush_partial_buckets.Set("0");
+ EXPECT_FALSE(BucketBackendConfig::FromEnvironment().flush_partial_buckets);
+ env.flush_partial_buckets.Unset();
+ EXPECT_FALSE(BucketBackendConfig::FromEnvironment().flush_partial_buckets);
+ EXPECT_TRUE(enabled.flush_partial_buckets);
}
TEST_F(BucketBackendConfigTest, ReadsIndependentValidValues) {
diff --git a/mooncake-store/tests/client_integration_test.cpp b/mooncake-store/tests/client_integration_test.cpp
index dad8595511..d14f03c313 100644
--- a/mooncake-store/tests/client_integration_test.cpp
+++ b/mooncake-store/tests/client_integration_test.cpp
@@ -473,6 +473,42 @@ TEST_F(ClientIntegrationTest, BatchPutPreservesObjectChecksumPairing) {
}
}
+TEST_F(ClientIntegrationTest, TaggedBatchWritesSkipChecksumFailures) {
+ if (!ClientObjectChecksumConfig::IsEnabledAtFirstUse()) {
+ GTEST_SKIP() << "MOONCAKE_STORE_CHECKSUM is not enabled";
+ }
+ void* buffer = client_buffer_allocator_->allocate(4);
+ ASSERT_NE(buffer, nullptr);
+ std::memcpy(buffer, "data", 4);
+ for (bool upsert : {false, true}) {
+ const std::vector keys = {
+ "invalid-slice", upsert ? "tagged-upsert" : "tagged-put"};
+ std::vector> slices{{Slice{nullptr, 1}},
+ {Slice{buffer, 4}}};
+ ReplicateConfig config;
+ config.session_ids = std::vector{"invalid", "valid"};
+ auto results = upsert ? test_client_->BatchUpsert(keys, slices, config)
+ : test_client_->BatchPut(keys, slices, config);
+ ASSERT_EQ(results.size(), 2);
+ ASSERT_FALSE(results[0]);
+ EXPECT_EQ(results[0].error(), ErrorCode::INVALID_PARAMS);
+ ASSERT_TRUE(results[1]) << toString(results[1].error());
+ auto query = test_client_->Query(keys[1]);
+ ASSERT_TRUE(query);
+ EXPECT_EQ(query->object_checksum, ComputeCrcChecksum("data", 4));
+
+ // A short tag vector must not become valid after filtering a key.
+ config.session_ids = std::vector{"wrong"};
+ results = upsert ? test_client_->BatchUpsert(keys, slices, config)
+ : test_client_->BatchPut(keys, slices, config);
+ for (const auto& result : results) {
+ ASSERT_FALSE(result);
+ EXPECT_EQ(result.error(), ErrorCode::INVALID_PARAMS);
+ }
+ }
+ client_buffer_allocator_->deallocate(buffer, 4);
+}
+
// Test Remove operation
TEST_F(ClientIntegrationTest, RemoveOperation) {
const std::string test_data = "Test data for removal";
diff --git a/mooncake-store/tests/dfs_immutable_bucket_client_test.cpp b/mooncake-store/tests/dfs_immutable_bucket_client_test.cpp
index fd031b053d..22262b112d 100644
--- a/mooncake-store/tests/dfs_immutable_bucket_client_test.cpp
+++ b/mooncake-store/tests/dfs_immutable_bucket_client_test.cpp
@@ -242,6 +242,60 @@ TEST_F(DfsImmutableBucketClientTest, ExhaustionEvictsWholeBucketAndRetries) {
EXPECT_EQ(third_query->replicas.size(), 2u);
}
+TEST_F(DfsImmutableBucketClientTest, DfsOnlyObjectsLiveAndDieWithTheirBucket) {
+ ReplicateConfig dfs_only;
+ dfs_only.replica_num = 0;
+ dfs_only.dfs_replica_num = 1;
+ auto expect_value = [this](const std::string& key,
+ const std::string& expected) {
+ auto query = writer_->Query(key);
+ ASSERT_TRUE(query) << key;
+ ASSERT_EQ(query->replicas.size(), 1u) << key;
+ ASSERT_TRUE(query->replicas[0].is_dfs_replica()) << key;
+ // The fixture's zero lease TTL makes Client::Get report LEASE_EXPIRED,
+ // so read the descriptor through the backend like the other tests.
+ std::string output(expected.size(), '\0');
+ auto reads =
+ backend_->BatchRead({{key,
+ query->replicas[0].get_dfs_descriptor(),
+ {{output.data(), output.size()}}}});
+ ASSERT_TRUE(reads[0]) << key;
+ EXPECT_EQ(output, expected) << key;
+ };
+
+ std::string first(4096, '1');
+ std::string second(4096, '2');
+ std::string third(4096, '3');
+ auto first_slices = Slices(first);
+ auto second_slices = Slices(second);
+ auto third_slices = Slices(third);
+ ASSERT_TRUE(writer_->Put("first", first_slices, dfs_only));
+ ASSERT_TRUE(writer_->Put("second", second_slices, dfs_only));
+ // Both buckets are full: the third write forces a whole-bucket eviction.
+ // With no memory replica, the evicted object disappears entirely.
+ ASSERT_TRUE(writer_->Put("third", third_slices, dfs_only));
+ auto first_query = writer_->Query("first");
+ ASSERT_FALSE(first_query);
+ EXPECT_EQ(first_query.error(), ErrorCode::OBJECT_NOT_FOUND);
+ expect_value("third", third);
+
+ adapter_->FailNextWrite();
+ std::string failed(4096, 'F');
+ auto failed_slices = Slices(failed);
+ auto failed_put = writer_->Put("failed", failed_slices, dfs_only);
+ ASSERT_FALSE(failed_put);
+ EXPECT_EQ(failed_put.error(), ErrorCode::FILE_WRITE_FAIL);
+ auto missing = writer_->Query("failed");
+ ASSERT_FALSE(missing);
+ EXPECT_EQ(missing.error(), ErrorCode::OBJECT_NOT_FOUND);
+
+ // Same-size upsert reallocates into a new bucket.
+ std::string replacement(4096, 'R');
+ auto replacement_slices = Slices(replacement);
+ ASSERT_TRUE(writer_->Upsert("third", replacement_slices, dfs_only));
+ expect_value("third", replacement);
+}
+
TEST_F(DfsImmutableBucketLruClientTest, BatchQueryRefreshesDfsBucketLru) {
std::string first(4096, '1');
std::string second(4096, '2');
diff --git a/mooncake-store/tests/dfs_posix_test.cpp b/mooncake-store/tests/dfs_posix_test.cpp
index 8a9b939b0a..1c32cb2db0 100644
--- a/mooncake-store/tests/dfs_posix_test.cpp
+++ b/mooncake-store/tests/dfs_posix_test.cpp
@@ -1425,6 +1425,42 @@ TEST_F(DfsBackendTest, LazyOpenRejectsMissingAndMalformedShardFiles) {
EXPECT_EQ(write[0].error(), ErrorCode::INVALID_PARAMS);
}
+// Once a shard is open, only the exact path validated at open time skips
+// normalization. Aliases and changed descriptors must still be validated
+// against the cached shard without reopening it.
+TEST_F(DfsBackendTest, CachedShardRevalidatesChangedDescriptors) {
+ ControlledPosixFsAdapter* controlled = nullptr;
+ auto backend = MakeControlledBackend(tmp_->path(), &controlled);
+ ASSERT_TRUE(backend->Init().has_value());
+
+ AlignedBuffer buf(4096);
+ ASSERT_NE(buf.data(), nullptr);
+ const auto read = [&](const std::string& path, int shard_idx) {
+ auto results = backend->BatchRead({{"cached",
+ {path, 0, 4096, 4096, shard_idx},
+ {{buf.data(), buf.size()}}}});
+ return results.at(0);
+ };
+ const std::filesystem::path root(tmp_->path());
+ const auto name = std::filesystem::path(ShardPath(0)).filename();
+
+ ASSERT_TRUE(read(ShardPath(0), 0));
+ ASSERT_TRUE(read(ShardPath(0), 0));
+ EXPECT_TRUE(read((root / "." / name).string(), 0));
+
+ for (const auto& [path, shard_idx] :
+ std::vector>{
+ {ShardPath(1), 0},
+ {ShardPath(0), 1},
+ {(root / ".." / name).string(), 0}}) {
+ SCOPED_TRACE(path + " shard_idx=" + std::to_string(shard_idx));
+ auto result = read(path, shard_idx);
+ ASSERT_FALSE(result);
+ EXPECT_EQ(result.error(), ErrorCode::INVALID_PARAMS);
+ }
+ EXPECT_EQ(controlled->OpenCallCount(), 1);
+}
+
TEST_F(DfsBackendTest, ConcurrentLazyOpenPreservesIndependentWrites) {
PosixFsAdapter adapter;
ASSERT_TRUE(adapter.Init(tmp_->path()));
diff --git a/mooncake-store/tests/dfs_sync_client_test.cpp b/mooncake-store/tests/dfs_sync_client_test.cpp
index 7505d1cca5..119b1b9b6d 100644
--- a/mooncake-store/tests/dfs_sync_client_test.cpp
+++ b/mooncake-store/tests/dfs_sync_client_test.cpp
@@ -133,6 +133,34 @@ class DfsSyncClientTest : public ::testing::Test {
return config;
}
+ static ReplicateConfig DfsOnlyConfig() {
+ ReplicateConfig config;
+ config.replica_num = 0;
+ config.dfs_replica_num = 1;
+ return config;
+ }
+
+ // Reads through Query + Client::Get and checks that DFS is the only
+ // replica.
+ void ExpectDfsOnlyValue(const std::string& key,
+ const std::string& expected) {
+ auto query = writer_->Query(key);
+ ASSERT_TRUE(query.has_value()) << key;
+ last_read_lease_deadline_ = query->lease_timeout;
+ ASSERT_EQ(query->replicas.size(), 1) << key;
+ ASSERT_TRUE(query->replicas[0].is_dfs_replica()) << key;
+ std::string output(expected.size(), '\0');
+ std::vector slices{{output.data(), output.size()}};
+ ASSERT_TRUE(writer_->Get(key, *query, slices).has_value()) << key;
+ EXPECT_EQ(output, expected) << key;
+ }
+
+ void ExpectNotFound(const std::string& key) {
+ auto query = writer_->Query(key);
+ ASSERT_FALSE(query.has_value()) << key;
+ EXPECT_EQ(query.error(), ErrorCode::OBJECT_NOT_FOUND) << key;
+ }
+
tl::expected QueryDfsOnly(const std::string& key) {
auto query = writer_->Query(key);
if (!query) {
@@ -570,4 +598,98 @@ TEST_F(DfsSyncClientTest, DfsMetricsAppearInSerializedOutput) {
EXPECT_NE(summary->find("=== DFS Metrics Summary ==="), std::string::npos);
}
+TEST_F(DfsSyncClientTest, DfsOnlyPutGetRemoveAndReput) {
+ const std::string key = "dfs_only_put";
+ std::string value(4096, 'a');
+ std::vector slices{{value.data(), value.size()}};
+ ASSERT_TRUE(writer_->Put(key, slices, DfsOnlyConfig()).has_value());
+ ExpectDfsOnlyValue(key, value);
+
+ WaitForReadLeaseExpiry();
+ ASSERT_TRUE(writer_->Remove(key).has_value());
+ ExpectNotFound(key);
+
+ std::string larger(8192, 'b');
+ std::vector larger_slices{{larger.data(), larger.size()}};
+ ASSERT_TRUE(writer_->Put(key, larger_slices, DfsOnlyConfig()).has_value());
+ ExpectDfsOnlyValue(key, larger);
+}
+
+TEST_F(DfsSyncClientTest, DfsOnlyBatchFailureOnlyRevokesFailedKey) {
+ std::vector keys{"dfs_only_ok", "dfs_only_fail"};
+ std::vector values{std::string(4096, 'c'),
+ std::string(4096, 'd')};
+ auto slices = MakeSlices(values);
+ adapter_->FailWriteCall(adapter_->WriteCalls() + 2);
+
+ auto results = writer_->BatchPut(keys, slices, DfsOnlyConfig());
+ ASSERT_EQ(results.size(), keys.size());
+ EXPECT_TRUE(results[0].has_value());
+ ASSERT_FALSE(results[1].has_value());
+ EXPECT_EQ(results[1].error(), ErrorCode::FILE_WRITE_FAIL);
+ ExpectDfsOnlyValue(keys[0], values[0]);
+ ExpectNotFound(keys[1]);
+
+ // The revoked key and its extent can be reused.
+ std::vector retry{{values[1].data(), values[1].size()}};
+ ASSERT_TRUE(writer_->Put(keys[1], retry, DfsOnlyConfig()).has_value());
+ ExpectDfsOnlyValue(keys[1], values[1]);
+}
+
+TEST_F(DfsSyncClientTest, DfsOnlyUpsertSameSizeResizeAndBatch) {
+ const std::string key = "dfs_only_upsert";
+ std::string initial(4096, 'e');
+ std::vector initial_slices{{initial.data(), initial.size()}};
+ ASSERT_TRUE(
+ writer_->Upsert(key, initial_slices, DfsOnlyConfig()).has_value());
+ ExpectDfsOnlyValue(key, initial);
+
+ WaitForReadLeaseExpiry();
+ std::string same_size(4096, 'f');
+ std::vector same_slices{{same_size.data(), same_size.size()}};
+ ASSERT_TRUE(writer_->Upsert(key, same_slices, DfsOnlyConfig()).has_value());
+ ExpectDfsOnlyValue(key, same_size);
+
+ WaitForReadLeaseExpiry();
+ std::string resized(12288, 'g');
+ std::vector resized_slices{{resized.data(), resized.size()}};
+ ASSERT_TRUE(
+ writer_->Upsert(key, resized_slices, DfsOnlyConfig()).has_value());
+ ExpectDfsOnlyValue(key, resized);
+
+ WaitForReadLeaseExpiry();
+ std::vector keys{key, "dfs_only_upsert_new"};
+ std::vector values{std::string(4096, 'h'),
+ std::string(4096, 'i')};
+ auto batch_slices = MakeSlices(values);
+ auto results = writer_->BatchUpsert(keys, batch_slices, DfsOnlyConfig());
+ ASSERT_EQ(results.size(), keys.size());
+ ASSERT_TRUE(results[0].has_value());
+ ASSERT_TRUE(results[1].has_value());
+ ExpectDfsOnlyValue(keys[0], values[0]);
+ ExpectDfsOnlyValue(keys[1], values[1]);
+}
+
+TEST_F(DfsSyncClientTest, DfsOnlyUpsertMatchesMemoryLostTopology) {
+ const std::string key = "dfs_only_memory_lost";
+ std::string initial(4096, 'j');
+ std::vector initial_slices{{initial.data(), initial.size()}};
+ ASSERT_TRUE(writer_->Put(key, initial_slices, DfsConfig()).has_value());
+ ASSERT_TRUE(provider_->UnmountSegment(segment_, segment_size_).has_value());
+ ExpectDfsOnlyValue(key, initial);
+
+ WaitForReadLeaseExpiry();
+ std::string updated(4096, 'k');
+ std::vector updated_slices{{updated.data(), updated.size()}};
+ auto mismatched = writer_->Upsert(key, updated_slices, DfsConfig());
+ ASSERT_FALSE(mismatched.has_value());
+ EXPECT_EQ(mismatched.error(), ErrorCode::INVALID_PARAMS);
+ ExpectDfsOnlyValue(key, initial);
+
+ WaitForReadLeaseExpiry();
+ ASSERT_TRUE(
+ writer_->Upsert(key, updated_slices, DfsOnlyConfig()).has_value());
+ ExpectDfsOnlyValue(key, updated);
+}
+
} // namespace mooncake::test
diff --git a/mooncake-store/tests/dummy_client_probe_key_test.cpp b/mooncake-store/tests/dummy_client_probe_key_test.cpp
index b8ba20c60d..96dfe99de2 100644
--- a/mooncake-store/tests/dummy_client_probe_key_test.cpp
+++ b/mooncake-store/tests/dummy_client_probe_key_test.cpp
@@ -150,6 +150,25 @@ TEST_F(DummyClientProbeKeyTest, BatchProbeKeyOverRpcReportsExistence) {
EXPECT_EQ(results[2], 1) << key_b << " should exist";
}
+TEST_F(DummyClientProbeKeyTest, LastHitOnlyPolicySurvivesBothRpcBoundaries) {
+ ASSERT_TRUE(SetupStack());
+ for (const auto &key : {"a", "b", "c"}) PutData(key, "state");
+ const GrantLeasePolicy policy{ProbeLeaseMode::LastHitOnly, 2};
+ EXPECT_EQ(dummy_client_->batchProbeKey({"a", "b", "c", "missing"}, policy),
+ (std::vector{1, 1, 1, 0}));
+ EXPECT_EQ(dummy_client_->batchProbeKey({"a", "missing", "b", "c"}, policy),
+ (std::vector{1, 0, 1, 1}));
+ EXPECT_EQ(real_client_->batchProbeKey({"a", "b", "b", "c"}, policy),
+ (std::vector{1, 1, 1, 1}));
+ EXPECT_EQ(dummy_client_->batchProbeKey({"a", "b", "c"}),
+ (std::vector{1, 1, 1}));
+ EXPECT_TRUE(dummy_client_->batchProbeKey({}, policy).empty());
+ const auto invalid = dummy_client_->batchProbeKey(
+ {"a", "b"}, {ProbeLeaseMode::LastHitOnly, 0});
+ ASSERT_EQ(invalid.size(), 2u);
+ for (int value : invalid) EXPECT_LT(value, 0);
+}
+
} // namespace testing
} // namespace mooncake
diff --git a/mooncake-store/tests/file_storage_test.cpp b/mooncake-store/tests/file_storage_test.cpp
index 3f74941b16..fb73f2dad1 100644
--- a/mooncake-store/tests/file_storage_test.cpp
+++ b/mooncake-store/tests/file_storage_test.cpp
@@ -540,7 +540,8 @@ class FileStorageTest : public ::testing::Test {
EXPECT_TRUE(unmount.has_value());
std::free(seg_ptr);
}
- void RunBatchPutHealWipeScenario(const std::string& data_path) {
+ void RunBatchPutHealWipeScenario(const std::string& data_path,
+ bool with_session_ids = false) {
std::filesystem::path master_root =
std::filesystem::path(data_path) / "heal_master_batch";
std::filesystem::create_directories(master_root);
@@ -624,7 +625,7 @@ class FileStorageTest : public ::testing::Test {
// One batch with both wiped keys and a fresh key.
const std::string fresh_key = "heal_batch_fresh";
const std::string updated(512, 'y');
- std::vector keys = {wiped_keys[0], wiped_keys[1], fresh_key};
+ std::vector keys = {fresh_key, wiped_keys[0], wiped_keys[1]};
std::vector> batched_slices;
std::vector buffers;
for (size_t i = 0; i < keys.size(); ++i) {
@@ -636,6 +637,10 @@ class FileStorageTest : public ::testing::Test {
}
ReplicateConfig cfg;
cfg.replica_num = 1;
+ if (with_session_ids) {
+ cfg.session_ids =
+ std::vector{"fresh", "wiped-a", "wiped-b"};
+ }
auto results = client.value()->BatchPut(keys, batched_slices, cfg);
ASSERT_EQ(results.size(), keys.size());
for (size_t i = 0; i < results.size(); ++i) {
@@ -1367,4 +1372,9 @@ TEST_F(FileStorageTest,
RunBatchPutHealWipeScenario(data_path);
}
+TEST_F(FileStorageTest,
+ TaggedBatchPutAfterPhysicalWipeHealsDanglingLocalDiskReplica) {
+ RunBatchPutHealWipeScenario(data_path, true);
+}
+
} // namespace mooncake
diff --git a/mooncake-store/tests/file_util_test.cpp b/mooncake-store/tests/file_util_test.cpp
index e53eaf5ca5..add44db19c 100644
--- a/mooncake-store/tests/file_util_test.cpp
+++ b/mooncake-store/tests/file_util_test.cpp
@@ -2,11 +2,18 @@
#include
#include
+#include
+#include
#include
#include
#include
+#include
+#include
#include
+#include
+#include
+
#include "common/file_util.h"
namespace mooncake::test {
@@ -46,6 +53,125 @@ class FileUtilTest : public ::testing::Test {
std::string tmp_dir_;
};
+TEST_F(FileUtilTest, SortByCreationTime_EmptyAndSingle) {
+ std::vector files;
+ FileUtil::SortByCreationTime(files);
+ EXPECT_TRUE(files.empty());
+
+ const auto path = fs::path(tmp_dir()) / "one";
+ std::ofstream(path) << "data";
+ files.emplace_back(path);
+ FileUtil::SortByCreationTime(files);
+ ASSERT_EQ(files.size(), 1);
+ EXPECT_EQ(files.front().path(), path);
+}
+
+TEST_F(FileUtilTest, SortByCreationTime_MissingFileKeepsKnownFilesOrdered) {
+ const auto early = fs::path(tmp_dir()) / "z_early";
+ const auto late = fs::path(tmp_dir()) / "a_late";
+ const auto missing = fs::path(tmp_dir()) / "removed_after_scan";
+ std::ofstream(early) << "early";
+ std::ofstream(missing) << "temporary";
+ const fs::directory_entry missing_entry(missing);
+ ASSERT_TRUE(fs::remove(missing));
+ std::this_thread::sleep_for(std::chrono::milliseconds(25));
+ std::ofstream(late) << "late";
+
+#if defined(__linux__)
+ struct statx early_stat{}, late_stat{};
+ ASSERT_EQ(statx(AT_FDCWD, early.c_str(), 0, STATX_BTIME, &early_stat), 0);
+ ASSERT_EQ(statx(AT_FDCWD, late.c_str(), 0, STATX_BTIME, &late_stat), 0);
+ if (!(early_stat.stx_mask & STATX_BTIME) ||
+ !(late_stat.stx_mask & STATX_BTIME) ||
+ std::tie(early_stat.stx_btime.tv_sec, early_stat.stx_btime.tv_nsec) >=
+ std::tie(late_stat.stx_btime.tv_sec, late_stat.stx_btime.tv_nsec)) {
+ GTEST_SKIP() << "Filesystem did not provide distinct birth times";
+ }
+#elif defined(__APPLE__)
+ struct stat early_stat{}, late_stat{};
+ ASSERT_EQ(stat(early.c_str(), &early_stat), 0);
+ ASSERT_EQ(stat(late.c_str(), &late_stat), 0);
+ if (std::tie(early_stat.st_birthtimespec.tv_sec,
+ early_stat.st_birthtimespec.tv_nsec) >=
+ std::tie(late_stat.st_birthtimespec.tv_sec,
+ late_stat.st_birthtimespec.tv_nsec)) {
+ GTEST_SKIP() << "Filesystem did not provide distinct birth times";
+ }
+#else
+ GTEST_SKIP() << "Birth-time queries are not supported on this platform";
+#endif
+
+ const std::vector entries{
+ fs::directory_entry(early), fs::directory_entry(late), missing_entry};
+ std::vector order{0, 1, 2};
+ do {
+ std::vector files;
+ for (int index : order) files.push_back(entries[index]);
+ FileUtil::SortByCreationTime(files);
+ ASSERT_EQ(files.size(), 3);
+ EXPECT_EQ(files[0].path(), early);
+ EXPECT_EQ(files[1].path(), late);
+ EXPECT_EQ(files[2].path(), missing);
+ } while (std::next_permutation(order.begin(), order.end()));
+ EXPECT_TRUE(fs::exists(early));
+ EXPECT_TRUE(fs::exists(late));
+}
+
+TEST_F(FileUtilTest, SortByCreationTime_RetainsAndOrdersFailedQueries) {
+ const auto a = fs::path(tmp_dir()) / "a";
+ const auto z = fs::path(tmp_dir()) / "z";
+ std::ofstream(a) << "a";
+ std::ofstream(z) << "z";
+ std::vector files{fs::directory_entry(z),
+ fs::directory_entry(a)};
+ ASSERT_TRUE(fs::remove(a));
+ ASSERT_TRUE(fs::remove(z));
+ FileUtil::SortByCreationTime(files);
+ ASSERT_EQ(files.size(), 2);
+ EXPECT_EQ(files[0].path(), a);
+ EXPECT_EQ(files[1].path(), z);
+}
+
+TEST_F(FileUtilTest, SortByCreationTime_EqualBirthTimesUsePath) {
+ const auto a = fs::path(tmp_dir()) / "a";
+ const auto z = fs::path(tmp_dir()) / "z";
+ std::ofstream(z) << "data";
+ fs::create_hard_link(z, a);
+ std::vector files{fs::directory_entry(z),
+ fs::directory_entry(a)};
+ FileUtil::SortByCreationTime(files);
+ ASSERT_EQ(files.size(), 2);
+ EXPECT_EQ(files[0].path(), a);
+ EXPECT_EQ(files[1].path(), z);
+ EXPECT_EQ(ReadFileContent(a.string()), "data");
+ EXPECT_EQ(ReadFileContent(z.string()), "data");
+}
+
+#if defined(__linux__)
+TEST_F(FileUtilTest, SortByCreationTime_RetainsReadableFileWithoutBirthTime) {
+ const fs::path without_birth_time = "/proc/self/status";
+ struct statx info{};
+ ASSERT_EQ(
+ statx(AT_FDCWD, without_birth_time.c_str(), 0, STATX_BTIME, &info), 0);
+ if (info.stx_mask & STATX_BTIME) {
+ GTEST_SKIP() << "procfs provides birth times on this system";
+ }
+ const auto known = fs::path(tmp_dir()) / "known";
+ std::ofstream(known) << "data";
+ ASSERT_EQ(statx(AT_FDCWD, known.c_str(), 0, STATX_BTIME, &info), 0);
+ if (!(info.stx_mask & STATX_BTIME)) {
+ GTEST_SKIP() << "Temporary filesystem does not provide birth times";
+ }
+ std::vector files{
+ fs::directory_entry(without_birth_time), fs::directory_entry(known)};
+ FileUtil::SortByCreationTime(files);
+ ASSERT_EQ(files.size(), 2);
+ EXPECT_EQ(files[0].path(), known);
+ EXPECT_EQ(files[1].path(), without_birth_time);
+ EXPECT_FALSE(ReadFileContent(without_birth_time.string()).empty());
+}
+#endif
+
// ========== SaveStringToFile ==========
TEST_F(FileUtilTest, SaveStringToFile_Basic) {
diff --git a/mooncake-store/tests/ha/leadership/etcd_leader_lease_ttl_e2e.sh b/mooncake-store/tests/ha/leadership/etcd_leader_lease_ttl_e2e.sh
new file mode 100755
index 0000000000..2ad5d920bb
--- /dev/null
+++ b/mooncake-store/tests/ha/leadership/etcd_leader_lease_ttl_e2e.sh
@@ -0,0 +1,193 @@
+#!/usr/bin/env bash
+# Checks that --ha_leader_lease_ttl_sec and the JSON key ha_leader_lease_ttl_sec
+# reach the lease a running HA master is granted, against a local one-member
+# etcd (same approach as etcd_leader_hang_e2e.sh):
+# - no flag: the default TTL (5 s) is granted;
+# - 15 and the range boundaries 2 and 60 are granted exactly;
+# - the JSON config key overrides the default;
+# - out-of-range values (1 and 61, flag or JSON) make the master exit
+# nonzero with the validation error, before it connects to etcd.
+# A master that exits during a success case fails the run.
+#
+# Requires mooncake_master from a normal build (no failpoints needed).
+set -euo pipefail
+# Case 8 refuses a bad JSON value through LOG(FATAL); do not leave a core file.
+ulimit -c 0
+
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." && pwd)"
+MASTER_BIN="${MASTER_BIN:-${ROOT_DIR}/build/mooncake-store/src/mooncake_master}"
+ETCD_BIN="${ETCD_BIN:-etcd}"
+ETCDCTL_BIN="${ETCDCTL_BIN:-etcdctl}"
+# All ports stay inside the local test range 23000-23999; every case uses a
+# base of 231xx: etcd client base+1, peer base+2, master rpc base+20.
+BASE=23100
+
+work="$(mktemp -d)"
+etcd_pid=""
+master_pid=""
+current_log=""
+
+cleanup() {
+ rc=$?
+ set +e
+ kill "${master_pid:-}" 2>/dev/null
+ wait "${master_pid:-}" 2>/dev/null
+ kill "${etcd_pid:-}" 2>/dev/null
+ wait "${etcd_pid:-}" 2>/dev/null
+ if [[ "${KEEP_WORK:-0}" != 1 ]]; then rm -rf "$work"; fi
+ exit "$rc"
+}
+trap cleanup EXIT
+
+fail() {
+ echo "FAIL: $*" >&2
+ [[ -n "$current_log" ]] && tail -n 30 "$current_log" 2>/dev/null >&2
+ exit 1
+}
+
+start_etcd() {
+ local port=$1 peer=$2 data=$3
+ "$ETCD_BIN" --name ttl-check-etcd --data-dir "$data" \
+ --listen-client-urls "http://127.0.0.1:${port}" \
+ --advertise-client-urls "http://127.0.0.1:${port}" \
+ --listen-peer-urls "http://127.0.0.1:${peer}" \
+ --initial-advertise-peer-urls "http://127.0.0.1:${peer}" \
+ --initial-cluster ttl-check-etcd=http://127.0.0.1:${peer} \
+ >"$work/etcd.log" 2>&1 &
+ etcd_pid=$!
+ for _ in $(seq 1 30); do
+ "$ETCDCTL_BIN" --endpoints="127.0.0.1:${port}" --command-timeout=1s \
+ endpoint health >/dev/null 2>&1 && return 0
+ sleep 1
+ done
+ fail "etcd on ${port} did not become healthy"
+}
+
+start_master() {
+ local rpc=$1 log=$2
+ shift 2
+ current_log="$log"
+ "$MASTER_BIN" "$@" \
+ --rpc_address=127.0.0.1 --rpc_port="$rpc" \
+ --logtostderr=1 >"$log" 2>&1 &
+ master_pid=$!
+}
+
+view_key() { echo "mooncake-store/${CLUSTER_ID}/master_view"; }
+
+# Reads the master_view lease id and granted TTL from etcd. Only valid
+# while the master is running.
+read_granted_ttl() {
+ local lease granted
+ lease=$("$ETCDCTL_BIN" --endpoints="127.0.0.1:${ETCD_PORT}" get \
+ "$(view_key)" -w json 2>/dev/null | python3 -c \
+ 'import json,sys; kvs=json.load(sys.stdin).get("kvs",[]); print(kvs[0].get("lease",0) if kvs else 0)')
+ [[ "$lease" != 0 ]] || fail "master_view has no lease attached"
+ granted=$("$ETCDCTL_BIN" --endpoints="127.0.0.1:${ETCD_PORT}" lease \
+ timetolive "$(printf '%x' "$lease")" -w json 2>/dev/null | python3 -c \
+ 'import json,sys; print(json.load(sys.stdin)["granted-ttl"])')
+ echo "$granted"
+}
+
+stop_master() {
+ kill "${master_pid:-}" 2>/dev/null || true
+ wait "${master_pid:-}" 2>/dev/null || true
+ master_pid=""
+}
+
+stop_etcd() {
+ kill "${etcd_pid:-}" 2>/dev/null || true
+ wait "${etcd_pid:-}" 2>/dev/null || true
+ etcd_pid=""
+ rm -rf "$work/etcd-data"
+}
+
+case_no=0
+next_case() {
+ case_no=$((case_no + 1))
+ ETCD_PORT=$((BASE + case_no * 10 + 1))
+ stop_etcd
+ start_etcd "$ETCD_PORT" $((ETCD_PORT + 1)) "$work/etcd-data"
+ CLUSTER_ID="ttl-e2e-case-${case_no}-$$"
+ mkdir -p "$work/case$case_no"
+}
+
+# Expected-success case: the master publishes its view under a lease whose
+# granted TTL must be exactly $1. Any master exit during the wait fails.
+run_granted_case() {
+ local expect_ttl=$1
+ shift
+ next_case
+ local rpc=$((BASE + case_no * 10 + 20)) log="$work/case$case_no/master.log"
+ start_master "$rpc" "$log" \
+ --enable_ha=true --ha_backend_type=etcd \
+ --etcd_endpoints="127.0.0.1:${ETCD_PORT}" --cluster_id="${CLUSTER_ID}" "$@"
+ local granted=""
+ for _ in $(seq 1 90); do
+ kill -0 "$master_pid" 2>/dev/null ||
+ fail "case $case_no: master exited before publishing its view"
+ if "$ETCDCTL_BIN" --endpoints="127.0.0.1:${ETCD_PORT}" get "$(view_key)" \
+ --print-value-only 2>/dev/null | grep -q "127.0.0.1:${rpc}"; then
+ granted=$(read_granted_ttl)
+ break
+ fi
+ sleep 1
+ done
+ [[ -n "$granted" ]] || fail "case $case_no: no view published in 90 s"
+ [[ "$granted" == "$expect_ttl" ]] ||
+ fail "case $case_no: granted TTL $granted, expected $expect_ttl"
+ grep -q "ha_leader_lease_ttl_sec=$expect_ttl" "$log" ||
+ fail "case $case_no does not log the effective TTL"
+ echo "case $case_no: granted TTL $granted - PASS"
+ stop_master
+}
+
+# Expected-rejection case: the out-of-range value is refused by config
+# validation before the supervisor connects to etcd (gflags validator at
+# ParseCommandLineFlags time for the flag; the range check in
+# LoadConfigFromCmdline for the JSON key). The contract asserted is a
+# nonzero exit plus the validation error in the log.
+run_refused_case() {
+ next_case
+ local rpc=$((BASE + case_no * 10 + 20)) log="$work/case$case_no/master.log"
+ start_master "$rpc" "$log" \
+ --enable_ha=true --ha_backend_type=etcd \
+ --etcd_endpoints="127.0.0.1:${ETCD_PORT}" --cluster_id="${CLUSTER_ID}" "$@"
+ local rc=""
+ # Bounded wait for the refusal to complete: a validation exit is
+ # immediate, so the bound only fails a wedged binary.
+ for _ in $(seq 1 30); do
+ kill -0 "$master_pid" 2>/dev/null || break
+ sleep 1
+ done
+ kill -0 "$master_pid" 2>/dev/null &&
+ fail "case $case_no: out-of-range TTL was accepted (master still running)"
+ wait "$master_pid" 2>/dev/null || rc=$?
+ master_pid=""
+ [[ -n "$rc" && "$rc" != 0 ]] ||
+ fail "case $case_no: out-of-range TTL exited ${rc:-0} instead of refusing"
+ grep -q "ha_leader_lease_ttl_sec must be in \[2, 60\]" "$log" ||
+ fail "case $case_no: refused master did not log the validation error"
+ echo "case $case_no: refused (exit $rc) with the validation error logged - PASS"
+}
+
+# 1. Default TTL: no flag -> granted 5.
+run_granted_case 5
+
+# 2. Explicit valid values incl. both range boundaries.
+for ttl in 15 2 60; do
+ run_granted_case "$ttl" --ha_leader_lease_ttl_sec="$ttl"
+done
+
+# 3. JSON config value (config_path) reaches the granted lease.
+printf '{"ha_leader_lease_ttl_sec": 17}\n' >"$work/config.json"
+run_granted_case 17 --config_path="$work/config.json"
+
+# 4. Out-of-range values (flag and JSON): refused at config validation.
+for bad in 1 61; do
+ run_refused_case --ha_leader_lease_ttl_sec="$bad"
+done
+printf '{"ha_leader_lease_ttl_sec": 61}\n' >"$work/config-bad.json"
+run_refused_case --config_path="$work/config-bad.json"
+
+echo "lease TTL propagation check passed in ${SECONDS}s"
diff --git a/mooncake-store/tests/ha/snapshot/batch_oplog/codec_test.cpp b/mooncake-store/tests/ha/snapshot/batch_oplog/codec_test.cpp
index 43d3899294..2698883ed2 100644
--- a/mooncake-store/tests/ha/snapshot/batch_oplog/codec_test.cpp
+++ b/mooncake-store/tests/ha/snapshot/batch_oplog/codec_test.cpp
@@ -64,6 +64,7 @@ TEST(BatchOpLogSnapshotCodecTest, ObjectChunkRoundTripsAndChecksEnvelope) {
metadata.size = 8192;
metadata.group_id = "group";
metadata.hard_pinned = true;
+ metadata.session_id = "session-snapshot";
std::vector objects{
{.tenant_id = "tenant", .key = "key", .metadata = metadata}};
auto encoded = EncodeBatchOpLogSnapshotObjectChunk(7, std::move(objects));
@@ -79,6 +80,8 @@ TEST(BatchOpLogSnapshotCodecTest, ObjectChunkRoundTripsAndChecksEnvelope) {
EXPECT_EQ(8192u, decoded->objects[0].metadata.size);
EXPECT_EQ("group", decoded->objects[0].metadata.group_id);
EXPECT_TRUE(decoded->objects[0].metadata.hard_pinned.value_or(false));
+ EXPECT_EQ(decoded->objects[0].metadata.session_id.value_or(""),
+ "session-snapshot");
EXPECT_FALSE(DecodeBatchOpLogSnapshotObjectChunk(encoded, 8, 1));
EXPECT_FALSE(DecodeBatchOpLogSnapshotObjectChunk(encoded, 7, 300000000));
@@ -94,6 +97,7 @@ TEST(BatchOpLogSnapshotCodecTest, OldObjectChunkDefaultsHardPinnedToFalse) {
ASSERT_TRUE(decoded) << decoded.error();
EXPECT_FALSE(decoded->objects[0].metadata.hard_pinned.value_or(false));
+ EXPECT_FALSE(decoded->objects[0].metadata.session_id.has_value());
}
TEST(BatchOpLogSnapshotCodecTest, RejectsTruncatedArtifacts) {
diff --git a/mooncake-store/tests/kv_event_publisher_test.cpp b/mooncake-store/tests/kv_event_publisher_test.cpp
index e3dfe7f51f..55699105f3 100644
--- a/mooncake-store/tests/kv_event_publisher_test.cpp
+++ b/mooncake-store/tests/kv_event_publisher_test.cpp
@@ -23,6 +23,10 @@ TEST(KvEventPublisherTest, DisabledPublisherIsNoop) {
KvEventPublisher publisher(config);
EXPECT_FALSE(publisher.enabled());
publisher.PublishStored("42", "cpu");
+ publisher.PublishStored("42", "cpu", TenantId::Default(), "", "session");
+ publisher.PublishCommitted("42", {"cpu"}, "default", "", "session");
+ publisher.SyncObjectState("42", {"disk"}, "default", "", {"cpu"},
+ "session");
publisher.PublishRemoved("42", "cpu");
publisher.PublishCleared("tenant-a");
const auto stats = publisher.GetStats();
@@ -182,6 +186,50 @@ struct ScopedSubscriber {
} // namespace
+TEST(KvEventPublisherTest, SessionTagIsStoredAttributionOnly) {
+ for (bool legacy : {false, true}) {
+ auto endpoint = MakeIpcEndpoint();
+ auto config = MakeEnabledConfig(endpoint);
+ config.emit_legacy_compat_fields = legacy;
+ KvEventPublisher publisher(config);
+ ScopedSubscriber subscriber;
+ ASSERT_TRUE(subscriber.Connect(endpoint));
+ std::string session = "session-a";
+ publisher.PublishStored("a", "cpu", "default", "group", session);
+ session = "changed-after-enqueue";
+ publisher.PublishCommitted("b", {"cpu", "disk"}, "default", "",
+ "session-b");
+ publisher.SyncObjectState("c", {"disk"}, "default", "", {"cpu"},
+ "session-c");
+ publisher.PublishStored("untagged", "cpu");
+ publisher.PublishCleared();
+ size_t received = 0;
+ while (received < 7) {
+ msgpack::object_handle payload;
+ ASSERT_TRUE(ReceivePayload(subscriber.sub, &payload));
+ for (uint32_t i = 0; i < EventCount(payload); ++i) {
+ const auto& event = EventAt(payload, i);
+ const auto* tag = FindMapValue(event, "session_id");
+ const auto kind =
+ MsgpackString(*FindMapValue(event, "event_type"));
+ if (kind == "stored") {
+ ASSERT_NE(tag, nullptr);
+ const auto key =
+ MsgpackString(*FindMapValue(event, "object_key"));
+ if (key == "untagged")
+ EXPECT_EQ(tag->type, msgpack::type::NIL);
+ else
+ EXPECT_EQ(MsgpackString(*tag), "session-" + key);
+ } else {
+ EXPECT_EQ(tag, nullptr);
+ }
+ ++received;
+ }
+ }
+ EXPECT_EQ(received, 7u);
+ }
+}
+
TEST(KvEventPublisherTest, PublishesSglangObjectKeyOverZmq) {
const std::string endpoint = MakeIpcEndpoint();
const std::string object_key =
diff --git a/mooncake-store/tests/master_config_test.cpp b/mooncake-store/tests/master_config_test.cpp
index 2e676ad763..52c7e134f5 100644
--- a/mooncake-store/tests/master_config_test.cpp
+++ b/mooncake-store/tests/master_config_test.cpp
@@ -61,5 +61,24 @@ TEST(ClientLivenessConfigTest, LegacyBuilderSetterDefaultsBothWindows) {
EXPECT_EQ(config.client_suspicion_ttl_sec, 45);
}
+TEST(HALeaseTtlConfigTest, SupervisorConfigDefaultsToMasterViewTtl) {
+ const MasterServiceSupervisorConfig config{};
+
+ EXPECT_EQ(DEFAULT_MASTER_VIEW_LEASE_TTL_SEC,
+ config.ha_leader_lease_ttl_sec);
+}
+
+TEST(HALeaseTtlConfigTest, SupervisorConfigCarriesMasterConfigTtl) {
+ MasterConfig master_config{};
+ master_config.ha_backend_type = "etcd";
+ EXPECT_EQ(DEFAULT_MASTER_VIEW_LEASE_TTL_SEC,
+ master_config.ha_leader_lease_ttl_sec);
+ master_config.ha_leader_lease_ttl_sec = 17;
+
+ const MasterServiceSupervisorConfig supervisor_config(master_config);
+
+ EXPECT_EQ(17, supervisor_config.ha_leader_lease_ttl_sec);
+}
+
} // namespace
} // namespace mooncake::test
diff --git a/mooncake-store/tests/master_service/dsl/scenario.cpp b/mooncake-store/tests/master_service/dsl/scenario.cpp
index 850fa72de4..16cd982ff9 100644
--- a/mooncake-store/tests/master_service/dsl/scenario.cpp
+++ b/mooncake-store/tests/master_service/dsl/scenario.cpp
@@ -1922,8 +1922,8 @@ MasterScenario& MasterScenario::Then(BatchProbeSpec batch_probe) {
return *this;
}
- const auto result =
- service_->BatchProbeKey(batch_probe.keys, TenantId(batch_probe.tenant));
+ const auto result = service_->BatchProbeKey({}, batch_probe.keys,
+ TenantId(batch_probe.tenant));
if (result.size() != batch_probe.expected.size()) {
Fail("BatchProbe returned " + std::to_string(result.size()) +
" results; expected " +
diff --git a/mooncake-store/tests/master_service_probe_policy_test.cpp b/mooncake-store/tests/master_service_probe_policy_test.cpp
new file mode 100644
index 0000000000..bcb88567b5
--- /dev/null
+++ b/mooncake-store/tests/master_service_probe_policy_test.cpp
@@ -0,0 +1,284 @@
+#include "master_service_test_fixture.h"
+#include "rpc_service.h"
+
+#include
+#include
+#include
+
+namespace mooncake::test {
+
+class BatchProbePolicyTest : public MasterServiceTest {
+ protected:
+ static constexpr GrantLeasePolicy kLastPair{ProbeLeaseMode::LastHitOnly, 2};
+
+ std::chrono::system_clock::time_point Deadline(MasterService& service,
+ const std::string& key) {
+ MasterServiceTestPeer::MetadataAccessorRO accessor(
+ &service,
+ MasterServiceTestPeer(service).MakeObjectIdentityForRequest(
+ key, TenantId::Default()));
+ EXPECT_TRUE(accessor.Exists());
+ return accessor.Exists() ? accessor.Get().EvictionDeadline()
+ : std::chrono::system_clock::time_point{};
+ }
+
+ void ExpectResults(const std::vector>& result,
+ const std::vector& expected) {
+ ASSERT_EQ(result.size(), expected.size());
+ for (size_t i = 0; i < result.size(); ++i) {
+ ASSERT_TRUE(result[i].has_value()) << i;
+ EXPECT_EQ(result[i].value(), expected[i]) << i;
+ }
+ }
+};
+
+TEST_F(BatchProbePolicyTest, NonePreservesReadAndSoftPinDeadlines) {
+ MasterService service(
+ MasterServiceConfig::builder().set_default_kv_lease_ttl(60000).build());
+ const auto context = PrepareSimpleSegment(service);
+ ReplicateConfig config;
+ config.replica_num = 1;
+ config.soft_pin_action = SoftPinAction::ENABLE;
+ PutCompletedObject(service, context.client_id, "key", config);
+ const auto before = Deadline(service, "key");
+ const auto soft_pin = GetSoftPinDeadline(service, "key");
+ ASSERT_TRUE(soft_pin.has_value());
+
+ // candidate_size is deliberately ignored in None mode.
+ ExpectResults(
+ service.BatchProbeKey({ProbeLeaseMode::None, 0},
+ {"key", "missing", "key"}, TenantId::Default()),
+ {true, false, true});
+ EXPECT_EQ(Deadline(service, "key"), before);
+ EXPECT_EQ(GetSoftPinDeadline(service, "key"), soft_pin);
+
+ ExpectResults(service.BatchExistKey({"key"}, TenantId::Default()), {true});
+ EXPECT_GT(Deadline(service, "key"), before);
+ const auto leased = Deadline(service, "key");
+ ExpectResults(service.BatchProbeKey({}, {"key"}, TenantId::Default()),
+ {true});
+ EXPECT_EQ(Deadline(service, "key"), leased);
+}
+
+TEST_F(BatchProbePolicyTest, SelectsLastCompleteSparseCandidate) {
+ MasterService service(
+ MasterServiceConfig::builder().set_default_kv_lease_ttl(60000).build());
+ const auto context = PrepareSimpleSegment(service);
+ ReplicateConfig config;
+ config.replica_num = 1;
+ config.soft_pin_action = SoftPinAction::ENABLE;
+ const std::vector keys = {"p0-state", "p0-conv", "p1-state",
+ "p1-conv", "p2-state", "p2-conv",
+ "p3-state", "p3-conv"};
+ for (size_t i : {0u, 1u, 4u, 5u, 6u}) {
+ PutCompletedObject(service, context.client_id, keys[i], config);
+ }
+ const auto old = Deadline(service, keys[0]);
+ const auto partial = Deadline(service, keys[6]);
+ const auto chosen = Deadline(service, keys[4]);
+ const auto soft_pin = GetSoftPinDeadline(service, keys[4]);
+ ExpectResults(service.BatchProbeKey(kLastPair, keys, TenantId::Default()),
+ {true, true, false, false, true, true, true, false});
+ EXPECT_EQ(Deadline(service, keys[0]), old);
+ EXPECT_EQ(Deadline(service, keys[6]), partial);
+ EXPECT_GT(Deadline(service, keys[4]), chosen);
+ EXPECT_EQ(GetSoftPinDeadline(service, keys[4]), soft_pin);
+ for (size_t i : {4u, 5u}) {
+ auto removed = service.Remove(keys[i], TenantId::Default());
+ ASSERT_FALSE(removed.has_value());
+ EXPECT_EQ(removed.error(), ErrorCode::OBJECT_HAS_LEASE);
+ }
+}
+
+TEST_F(BatchProbePolicyTest, OnlySelectedCandidateSurvivesNormalEviction) {
+ MasterService service(
+ MasterServiceConfig::builder().set_default_kv_lease_ttl(60000).build());
+ const auto context = PrepareSimpleSegment(service);
+ ReplicateConfig config;
+ config.replica_num = 1;
+ const std::vector keys = {"z", "y", "b", "a"};
+ for (const auto& key : keys) {
+ PutCompletedObject(service, context.client_id, key, config);
+ }
+ ExpectResults(service.BatchProbeKey(kLastPair, keys, TenantId::Default()),
+ {true, true, true, true});
+ MasterServiceTestPeer(service).RunBatchEvictForTesting(1.0, 1.0);
+ ExpectResults(service.BatchProbeKey({}, keys, TenantId::Default()),
+ {false, false, true, true});
+}
+
+TEST_F(BatchProbePolicyTest, IncompleteAndUnreadableCandidatesAreNotLeased) {
+ MasterService service;
+ const auto context = PrepareSimpleSegment(service);
+ ReplicateConfig config;
+ config.replica_num = 1;
+ PutCompletedObject(service, context.client_id, "complete", config);
+ ASSERT_TRUE(service
+ .PutStart(context.client_id, "writing", TenantId::Default(),
+ 1024, config)
+ .has_value());
+ const auto before = Deadline(service, "complete");
+ const std::vector