[Store] Add replication guarantees (#744)

* add replication guarantees

Signed-off-by: Vladislav Nosivskoy <vladnosiv@gmail.com>

* refactor implementation

Signed-off-by: Vladislav Nosivskoy <vladnosiv@gmail.com>

* fix merge aftermath

Signed-off-by: Vladislav Nosivskoy <vladnosiv@gmail.com>

* less copies

Signed-off-by: Vladislav Nosivskoy <vladnosiv@gmail.com>

* try fix wheel tests

Signed-off-by: Vladislav Nosivskoy <vladnosiv@gmail.com>

* fix wheel tests

Signed-off-by: Vladislav Nosivskoy <vladnosiv@gmail.com>

* remove old comment

Signed-off-by: Vladislav Nosivskoy <vladnosiv@gmail.com>

* fix wheel tests and add replication fault tolerance wheel test

Signed-off-by: Vladislav Nosivskoy <vladnosiv@gmail.com>

* split wheel tests

Signed-off-by: Vladislav Nosivskoy <vladnosiv@gmail.com>

* simplify code

Signed-off-by: Vladislav Nosivskoy <vladnosiv@gmail.com>

* replica allocation is best effort operation

Signed-off-by: Vladislav Nosivskoy <vladnosiv@gmail.com>

* fix tests for new best-effort behaviour

Signed-off-by: Vladislav Nosivskoy <vladnosiv@gmail.com>

* fix

Signed-off-by: Vladislav Nosivskoy <vladnosiv@gmail.com>

* update docs

Signed-off-by: Vladislav Nosivskoy <vladnosiv@gmail.com>

* update another docs

Signed-off-by: Vladislav Nosivskoy <vladnosiv@gmail.com>

---------

Signed-off-by: Vladislav Nosivskoy <vladnosiv@gmail.com>
This commit is contained in:
Vladislav Nosivskoy 2025-08-25 15:24:41 +03:00 committed by GitHub
parent f76c92295b
commit aa9d4471a1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 906 additions and 415 deletions

View File

@ -4,7 +4,7 @@ Mooncake aims to enhance the inference efficiency of large language models (LLMs
Mooncake:
- provides object-level data storage services
- supports data replication in the cache layer, with a lightweight design due to not guaranteeing high availability
- supports data replication in the cache layer with slice-level placement guarantees and best-effort allocation, with a lightweight design due to not guaranteeing high availability
- ensures the atomicity of object write operations, meaning a `Get` operation will always read one consistent version, but not necessarily the latest one
- supports striping and parallel I/O transfer for larger objects to utilize the aggregated bandwidth of multiple network cards
- supports multiple modes for flushing slow object storage

View File

@ -10,7 +10,7 @@ Mooncake Store provides low-level object storage and management capabilities, in
Key features of Mooncake Store include:
- **Object-level storage operations**: Mooncake Store provides simple and easy-to-use object-level APIs, including `Put`, `Get`, and `Remove` operations.
- **Multi-replica support**: Mooncake Store supports storing multiple data replicas for the same object, effectively alleviating hotspots in access pressure.
- **Multi-replica support**: Mooncake Store supports storing multiple data replicas for the same object, effectively alleviating hotspots in access pressure. Each slice within an object is guaranteed to be placed in different segments, while different objects' slices may share segments. Replication operates on a best-effort basis.
- **Strong consistency**: Mooncake Store Guarantees that `Get` operations always read accurate and complete data, and after a successful write, all subsequent Gets will return the most recent value.
- **Zero-copy, bandwidth-saturating transfers**: Powered by the Transfer Engine, Mooncake Store eliminates redundant memory copies and exploits multi-NIC GPUDirect RDMA pooling to drive data across the network at full line rate while keeping CPU overhead negligible.
- **High bandwidth utilization**: Mooncake Store supports striping and parallel I/O transfer of large objects, fully utilizing multi-NIC aggregated bandwidth for high-speed data reads and writes.
@ -89,7 +89,14 @@ tl::expected<void, ErrorCode> Put(const ObjectKey& key,
![mooncake-store-simple-put](../../image/mooncake-store-simple-put.png)
Used to store the value corresponding to `key`. The required number of replicas can be set via the `config` parameter.(When persistence is enabled, Put not only writes to the memory pool but also asynchronously initiates a data persistence operation to the SSD.) The data structure details of `ReplicateConfig` are as follows:
Used to store the value corresponding to `key`. The required number of replicas can be set via the `config` parameter.(When persistence is enabled, Put not only writes to the memory pool but also asynchronously initiates a data persistence operation to the SSD.)
**Replication Guarantees and Best Effort Behavior:**
- Each slice of an object is guaranteed to be replicated to different segments, ensuring distribution across separate storage nodes
- Different slices from different objects may be placed in the same segment
- Replication operates on a best-effort basis: if insufficient space is available for all requested replicas, the object will still be written with as many replicas as possible
The data structure details of `ReplicateConfig` are as follows:
```C++
struct ReplicateConfig {
@ -246,7 +253,7 @@ message PutStartResponse {
- **Request**: `PutStartRequest` containing the key, data length, and replica configuration config.
- **Response**: `PutStartResponse` containing the status code status_code and the allocated replica information replica_list.
- **Description**: Before writing an object, the Client must call PutStart to request storage space from the Master Service. The Master Service allocates space based on the config and returns the allocation results (`replica_list`) to the Client. The Client then writes data to the storage nodes where the allocated replicas are located. The need for both start and end steps ensures that other Clients do not read partially written values, preventing dirty reads.
- **Description**: Before writing an object, the Client must call PutStart to request storage space from the Master Service. The Master Service allocates space based on the config and returns the allocation results (`replica_list`) to the Client. The allocation strategy ensures that each slice of the object is placed in different segments, while operating on a best-effort basis - if insufficient space is available for all requested replicas, as many replicas as possible will be allocated. The Client then writes data to the storage nodes where the allocated replicas are located. The need for both start and end steps ensures that other Clients do not read partially written values, preventing dirty reads.
4. PutEnd

View File

@ -3,7 +3,7 @@
Mooncake 旨在通过在高速互联的 DRAM/SSD 资源上构建一个多级缓存池来提升大型语言模型LLM等场景下的推理效率尤其是在慢速对象存储环境中。与传统缓存相比Mooncake 的独特之处在于,它能够利用 (GPUDirect) RDMA 技术,以零拷贝的方式将数据从发起端的 DRAM/VRAM 直接传输到接收端的 DRAM/SSD同时最大限度地利用单机多网卡资源。
- 提供对象级别的数据存储服务
- 支持在缓存层多副本保存数据,由于不保证绝对的高可用,因此系统设计更为轻量化
- 支持在缓存层多副本保存数据,提供slice级别的分布保证和尽力而为的分配策略由于不保证绝对的高可用,因此系统设计更为轻量化
- 保证对象写操作的原子性,即 Get 一定会读到某次 Put 生成的完整的数据,但不一定是最新的
- 支持对较大的对象进行条带化和并行 I/O 传输,以利用多网卡的聚合带宽
- 支持 Eager/Lazy/None 三种下刷慢速对象存储的模式,分别对应持久化要求从高到低的对象

View File

@ -11,7 +11,7 @@ Mooncake Store 提供了底层的对象存储和管理能力,包括可配置
Mooncake Store 的主要特性包括:
* **对象级存储操作**:提供简单易用的对象级 API包括 Put、Get 和 Remove 等操作,方便用户进行数据管理。
* **多副本支持**:支持为同一对象保存多个数据副本,有效缓解热点访问压力。
* **多副本支持**:支持为同一对象保存多个数据副本,有效缓解热点访问压力。保证同一对象的每个slice被放置在不同的segment中不同对象的slice可以共享segment。采用尽力而为的副本分配策略。
* **强一致性**:保证 `Get` 操作读取到完整且正确的数据,并且数据写入成功后,后续的 `Get` 操作一定能读取到最新写入的值。
* **零拷贝、高带宽利用**:由 Transfer Engine 提供支持, 数据链路零拷贝,对大型对象进行条带化和并行 I/O 传输,充分利用多网卡聚合带宽,实现高速数据读写。
* **动态资源伸缩**:支持动态添加和删除节点,灵活应对系统负载变化,实现资源的弹性管理。
@ -95,6 +95,12 @@ tl::expected<void, ErrorCode> Put(const ObjectKey& key,
![mooncake-store-simple-put](../../image/mooncake-store-simple-put.png)
用于存储 `key` 对应的值。可通过 `config` 参数设置所需的副本数量。(当启用了持久化功能时,`Put`除了对memory pool的写入之外还会异步发起一次向SSD的数据持久化操作
**副本保证和尽力而为行为:**
- 保证对象的每个slice被复制到不同的segment确保分布在不同的存储节点上
- 不同对象的slice可能被放置在同一个segment中
- 副本采用尽力而为的方式运行:如果没有足够的空间来分配所有请求的副本,对象仍将被写入,副本数量为实际能够分配的数量
其中`ReplicateConfig` 的数据结构细节如下:
```C++
@ -253,7 +259,7 @@ message PutStartResponse {
* 请求: PutStartRequest包含 key、数据长度和副本配置config。
* 响应: PutStartResponse包含状态码 status_code 和分配好的副本信息 replica_list。
说明: Client 在写入对象前,需要先调用 PutStart 向 `Master Service` 申请存储空间。`Master Service` 会根据 config 分配空间并将分配结果replica_list返回给 Client。Client 随后将数据写入到分配副本所在的存储节点。 之所以需要 start 和 end 两步是为确保其他Client不会读到正在写的值进而造成脏读。
说明: Client 在写入对象前,需要先调用 PutStart 向 `Master Service` 申请存储空间。`Master Service` 会根据 config 分配空间并将分配结果replica_list返回给 Client。分配策略确保对象的每个slice被放置在不同的segment中同时采用尽力而为的方式运行——如果没有足够的空间来分配所有请求的副本将分配尽可能多的副本。Client 随后将数据写入到分配副本所在的存储节点。 之所以需要 start 和 end 两步是为确保其他Client不会读到正在写的值进而造成脏读。
4. PutEnd

View File

@ -4,7 +4,7 @@ Mooncake aims to enhance the inference efficiency of large language models (LLMs
Mooncake:
- provides object-level data storage services
- supports data replication in the cache layer, with a lightweight design due to not guaranteeing high availability
- supports data replication in the cache layer with slice-level placement guarantees and best-effort allocation, with a lightweight design due to not guaranteeing high availability
- ensures the atomicity of object write operations, meaning a `Get` operation will always read one consistent version, but not necessarily the latest one
- supports striping and parallel I/O transfer for larger objects to utilize the aggregated bandwidth of multiple network cards
- supports multiple modes for flushing slow object storage

View File

@ -10,7 +10,7 @@ Mooncake Store provides low-level object storage and management capabilities, in
Key features of Mooncake Store include:
- **Object-level storage operations**: Mooncake Store provides simple and easy-to-use object-level APIs, including `Put`, `Get`, and `Remove` operations.
- **Multi-replica support**: Mooncake Store supports storing multiple data replicas for the same object, effectively alleviating hotspots in access pressure.
- **Multi-replica support**: Mooncake Store supports storing multiple data replicas for the same object, effectively alleviating hotspots in access pressure. Each slice within an object is guaranteed to be placed in different segments, while different objects' slices may share segments. Replication operates on a best-effort basis.
- **Strong consistency**: Mooncake Store Guarantees that `Get` operations always read accurate and complete data, and after a successful write, all subsequent Gets will return the most recent value.
- **Zero-copy, bandwidth-saturating transfers**: Powered by the Transfer Engine, Mooncake Store eliminates redundant memory copies and exploits multi-NIC GPUDirect RDMA pooling to drive data across the network at full line rate while keeping CPU overhead negligible.
- **High bandwidth utilization**: Mooncake Store supports striping and parallel I/O transfer of large objects, fully utilizing multi-NIC aggregated bandwidth for high-speed data reads and writes.
@ -89,7 +89,14 @@ tl::expected<void, ErrorCode> Put(const ObjectKey& key,
![mooncake-store-simple-put](../image/mooncake-store-simple-put.png)
Used to store the value corresponding to `key`. The required number of replicas can be set via the `config` parameter.(When persistence is enabled, Put not only writes to the memory pool but also asynchronously initiates a data persistence operation to the SSD.) The data structure details of `ReplicateConfig` are as follows:
Used to store the value corresponding to `key`. The required number of replicas can be set via the `config` parameter.(When persistence is enabled, Put not only writes to the memory pool but also asynchronously initiates a data persistence operation to the SSD.)
**Replication Guarantees and Best Effort Behavior:**
- Each slice of an object is guaranteed to be replicated to different segments, ensuring distribution across separate storage nodes
- Different slices from different objects may be placed in the same segment
- Replication operates on a best-effort basis: if insufficient space is available for all requested replicas, the object will still be written with as many replicas as possible
The data structure details of `ReplicateConfig` are as follows:
```C++
struct ReplicateConfig {
@ -246,7 +253,7 @@ message PutStartResponse {
- **Request**: `PutStartRequest` containing the key, data length, and replica configuration config.
- **Response**: `PutStartResponse` containing the status code status_code and the allocated replica information replica_list.
- **Description**: Before writing an object, the Client must call PutStart to request storage space from the Master Service. The Master Service allocates space based on the config and returns the allocation results (`replica_list`) to the Client. The Client then writes data to the storage nodes where the allocated replicas are located. The need for both start and end steps ensures that other Clients do not read partially written values, preventing dirty reads.
- **Description**: Before writing an object, the Client must call PutStart to request storage space from the Master Service. The Master Service allocates space based on the config and returns the allocation results (`replica_list`) to the Client. The allocation strategy ensures that each slice of the object is placed in different segments, while operating on a best-effort basis - if insufficient space is available for all requested replicas, as many replicas as possible will be allocated. The Client then writes data to the storage nodes where the allocated replicas are located. The need for both start and end steps ensures that other Clients do not read partially written values, preventing dirty reads.
4. PutEnd

View File

@ -1,9 +1,13 @@
#pragma once
#include <algorithm>
#include <memory>
#include <optional>
#include <random>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <ylt/util/tl/expected.hpp>
#include "allocator.h" // Contains BufferAllocator declaration
#include "types.h"
@ -11,127 +15,241 @@
namespace mooncake {
/**
* @brief Abstract interface for allocation strategy, responsible for choosing
* among multiple BufferAllocators.
* @brief Abstract interface for allocation strategy, responsible for
* allocating multiple slices across multiple replicas using available
* BufferAllocators.
*
* The allocation strategy follows best-effort semantics: if the requested
* number of replicas cannot be fully satisfied due to resource constraints,
* it will allocate as many replicas as possible rather than failing entirely.
* Only returns an error if no replicas can be allocated at all.
*/
class AllocationStrategy {
public:
virtual ~AllocationStrategy() = default;
/**
* @brief Given all mounted BufferAllocators and required object size,
* the strategy can freely choose a suitable BufferAllocator.
* @brief Allocates multiple slices across the requested number of replicas
* using best-effort semantics. Each replica will contain all
* requested slices.
*
* The allocation follows best-effort semantics: if the full requested
* replica count cannot be satisfied, the method will allocate as many
* replicas as possible across different segments. For each slice, replicas
* are guaranteed to be placed on different segments to ensure redundancy.
*
* @param allocators Container of mounted allocators
* @param allocators_by_name Container of mounted allocators, key is
* segment_name, value is the corresponding allocator
* @param objectSize Size of object to be allocated
* @param config Replica configuration
* @return Selected allocator; returns nullptr if allocation is not possible
* or no suitable allocator is found
* segment_name, value is the corresponding
* allocators
* @param slice_sizes Sizes of slices to be allocated in each replica
* @param config Replica configuration containing number of replicas and
* placement constraints
* @return tl::expected<std::vector<Replica>, ErrorCode> containing
* allocated replicas.
* - On success: vector of allocated replicas (may be fewer than
* requested due to resource constraints, but at least 1)
* - On failure: ErrorCode::NO_AVAILABLE_HANDLE if no replicas can
* be allocated, ErrorCode::INVALID_PARAMS for invalid
* configuration
*/
virtual std::unique_ptr<AllocatedBuffer> Allocate(
virtual tl::expected<std::vector<Replica>, ErrorCode> Allocate(
const std::vector<std::shared_ptr<BufferAllocatorBase>>& allocators,
const std::unordered_map<
std::string, std::vector<std::shared_ptr<BufferAllocatorBase>>>&
allocators_by_name,
size_t objectSize, const ReplicateConfig& config) = 0;
const std::vector<size_t>& slice_sizes,
const ReplicateConfig& config) = 0;
};
/**
* @brief Random allocation strategy with local preference support.
* @brief Random batch allocation strategy with local preference and
* replication guarantees support using best-effort semantics.
*
* This strategy first attempts to allocate from a preferred segment if
* specified, then falls back to random allocation among all available
* allocators.
* This strategy ensures that for each slice, its replicas are placed in
* different segments. Different slices may use the same segments.
*
* Best-effort behavior:
* - Attempts to allocate the requested number of replicas
* - If insufficient segments are available, allocates as many replicas as
* possible (limited by the number of available segments)
* - Only fails if no replicas can be allocated at all
* - Preferred segment allocation is attempted first if specified
*/
class RandomAllocationStrategy : public AllocationStrategy {
public:
RandomAllocationStrategy() : rng_(std::random_device{}()) {}
std::unique_ptr<AllocatedBuffer> Allocate(
tl::expected<std::vector<Replica>, ErrorCode> Allocate(
const std::vector<std::shared_ptr<BufferAllocatorBase>>& allocators,
const std::unordered_map<
std::string, std::vector<std::shared_ptr<BufferAllocatorBase>>>&
allocators_by_name,
size_t objectSize, const ReplicateConfig& config) override {
// Fast path: single allocator case
if (allocators.size() == 1) {
return allocators[0]->allocate(objectSize);
const std::vector<size_t>& slice_sizes, const ReplicateConfig& config) {
if (auto validation_error =
validateInput(slice_sizes, config.replica_num)) {
return tl::make_unexpected(*validation_error);
}
// Try preferred segment first if specified
if (auto preferred_buffer =
TryPreferredAllocate(allocators_by_name, objectSize, config)) {
return preferred_buffer;
std::vector<std::vector<std::unique_ptr<AllocatedBuffer>>>
replica_buffers(config.replica_num);
for (auto& replica_buffer : replica_buffers) {
replica_buffer.reserve(slice_sizes.size());
}
// Fall back to random allocation among all eligible allocators
return TryRandomAllocate(allocators, objectSize);
// Track the actual number of replicas we can allocate
size_t actual_replica_count = config.replica_num;
// Allocate each slice across replicas
for (size_t slice_idx = 0; slice_idx < slice_sizes.size();
++slice_idx) {
auto slice_replicas = allocateSlice(allocators, allocators_by_name,
slice_sizes[slice_idx],
actual_replica_count, config);
if (slice_replicas.empty()) {
return tl::make_unexpected(ErrorCode::NO_AVAILABLE_HANDLE);
}
if (slice_replicas.size() < actual_replica_count) {
actual_replica_count = slice_replicas.size();
// NOTE: replica allocation is best effort
LOG(WARNING)
<< "Failed to allocate all replicas for slice " << slice_idx
<< ", reducing replica count to " << actual_replica_count;
// Resize replica_buffers to match the new count
replica_buffers.resize(actual_replica_count);
}
for (size_t replica_idx = 0; replica_idx < actual_replica_count;
++replica_idx) {
replica_buffers[replica_idx].push_back(
std::move(slice_replicas[replica_idx]));
}
}
std::vector<Replica> replicas;
replicas.reserve(actual_replica_count);
for (size_t replica_idx = 0; replica_idx < actual_replica_count;
++replica_idx) {
replicas.emplace_back(std::move(replica_buffers[replica_idx]),
ReplicaStatus::PROCESSING);
}
return replicas;
}
private:
static constexpr size_t kMaxRetryLimit = 10;
std::mt19937 rng_;
std::mt19937 rng_; // Mersenne Twister random number generator
/**
* @brief Attempts allocation from preferred segment if available and
* eligible
*/
std::unique_ptr<AllocatedBuffer> TryPreferredAllocate(
const std::unordered_map<
std::string, std::vector<std::shared_ptr<BufferAllocatorBase>>>&
allocators,
size_t objectSize, const ReplicateConfig& config) {
if (config.preferred_segment.empty()) {
return nullptr;
std::optional<ErrorCode> validateInput(
const std::vector<size_t>& slice_sizes, size_t replica_num) const {
if (replica_num == 0 || slice_sizes.empty() ||
std::count(slice_sizes.begin(), slice_sizes.end(), 0) > 0) {
return ErrorCode::INVALID_PARAMS;
}
auto preferred_it = allocators.find(config.preferred_segment);
if (preferred_it == allocators.end()) {
return nullptr;
}
auto& preferred_allocators = preferred_it->second;
for (auto& allocator : preferred_allocators) {
auto buffer = allocator->allocate(objectSize);
if (buffer != nullptr) {
return buffer;
}
}
return nullptr;
return std::nullopt;
}
/**
* @brief Attempts allocation with random selection and retry logic
* @brief Allocates replicas for a single slice across different segments
*/
std::unique_ptr<AllocatedBuffer> TryRandomAllocate(
std::vector<std::unique_ptr<AllocatedBuffer>> allocateSlice(
const std::vector<std::shared_ptr<BufferAllocatorBase>>& allocators,
size_t objectSize) {
const size_t max_tries = std::min(kMaxRetryLimit, allocators.size());
const std::unordered_map<
std::string, std::vector<std::shared_ptr<BufferAllocatorBase>>>&
allocators_by_name,
size_t slice_size, size_t replica_num, const ReplicateConfig& config,
std::unordered_set<std::string>& used_segments) {
std::vector<std::unique_ptr<AllocatedBuffer>> buffers;
buffers.reserve(replica_num);
std::vector<size_t> allocator_indices(allocators.size());
std::iota(allocator_indices.begin(), allocator_indices.end(), 0);
for (size_t i = 0; i < replica_num; ++i) {
auto buffer =
allocateSingleBuffer(allocators, allocators_by_name, slice_size,
config, used_segments);
for (size_t try_count = 0; try_count < max_tries; ++try_count) {
// Randomly select an allocator
std::uniform_int_distribution<size_t> dist(
0, allocator_indices.size() - 1);
const size_t random_index = allocator_indices[dist(rng_)];
if (!buffer) {
break;
}
auto& allocator = allocators[random_index];
if (auto buffer = allocator->allocate(objectSize)) {
used_segments.insert(buffer->getSegmentName());
buffers.push_back(std::move(buffer));
}
return buffers;
}
std::vector<std::unique_ptr<AllocatedBuffer>> allocateSlice(
const std::vector<std::shared_ptr<BufferAllocatorBase>>& allocators,
const std::unordered_map<
std::string, std::vector<std::shared_ptr<BufferAllocatorBase>>>&
allocators_by_name,
size_t slice_size, size_t replica_num, const ReplicateConfig& config) {
std::unordered_set<std::string> empty_segments;
return allocateSlice(allocators, allocators_by_name, slice_size,
replica_num, config, empty_segments);
}
/**
* @brief Allocates a single buffer respecting preferences and exclusions
*/
std::unique_ptr<AllocatedBuffer> allocateSingleBuffer(
const std::vector<std::shared_ptr<BufferAllocatorBase>>& allocators,
const std::unordered_map<
std::string, std::vector<std::shared_ptr<BufferAllocatorBase>>>&
allocators_by_name,
size_t size, const ReplicateConfig& config,
const std::unordered_set<std::string>& excluded_segments) {
// Try preferred segment first
if (!config.preferred_segment.empty() &&
!excluded_segments.contains(config.preferred_segment)) {
auto preferred_it =
allocators_by_name.find(config.preferred_segment);
if (preferred_it != allocators_by_name.end()) {
for (auto& allocator : preferred_it->second) {
if (auto buffer = allocator->allocate(size)) {
return buffer;
}
}
}
}
return tryRandomAllocate(allocators, size, excluded_segments);
}
/**
* @brief Attempts allocation with random selection
*/
std::unique_ptr<AllocatedBuffer> tryRandomAllocate(
const std::vector<std::shared_ptr<BufferAllocatorBase>>& allocators,
size_t size, const std::unordered_set<std::string>& excluded_segments) {
std::vector<size_t> eligible_indices;
eligible_indices.reserve(allocators.size());
for (size_t i = 0; i < allocators.size(); ++i) {
if (!excluded_segments.contains(allocators[i]->getSegmentName())) {
eligible_indices.push_back(i);
}
}
if (eligible_indices.empty()) {
return nullptr;
}
std::shuffle(eligible_indices.begin(), eligible_indices.end(), rng_);
const size_t max_tries =
std::min(kMaxRetryLimit, eligible_indices.size());
for (size_t i = 0; i < max_tries; ++i) {
auto& allocator = allocators[eligible_indices[i]];
if (auto buffer = allocator->allocate(size)) {
return buffer;
}
// Remove failed allocator and continue with remaining ones
if (random_index + 1 != allocator_indices.size()) {
std::swap(allocator_indices[random_index],
allocator_indices[allocator_indices.size() - 1]);
}
allocator_indices.pop_back();
}
return nullptr;
}
};

View File

@ -5,6 +5,7 @@
#include <cstdint>
#include <map>
#include <memory>
#include <optional>
#include <string>
#include <unordered_map>
#include <variant>
@ -289,6 +290,10 @@ class AllocatedBuffer {
// Serialize the buffer into a descriptor for transfer
[[nodiscard]] Descriptor get_descriptor() const;
[[nodiscard]] std::string getSegmentName() const noexcept {
return segment_name_;
}
// Friend declaration for operator<<
friend std::ostream& operator<<(std::ostream& os,
const AllocatedBuffer& buffer);
@ -393,6 +398,9 @@ class Replica {
return false; // DiskReplicaData does not have handles
}
[[nodiscard]] std::vector<std::optional<std::string>> get_segment_names()
const;
void mark_complete() {
if (status_ == ReplicaStatus::PROCESSING) {
status_ = ReplicaStatus::COMPLETE;
@ -504,6 +512,25 @@ inline Replica::Descriptor Replica::get_descriptor() const {
return desc;
}
inline std::vector<std::optional<std::string>> Replica::get_segment_names()
const {
if (is_memory_replica()) {
const auto& mem_data = std::get<MemoryReplicaData>(data_);
std::vector<std::optional<std::string>> segment_names(
mem_data.buffers.size());
for (size_t i = 0; i < mem_data.buffers.size(); ++i) {
if (mem_data.buffers[i] &&
mem_data.buffers[i]->isAllocatorValid()) {
segment_names[i] = mem_data.buffers[i]->getSegmentName();
} else {
segment_names[i] = std::nullopt;
}
}
return segment_names;
}
return std::vector<std::optional<std::string>>();
}
inline std::ostream& operator<<(std::ostream& os, const Replica& replica) {
os << "Replica: { status: " << replica.status_ << ", ";

View File

@ -392,40 +392,26 @@ auto MasterService::PutStart(const std::string& key,
// Allocate replicas
std::vector<Replica> replicas;
replicas.reserve(config.replica_num + use_disk_replica_);
{
ScopedAllocatorAccess allocator_access =
segment_manager_.getAllocatorAccess();
auto& allocators = allocator_access.getAllocators();
auto& allocators_by_name = allocator_access.getAllocatorsByName();
for (size_t i = 0; i < config.replica_num; ++i) {
std::vector<std::unique_ptr<AllocatedBuffer>> handles;
handles.reserve(slice_lengths.size());
// Allocate space for each slice
for (size_t j = 0; j < slice_lengths.size(); ++j) {
auto chunk_size = slice_lengths[j];
auto allocation_result = allocation_strategy_->Allocate(
allocators, allocators_by_name, slice_lengths, config);
// Use the unified allocation strategy with replica config
auto handle = allocation_strategy_->Allocate(
allocators, allocators_by_name, chunk_size, config);
if (!handle) {
// If the allocation failed, we need to evict some objects
// to free up space for future allocations.
need_eviction_ = true;
return tl::make_unexpected(ErrorCode::NO_AVAILABLE_HANDLE);
}
VLOG(1) << "key=" << key << ", replica_id=" << i
<< ", slice_index=" << j << ", handle=" << *handle
<< ", action=slice_allocated";
handles.emplace_back(std::move(handle));
if (!allocation_result.has_value()) {
LOG(ERROR) << "Failed to allocate all replicas for key=" << key
<< ", error: " << allocation_result.error();
if (allocation_result.error() == ErrorCode::INVALID_PARAMS) {
return tl::make_unexpected(ErrorCode::INVALID_PARAMS);
}
replicas.emplace_back(std::move(handles),
ReplicaStatus::PROCESSING);
need_eviction_ = true;
return tl::make_unexpected(ErrorCode::NO_AVAILABLE_HANDLE);
}
replicas = std::move(allocation_result.value());
}
// If disk replica is enabled, allocate a disk replica

View File

@ -23,7 +23,8 @@ class AllocationStrategyTest : public ::testing::Test {
const std::string& segment_name, size_t base_offset,
BufferAllocatorType allocator_type) {
const size_t base = 0x100000000ULL + base_offset; // 4GB + offset
const size_t size = 1024 * 1024 * 16; // 16MB (multiple of 4MB)
const size_t size =
1024 * 1024 * 64; // 64MB (for multiple slabs in cachelib)
switch (allocator_type) {
case BufferAllocatorType::CACHELIB:
return std::make_shared<CachelibBufferAllocator>(segment_name,
@ -50,9 +51,11 @@ TEST_F(AllocationStrategyTest, EmptyAllocatorsMap) {
std::vector<std::shared_ptr<BufferAllocatorBase>> empty_allocators;
ReplicateConfig config{1, false, "local"};
auto result = strategy_->Allocate(empty_allocators,
empty_allocators_by_name, 100, config);
EXPECT_EQ(result, nullptr);
std::vector<size_t> slice_sizes = {100};
auto result = strategy_->Allocate(
empty_allocators, empty_allocators_by_name, slice_sizes, config);
EXPECT_FALSE(result.has_value());
EXPECT_EQ(result.error(), ErrorCode::NO_AVAILABLE_HANDLE);
}
// Test preferred segment behavior with empty allocators
@ -63,9 +66,11 @@ TEST_F(AllocationStrategyTest, PreferredSegmentWithEmptyAllocators) {
std::vector<std::shared_ptr<BufferAllocatorBase>> empty_allocators;
ReplicateConfig config{1, false, "preferred_segment"};
auto result = strategy_->Allocate(empty_allocators,
empty_allocators_by_name, 100, config);
EXPECT_EQ(result, nullptr); // Should return nullptr for empty allocators
std::vector<size_t> slice_sizes = {100};
auto result = strategy_->Allocate(
empty_allocators, empty_allocators_by_name, slice_sizes, config);
EXPECT_FALSE(result.has_value());
EXPECT_EQ(result.error(), ErrorCode::NO_AVAILABLE_HANDLE);
}
// Test preferred segment allocation when available
@ -86,13 +91,21 @@ TEST_F(AllocationStrategyTest, PreferredSegmentAllocation) {
allocators.push_back(allocator2);
ReplicateConfig config{1, false, "preferred"};
size_t alloc_size = 1024;
std::vector<size_t> slice_sizes = {1024};
auto result = strategy_->Allocate(allocators, allocators_by_name,
alloc_size, config);
ASSERT_NE(result, nullptr);
EXPECT_EQ(result->get_descriptor().segment_name_, "preferred");
EXPECT_EQ(result->get_descriptor().size_, alloc_size);
slice_sizes, config);
ASSERT_TRUE(result.has_value());
EXPECT_EQ(result.value().size(), 1);
ASSERT_FALSE(result.value().empty());
const auto& replica = result.value()[0];
auto descriptor = replica.get_descriptor();
ASSERT_TRUE(descriptor.is_memory_replica());
const auto& mem_desc = descriptor.get_memory_descriptor();
ASSERT_EQ(mem_desc.buffer_descriptors.size(), 1);
EXPECT_EQ(mem_desc.buffer_descriptors[0].segment_name_, "preferred");
EXPECT_EQ(mem_desc.buffer_descriptors[0].size_, 1024);
}
}
@ -114,20 +127,62 @@ TEST_F(AllocationStrategyTest, PreferredSegmentNotFound) {
allocators.push_back(allocator2);
ReplicateConfig config{1, false, "nonexistent"};
size_t alloc_size = 1024;
std::vector<size_t> slice_sizes = {1024};
auto result = strategy_->Allocate(allocators, allocators_by_name,
alloc_size, config);
ASSERT_NE(result, nullptr);
// Should allocate from one of the available segments
std::string segment_name = result->get_descriptor().segment_name_;
slice_sizes, config);
ASSERT_TRUE(result.has_value());
EXPECT_EQ(result.value().size(), 1);
const auto& replica = result.value()[0];
auto descriptor = replica.get_descriptor();
ASSERT_TRUE(descriptor.is_memory_replica());
const auto& mem_desc = descriptor.get_memory_descriptor();
ASSERT_EQ(mem_desc.buffer_descriptors.size(), 1);
std::string segment_name = mem_desc.buffer_descriptors[0].segment_name_;
EXPECT_TRUE(segment_name == "segment1" || segment_name == "segment2");
EXPECT_EQ(result->get_descriptor().size_, alloc_size);
EXPECT_EQ(mem_desc.buffer_descriptors[0].size_, 1024);
}
}
// Test multiple allocators with random selection
TEST_F(AllocationStrategyTest, MultipleAllocatorsRandomSelection) {
// Test multiple slices allocation
TEST_F(AllocationStrategyTest, MultipleSlicesAllocation) {
for (const auto& allocator_type : allocator_types_) {
auto allocator1 = CreateTestAllocator("segment1", 0, allocator_type);
auto allocator2 =
CreateTestAllocator("segment2", 0x10000000ULL, allocator_type);
std::unordered_map<std::string,
std::vector<std::shared_ptr<BufferAllocatorBase>>>
allocators_by_name;
std::vector<std::shared_ptr<BufferAllocatorBase>> allocators;
allocators_by_name["segment1"].push_back(allocator1);
allocators_by_name["segment2"].push_back(allocator2);
allocators.push_back(allocator1);
allocators.push_back(allocator2);
ReplicateConfig config{1, false, ""};
std::vector<size_t> slice_sizes = {1024, 2048, 512};
auto result = strategy_->Allocate(allocators, allocators_by_name,
slice_sizes, config);
ASSERT_TRUE(result.has_value());
EXPECT_EQ(result.value().size(), 1);
const auto& replica = result.value()[0];
auto descriptor = replica.get_descriptor();
ASSERT_TRUE(descriptor.is_memory_replica());
const auto& mem_desc = descriptor.get_memory_descriptor();
ASSERT_EQ(mem_desc.buffer_descriptors.size(), 3);
EXPECT_EQ(mem_desc.buffer_descriptors[0].size_, 1024);
EXPECT_EQ(mem_desc.buffer_descriptors[1].size_, 2048);
EXPECT_EQ(mem_desc.buffer_descriptors[2].size_, 512);
}
}
// Test multiple replicas allocation
TEST_F(AllocationStrategyTest, MultipleReplicasAllocation) {
for (const auto& allocator_type : allocator_types_) {
auto allocator1 = CreateTestAllocator("segment1", 0, allocator_type);
auto allocator2 =
@ -147,24 +202,33 @@ TEST_F(AllocationStrategyTest, MultipleAllocatorsRandomSelection) {
allocators.push_back(allocator2);
allocators.push_back(allocator3);
ReplicateConfig config{1, false, ""}; // No preferred segment
size_t alloc_size = 1024;
ReplicateConfig config{3, false, ""}; // Request 3 replicas
std::vector<size_t> slice_sizes = {1024, 2048};
// Perform multiple allocations to test randomness
std::vector<std::string> allocated_segments;
for (int i = 0; i < 10; ++i) {
auto result = strategy_->Allocate(allocators, allocators_by_name,
alloc_size, config);
ASSERT_NE(result, nullptr);
allocated_segments.push_back(
result->get_descriptor().segment_name_);
EXPECT_EQ(result->get_descriptor().size_, alloc_size);
auto result = strategy_->Allocate(allocators, allocators_by_name,
slice_sizes, config);
ASSERT_TRUE(result.has_value());
EXPECT_EQ(result.value().size(), 3);
// Check each replica has all slices
for (const auto& replica : result.value()) {
auto descriptor = replica.get_descriptor();
ASSERT_TRUE(descriptor.is_memory_replica());
const auto& mem_desc = descriptor.get_memory_descriptor();
ASSERT_EQ(mem_desc.buffer_descriptors.size(), 2);
EXPECT_EQ(mem_desc.buffer_descriptors[0].size_, 1024);
EXPECT_EQ(mem_desc.buffer_descriptors[1].size_, 2048);
}
// Verify that allocations happened on available segments
for (const auto& segment : allocated_segments) {
EXPECT_TRUE(segment == "segment1" || segment == "segment2" ||
segment == "segment3");
// Check that replicas are on different segments
std::set<std::string> used_segments;
for (const auto& replica : result.value()) {
auto segment_names = replica.get_segment_names();
for (const auto& name_ptr : segment_names) {
if (name_ptr) {
used_segments.insert(*name_ptr);
}
}
}
}
}
@ -188,25 +252,31 @@ TEST_F(AllocationStrategyTest, PreferredSegmentInsufficientSpace) {
// First, fill up the preferred allocator
ReplicateConfig config{1, false, "preferred"};
std::vector<std::unique_ptr<AllocatedBuffer>> buffers;
std::vector<size_t> large_slices = {
10 * 1024 * 1024, 10 * 1024 * 1024, 10 * 1024 * 1024,
10 * 1024 * 1024, 10 * 1024 * 1024, 10 * 1024 * 1024,
3 * 1024 * 1024}; // 63MB out of 64MB
// Allocate most of the space in preferred segment
size_t large_alloc = 15 * 1024 * 1024; // 15MB out of 16MB
auto large_buffer = strategy_->Allocate(allocators, allocators_by_name,
large_alloc, config);
ASSERT_NE(large_buffer, nullptr);
EXPECT_EQ(large_buffer->get_descriptor().segment_name_, "preferred");
buffers.push_back(std::move(large_buffer));
auto large_result = strategy_->Allocate(allocators, allocators_by_name,
large_slices, config);
ASSERT_TRUE(large_result.has_value());
auto large_desc = large_result.value()[0].get_descriptor();
ASSERT_TRUE(large_desc.is_memory_replica());
EXPECT_EQ(large_desc.get_memory_descriptor()
.buffer_descriptors[0]
.segment_name_,
"preferred");
// Now try to allocate more than remaining space in preferred segment
size_t small_alloc = 2 * 1024 * 1024; // 2MB (more than remaining ~1MB)
std::vector<size_t> small_slice = {2 * 1024 * 1024};
auto result = strategy_->Allocate(allocators, allocators_by_name,
small_alloc, config);
ASSERT_NE(result, nullptr);
// Should fall back to segment1 since preferred doesn't have enough
// space
EXPECT_EQ(result->get_descriptor().segment_name_, "segment1");
EXPECT_EQ(result->get_descriptor().size_, small_alloc);
small_slice, config);
ASSERT_TRUE(result.has_value());
auto small_desc = result.value()[0].get_descriptor();
ASSERT_TRUE(small_desc.is_memory_replica());
const auto& mem_desc = small_desc.get_memory_descriptor();
EXPECT_EQ(mem_desc.buffer_descriptors[0].segment_name_, "segment1");
EXPECT_EQ(mem_desc.buffer_descriptors[0].size_, 2 * 1024 * 1024);
}
}
@ -228,24 +298,25 @@ TEST_F(AllocationStrategyTest, AllAllocatorsFull) {
allocators.push_back(allocator2);
ReplicateConfig config{1, false, ""};
std::vector<std::unique_ptr<AllocatedBuffer>> buffers;
// Fill up both allocators
size_t large_alloc = 15 * 1024 * 1024; // 15MB each
auto buffer1 = strategy_->Allocate(allocators, allocators_by_name,
large_alloc, config);
auto buffer2 = strategy_->Allocate(allocators, allocators_by_name,
large_alloc, config);
ASSERT_NE(buffer1, nullptr);
ASSERT_NE(buffer2, nullptr);
buffers.push_back(std::move(buffer1));
buffers.push_back(std::move(buffer2));
std::vector<size_t> large_slices = {15 * 1024 * 1024, 15 * 1024 * 1024,
15 * 1024 * 1024,
15 * 1024 * 1024}; // 60MB
auto result1 = strategy_->Allocate(allocators, allocators_by_name,
large_slices, config);
ASSERT_TRUE(result1.has_value());
auto result2 = strategy_->Allocate(allocators, allocators_by_name,
large_slices, config);
ASSERT_TRUE(result2.has_value());
// Try to allocate more than remaining space
size_t impossible_alloc = 5 * 1024 * 1024; // 5MB (more than remaining)
std::vector<size_t> impossible_slice = {
5 * 1024 * 1024}; // 5MB (more than remaining)
auto result = strategy_->Allocate(allocators, allocators_by_name,
impossible_alloc, config);
EXPECT_EQ(result, nullptr); // Should fail
impossible_slice, config);
EXPECT_FALSE(result.has_value());
EXPECT_EQ(result.error(), ErrorCode::NO_AVAILABLE_HANDLE);
}
}
@ -262,14 +333,12 @@ TEST_F(AllocationStrategyTest, ZeroSizeAllocation) {
allocators.push_back(allocator);
ReplicateConfig config{1, false, ""};
std::vector<size_t> zero_slice = {0};
auto result =
strategy_->Allocate(allocators, allocators_by_name, 0, config);
// Zero-size allocation behavior depends on BufferAllocator
// implementation This test documents the current behavior
if (result != nullptr) {
EXPECT_EQ(result->get_descriptor().segment_name_, "segment1");
}
auto result = strategy_->Allocate(allocators, allocators_by_name,
zero_slice, config);
EXPECT_FALSE(result.has_value());
EXPECT_EQ(result.error(), ErrorCode::INVALID_PARAMS);
}
}
@ -286,13 +355,104 @@ TEST_F(AllocationStrategyTest, VeryLargeSizeAllocation) {
allocators.push_back(allocator);
ReplicateConfig config{1, false, ""};
size_t huge_size =
100 * 1024 * 1024; // 100MB (larger than 16MB capacity)
std::vector<size_t> huge_slice = {
100 * 1024 * 1024}; // 100MB (larger than 64MB capacity)
auto result = strategy_->Allocate(allocators, allocators_by_name,
huge_size, config);
EXPECT_EQ(result, nullptr); // Should fail due to insufficient capacity
huge_slice, config);
EXPECT_FALSE(result.has_value());
EXPECT_EQ(result.error(), ErrorCode::NO_AVAILABLE_HANDLE);
}
}
// Test empty slice sizes
TEST_F(AllocationStrategyTest, EmptySliceSizes) {
auto allocator =
CreateTestAllocator("segment1", 0, BufferAllocatorType::OFFSET);
std::unordered_map<std::string,
std::vector<std::shared_ptr<BufferAllocatorBase>>>
allocators_by_name;
std::vector<std::shared_ptr<BufferAllocatorBase>> allocators;
allocators_by_name["segment1"].push_back(allocator);
allocators.push_back(allocator);
ReplicateConfig config{1, false, ""};
std::vector<size_t> empty_slices;
auto result = strategy_->Allocate(allocators, allocators_by_name,
empty_slices, config);
EXPECT_FALSE(result.has_value());
EXPECT_EQ(result.error(), ErrorCode::INVALID_PARAMS);
}
// Test invalid replication count
TEST_F(AllocationStrategyTest, InvalidReplicationCount) {
auto allocator =
CreateTestAllocator("segment1", 0, BufferAllocatorType::OFFSET);
std::unordered_map<std::string,
std::vector<std::shared_ptr<BufferAllocatorBase>>>
allocators_by_name;
std::vector<std::shared_ptr<BufferAllocatorBase>> allocators;
allocators_by_name["segment1"].push_back(allocator);
allocators.push_back(allocator);
ReplicateConfig config{0, false, ""}; // Invalid: 0 replicas
std::vector<size_t> slice_sizes = {1024};
auto result = strategy_->Allocate(allocators, allocators_by_name,
slice_sizes, config);
EXPECT_FALSE(result.has_value());
EXPECT_EQ(result.error(), ErrorCode::INVALID_PARAMS);
}
// Test best-effort behavior when insufficient allocators for requested replica
// count
TEST_F(AllocationStrategyTest, InsufficientAllocatorsForReplicas) {
auto allocator1 =
CreateTestAllocator("segment1", 0, BufferAllocatorType::OFFSET);
auto allocator2 = CreateTestAllocator("segment2", 0x10000000ULL,
BufferAllocatorType::OFFSET);
std::unordered_map<std::string,
std::vector<std::shared_ptr<BufferAllocatorBase>>>
allocators_by_name;
std::vector<std::shared_ptr<BufferAllocatorBase>> allocators;
allocators_by_name["segment1"].push_back(allocator1);
allocators_by_name["segment2"].push_back(allocator2);
allocators.push_back(allocator1);
allocators.push_back(allocator2);
ReplicateConfig config{
5, false, ""}; // Request 5 replicas, but only 2 segments available
std::vector<size_t> slice_sizes = {1024};
auto result = strategy_->Allocate(allocators, allocators_by_name,
slice_sizes, config);
// With best-effort semantics, should succeed with available replicas
EXPECT_TRUE(result.has_value());
// Should get 2 replicas (limited by number of segments)
EXPECT_EQ(2u, result.value().size());
// Verify each replica has the expected slice structure
for (const auto& replica : result.value()) {
auto descriptor = replica.get_descriptor();
ASSERT_TRUE(descriptor.is_memory_replica());
const auto& mem_desc = descriptor.get_memory_descriptor();
ASSERT_EQ(mem_desc.buffer_descriptors.size(), 1u);
EXPECT_EQ(mem_desc.buffer_descriptors[0].size_, 1024u);
}
// Verify replicas are on different segments
std::unordered_set<std::string> segment_names;
for (const auto& replica : result.value()) {
auto descriptor = replica.get_descriptor();
const auto& mem_desc = descriptor.get_memory_descriptor();
segment_names.insert(mem_desc.buffer_descriptors[0].segment_name_);
}
EXPECT_EQ(2u, segment_names.size());
}
} // namespace mooncake

View File

@ -8,6 +8,7 @@
#include <random>
#include <thread>
#include <vector>
#include <unordered_set>
#include "types.h"
@ -355,15 +356,17 @@ TEST_F(MasterServiceTest, PutStartEndFlow) {
TEST_F(MasterServiceTest, RandomPutStartEndFlow) {
std::unique_ptr<MasterService> service_(new MasterService());
constexpr size_t buffer = 0x300000000;
constexpr size_t size = 1024 * 1024 * 16;
std::string segment_name = "test_segment";
Segment segment(generate_uuid(), segment_name, buffer, size);
// Mount 5 segments, each 16MB
constexpr size_t kBaseAddr = 0x300000000;
constexpr size_t kSegmentSize = 1024 * 1024 * 16; // 16MB
UUID client_id = generate_uuid();
auto mount_result = service_->MountSegment(segment, client_id);
ASSERT_TRUE(mount_result.has_value());
for (int i = 0; i < 5; ++i) {
Segment segment(generate_uuid(), "segment_" + std::to_string(i),
kBaseAddr + static_cast<size_t>(i) * kSegmentSize,
kSegmentSize);
ASSERT_TRUE(service_->MountSegment(segment, client_id).has_value());
}
// Test PutStart
std::string key = "test_key";
@ -954,20 +957,22 @@ TEST_F(MasterServiceTest, RemoveAll) {
}
TEST_F(MasterServiceTest, MultiSliceMultiReplicaFlow) {
auto service_config = MasterServiceConfig::builder().build();
const uint64_t kv_lease_ttl = 50;
auto service_config = MasterServiceConfig::builder()
.set_default_kv_lease_ttl(kv_lease_ttl)
.build();
std::unique_ptr<MasterService> service_(new MasterService(service_config));
// Mount a segment with sufficient size for multiple replicas
constexpr size_t buffer = 0x300000000;
constexpr size_t segment_size =
1024 * 1024 * 64; // 64MB to accommodate multiple replicas
std::string segment_name = "test_segment_multi";
Segment segment(generate_uuid(), segment_name, buffer, segment_size);
// Mount 3 segments, each 64MB
constexpr size_t kBaseAddr = 0x300000000;
constexpr size_t kSegmentSize = 1024 * 1024 * 64; // 64MB
UUID client_id = generate_uuid();
auto mount_result = service_->MountSegment(segment, client_id);
ASSERT_TRUE(mount_result.has_value());
for (int i = 0; i < 3; ++i) {
Segment segment(generate_uuid(), "segment_" + std::to_string(i),
kBaseAddr + static_cast<size_t>(i) * kSegmentSize,
kSegmentSize);
ASSERT_TRUE(service_->MountSegment(segment, client_id).has_value());
}
// Test parameters
std::string key = "multi_slice_object";
@ -1386,13 +1391,11 @@ TEST_F(MasterServiceTest, UnmountSegmentImmediateCleanup) {
TEST_F(MasterServiceTest, ReadableAfterPartialUnmountWithReplication) {
std::unique_ptr<MasterService> service_(new MasterService());
// TODO: mount two larger segments when replication affinity fixed
// Mount two segments sized to fit exactly one replica each
// Mount two large segments
constexpr size_t buffer1 = 0x300000000;
constexpr size_t buffer2 = 0x400000000;
constexpr size_t segment_size = 1024 * 1024 * 16;
constexpr size_t object_size =
segment_size / 2 + 16; // force at most 1 replica per segment
constexpr size_t segment_size = 1024 * 1024 * 64; // 64MB
constexpr size_t object_size = 1024 * 1024; // 1MB
Segment segment1(generate_uuid(), "segment1", buffer1, segment_size);
Segment segment2(generate_uuid(), "segment2", buffer2, segment_size);
@ -2008,6 +2011,80 @@ TEST_F(MasterServiceTest, SoftPinObjectsNotAllowEvict) {
service_->RemoveAll();
}
TEST_F(MasterServiceTest, PerSliceReplicaSegmentsAreUnique) {
std::unique_ptr<MasterService> service_(new MasterService());
// Mount 20 segments, each 16MB and slab-aligned
constexpr size_t kBaseAddr = 0x300000000;
constexpr size_t kSegmentSize = 1024 * 1024 * 16; // 16MB
UUID client_id = generate_uuid();
for (int i = 0; i < 20; ++i) {
Segment segment(generate_uuid(), "segment_" + std::to_string(i),
kBaseAddr + static_cast<size_t>(i) * kSegmentSize,
kSegmentSize);
ASSERT_TRUE(service_->MountSegment(segment, client_id).has_value());
}
// Object with 16 slices of ~1MB and replication factor 10
const std::string key = "replica_uniqueness_test_key";
std::vector<uint64_t> slice_lengths(16, 1024 * 1024 - 16);
ReplicateConfig config;
config.replica_num = 10;
auto put_start_result = service_->PutStart(key, slice_lengths, config);
ASSERT_TRUE(put_start_result.has_value());
auto replica_list_local = put_start_result.value();
ASSERT_EQ(config.replica_num, replica_list_local.size());
// For each slice index, segment names across replicas must be unique
for (size_t slice_idx = 0; slice_idx < slice_lengths.size(); ++slice_idx) {
std::unordered_set<std::string> segment_names;
for (const auto& replica : replica_list_local) {
ASSERT_TRUE(replica.is_memory_replica());
const auto& mem = replica.get_memory_descriptor();
ASSERT_EQ(slice_lengths.size(), mem.buffer_descriptors.size());
segment_names.insert(
mem.buffer_descriptors[slice_idx].segment_name_);
}
EXPECT_EQ(segment_names.size(), config.replica_num)
<< "Duplicate segment found for slice index " << slice_idx;
}
ASSERT_TRUE(service_->PutEnd(key, ReplicaType::MEMORY).has_value());
}
TEST_F(MasterServiceTest, ReplicationFactorTwoWithSingleSegment) {
std::unique_ptr<MasterService> service_(new MasterService());
// Mount a single 16MB segment
constexpr size_t kBaseAddr = 0x300000000;
constexpr size_t kSegmentSize = 1024 * 1024 * 16; // 16MB
Segment segment(generate_uuid(), "single_segment", kBaseAddr, kSegmentSize);
UUID client_id = generate_uuid();
ASSERT_TRUE(service_->MountSegment(segment, client_id).has_value());
// Request replication factor 2 with a single 1KB slice
// With best-effort semantics, should succeed with 1 replica
const std::string key = "replication_factor_two_single_segment";
std::vector<uint64_t> slice_lengths{1024};
ReplicateConfig config;
config.replica_num = 2;
auto put_start_result = service_->PutStart(key, slice_lengths, config);
ASSERT_TRUE(put_start_result.has_value());
auto replicas = put_start_result.value();
// Should get 1 replica instead of the requested 2 (best-effort)
EXPECT_EQ(1u, replicas.size());
EXPECT_TRUE(replicas[0].is_memory_replica());
// Verify the replica is properly allocated on the single segment
auto mem_desc = replicas[0].get_memory_descriptor();
EXPECT_EQ(1u, mem_desc.buffer_descriptors.size());
EXPECT_EQ("single_segment", mem_desc.buffer_descriptors[0].segment_name_);
EXPECT_EQ(1024u, mem_desc.buffer_descriptors[0].size_);
}
TEST_F(MasterServiceTest, BatchExistKeyTest) {
std::unique_ptr<MasterService> service_(new MasterService());

View File

@ -11,6 +11,7 @@ DEFAULT_DEFAULT_KV_LEASE_TTL = 5000 # 5000 milliseconds
# Use environment variable if set, otherwise use default
default_kv_lease_ttl = int(os.getenv("DEFAULT_KV_LEASE_TTL", DEFAULT_DEFAULT_KV_LEASE_TTL))
def get_client(store, local_buffer_size_param=None):
"""Initialize and setup the distributed store client."""
protocol = os.getenv("PROTOCOL", "tcp")
@ -57,7 +58,9 @@ class TestZeroLocalBufferSize(unittest.TestCase):
result = zero_buffer_store.is_exist(key)
self.assertEqual(result, 0, "Key should not exist after failed put")
class TestDistributedObjectStore(unittest.TestCase):
class TestDistributedObjectStoreSingleStore(unittest.TestCase):
"""Test class for single store operations (no replication)."""
@classmethod
def setUpClass(cls):
"""Initialize the store once for all tests."""
@ -567,214 +570,6 @@ class TestDistributedObjectStore(unittest.TestCase):
self.assertIsInstance(config_str, str)
self.assertIn("3", config_str) # Should contain replica_num
def test_put_with_config_parameter(self):
"""Test put method with config parameter."""
from mooncake.store import ReplicateConfig
test_data = b"Hello, Config World!"
key = "test_put_config_key"
# Test with default config (backward compatibility)
result = self.store.put(key=key, value=test_data)
self.assertEqual(result, 0)
# Verify data
retrieved_data = self.store.get(key)
self.assertEqual(retrieved_data, test_data)
# Clean up
time.sleep(default_kv_lease_ttl / 1000)
self.assertEqual(self.store.remove(key), 0)
# Test with custom config
config = ReplicateConfig()
config.replica_num = 2
key2 = "test_put_config_key2"
result = self.store.put(key=key2, value=test_data, config=config)
self.assertEqual(result, 0)
# Verify data
retrieved_data = self.store.get(key2)
self.assertEqual(retrieved_data, test_data)
# Clean up
time.sleep(default_kv_lease_ttl / 1000)
self.assertEqual(self.store.remove(key2), 0)
with self.assertRaises(TypeError):
result = self.store.put(key_arg_name_error=key, value=test_data, config=config)
with self.assertRaises(TypeError):
result = self.store.put(key=key, value_arg_name_error=test_data, config=config)
with self.assertRaises(TypeError):
result = self.store.put(key=key, value=test_data, config_arg_name_error=config)
def test_put_batch_with_config_parameter(self):
"""Test put_batch method with config parameter."""
from mooncake.store import ReplicateConfig
keys = ["test_batch_config_key1", "test_batch_config_key2", "test_batch_config_key3"]
values = [b"Batch Data 1", b"Batch Data 2", b"Batch Data 3"]
# Test with default config (backward compatibility)
result = self.store.put_batch(keys, values)
self.assertEqual(result, 0)
# Verify data
for key, expected_value in zip(keys, values):
retrieved_data = self.store.get(key)
self.assertEqual(retrieved_data, expected_value)
# Clean up
time.sleep(default_kv_lease_ttl / 1000)
for key in keys:
self.assertEqual(self.store.remove(key), 0)
# Test with custom config
config = ReplicateConfig()
config.replica_num = 2
keys2 = ["test_batch_config_key4", "test_batch_config_key5", "test_batch_config_key6"]
result = self.store.put_batch(keys=keys2, values=values, config=config)
self.assertEqual(result, 0)
# Verify data
for key, expected_value in zip(keys2, values):
retrieved_data = self.store.get(key)
self.assertEqual(retrieved_data, expected_value)
# Clean up
time.sleep(default_kv_lease_ttl / 1000)
for key in keys2:
self.assertEqual(self.store.remove(key), 0)
def test_put_from_with_config_parameter(self):
"""Test put_from method with config parameter."""
import ctypes
from mooncake.store import ReplicateConfig
test_data = b"Hello, put_from config world!"
key = "test_put_from_config_key"
buffer_size = len(test_data)
# Allocate and register buffer
buffer = (ctypes.c_ubyte * buffer_size)()
buffer_ptr = ctypes.addressof(buffer)
result = self.store.register_buffer(buffer_ptr, buffer_size)
self.assertEqual(result, 0)
# Copy test data to buffer
ctypes.memmove(buffer, test_data, len(test_data))
# Test with default config (backward compatibility)
result = self.store.put_from(key=key, buffer_ptr=buffer_ptr, size=len(test_data))
self.assertEqual(result, 0)
# Verify data
retrieved_data = self.store.get(key)
self.assertEqual(retrieved_data, test_data)
# Clean up
time.sleep(default_kv_lease_ttl / 1000)
self.assertEqual(self.store.remove(key), 0)
# Test with custom config
config = ReplicateConfig()
config.replica_num = 2
config.with_soft_pin = False
key2 = "test_put_from_config_key2"
result = self.store.put_from(key=key2, buffer_ptr=buffer_ptr, size=len(test_data), config=config)
self.assertEqual(result, 0)
# Verify data
retrieved_data = self.store.get(key2)
self.assertEqual(retrieved_data, test_data)
# Clean up
time.sleep(default_kv_lease_ttl / 1000)
self.assertEqual(self.store.unregister_buffer(buffer_ptr), 0)
self.assertEqual(self.store.remove(key2), 0)
def test_batch_put_from_with_config_parameter(self):
"""Test batch_put_from method with config parameter."""
import ctypes
from mooncake.store import ReplicateConfig
# Test data
test_data = [
b"Batch Config Data 1",
b"Batch Config Data 2",
b"Batch Config Data 3"
]
keys = ["test_batch_put_from_config_key1", "test_batch_put_from_config_key2", "test_batch_put_from_config_key3"]
# Use large spacing between buffers
buffer_spacing = 1024 * 1024 # 1MB spacing
total_buffer_size = buffer_spacing * len(test_data)
large_buffer = (ctypes.c_ubyte * total_buffer_size)()
large_buffer_ptr = ctypes.addressof(large_buffer)
# Register buffer
result = self.store.register_buffer(large_buffer_ptr, total_buffer_size)
self.assertEqual(result, 0)
# Prepare individual buffers
buffer_ptrs = []
buffer_sizes = []
for i, data in enumerate(test_data):
offset = i * buffer_spacing
buffer_ptr = large_buffer_ptr + offset
# Copy test data to buffer
ctypes.memmove(ctypes.c_void_p(buffer_ptr), data, len(data))
buffer_ptrs.append(buffer_ptr)
buffer_sizes.append(len(data))
# Test with default config (backward compatibility)
results = self.store.batch_put_from(keys=keys, buffer_ptrs=buffer_ptrs, sizes=buffer_sizes)
self.assertEqual(len(results), len(keys))
for result in results:
self.assertEqual(result, 0)
# Verify data
for key, expected_data in zip(keys, test_data):
retrieved_data = self.store.get(key)
self.assertEqual(retrieved_data, expected_data)
# Clean up
time.sleep(default_kv_lease_ttl / 1000)
for key in keys:
self.assertEqual(self.store.remove(key), 0)
# Test with custom config
config = ReplicateConfig()
config.replica_num = 2
config.with_soft_pin = False
keys2 = ["test_batch_put_from_config_key4", "test_batch_put_from_config_key5", "test_batch_put_from_config_key6"]
results = self.store.batch_put_from(keys=keys2, buffer_ptrs=buffer_ptrs, sizes=buffer_sizes, config=config)
self.assertEqual(len(results), len(keys2))
for result in results:
self.assertEqual(result, 0)
# Verify data
for key, expected_data in zip(keys2, test_data):
retrieved_data = self.store.get(key)
self.assertEqual(retrieved_data, expected_data)
# Clean up
time.sleep(default_kv_lease_ttl / 1000)
self.assertEqual(self.store.unregister_buffer(large_buffer_ptr), 0)
for key in keys2:
self.assertEqual(self.store.remove(key), 0)
def test_batch_get_buffer_operations(self):
"""Test batch_get_buffer operations for multiple keys."""
# Test data
@ -839,5 +634,6 @@ class TestDistributedObjectStore(unittest.TestCase):
for key in keys:
self.assertEqual(self.store.remove(key), 0)
if __name__ == '__main__':
unittest.main()

View File

@ -0,0 +1,306 @@
import unittest
import os
import time
from mooncake.store import MooncakeDistributedStore, ReplicateConfig
# The lease time of the kv object, should be set equal to
# the master's value.
DEFAULT_DEFAULT_KV_LEASE_TTL = 5000 # 5000 milliseconds
# Use environment variable if set, otherwise use default
default_kv_lease_ttl = int(os.getenv("DEFAULT_KV_LEASE_TTL", DEFAULT_DEFAULT_KV_LEASE_TTL))
def get_clients(stores, local_buffer_size_param=None):
"""Initialize and setup the distributed store clients."""
protocol = os.getenv("PROTOCOL", "tcp")
device_name = os.getenv("DEVICE_NAME", "ibp6s0")
base_hostname = os.getenv("LOCAL_HOSTNAME", "localhost")
metadata_server = os.getenv("MC_METADATA_SERVER", "http://127.0.0.1:8080/metadata")
segment_size = 1600 * 1024 * 1024 # 1600 MB per segment
local_buffer_size = (
local_buffer_size_param if local_buffer_size_param is not None
else 512 * 1024 * 1024 # 512 MB
)
master_server_address = os.getenv("MASTER_SERVER", "127.0.0.1:50051")
base_port = 12345
for i, store in enumerate(stores):
hostname = f"{base_hostname}:{base_port + i}"
retcode = store.setup(
hostname,
metadata_server,
segment_size,
local_buffer_size,
protocol,
device_name,
master_server_address
)
if retcode:
raise RuntimeError(f"Failed to setup segment. Return code: {retcode}")
def get_client(store, local_buffer_size_param=None):
"""Initialize and setup the distributed store client."""
return get_clients([store], local_buffer_size_param)
class TestDistributedObjectStoreReplication(unittest.TestCase):
"""Test class for replication operations (multiple stores)."""
@classmethod
def setUpClass(cls):
"""Initialize the main store and additional stores for replication."""
cls.store = MooncakeDistributedStore()
# Additional stores for replication testing
cls.additional_stores = []
cls.max_replicate_num = 2
for _ in range(cls.max_replicate_num - 1): # -1 because main store is already created
cls.additional_stores.append(MooncakeDistributedStore())
get_clients([cls.store] + cls.additional_stores)
def test_put_with_config_parameter(self):
"""Test put method with config parameter."""
test_data = b"Hello, Config World!"
key = "test_put_config_key"
# Test with default config (backward compatibility)
result = self.store.put(key=key, value=test_data)
self.assertEqual(result, 0)
# Verify data
retrieved_data = self.store.get(key)
self.assertEqual(retrieved_data, test_data)
# Clean up
time.sleep(default_kv_lease_ttl / 1000)
self.assertEqual(self.store.remove(key), 0)
# Test with custom config
config = ReplicateConfig()
config.replica_num = self.max_replicate_num
key2 = "test_put_config_key2"
result = self.store.put(key=key2, value=test_data, config=config)
self.assertEqual(result, 0)
# Verify data
retrieved_data = self.store.get(key2)
self.assertEqual(retrieved_data, test_data)
# Clean up
time.sleep(default_kv_lease_ttl / 1000)
self.assertEqual(self.store.remove(key2), 0)
with self.assertRaises(TypeError):
result = self.store.put(key_arg_name_error=key, value=test_data, config=config)
with self.assertRaises(TypeError):
result = self.store.put(key=key, value_arg_name_error=test_data, config=config)
with self.assertRaises(TypeError):
result = self.store.put(key=key, value=test_data, config_arg_name_error=config)
def test_put_batch_with_config_parameter(self):
"""Test put_batch method with config parameter."""
keys = ["test_batch_config_key1", "test_batch_config_key2", "test_batch_config_key3"]
values = [b"Batch Data 1", b"Batch Data 2", b"Batch Data 3"]
# Test with default config (backward compatibility)
result = self.store.put_batch(keys, values)
self.assertEqual(result, 0)
# Verify data
for key, expected_value in zip(keys, values):
retrieved_data = self.store.get(key)
self.assertEqual(retrieved_data, expected_value)
# Clean up
time.sleep(default_kv_lease_ttl / 1000)
for key in keys:
self.assertEqual(self.store.remove(key), 0)
# Test with custom config
config = ReplicateConfig()
config.replica_num = self.max_replicate_num
keys2 = ["test_batch_config_key4", "test_batch_config_key5", "test_batch_config_key6"]
result = self.store.put_batch(keys=keys2, values=values, config=config)
self.assertEqual(result, 0)
# Verify data
for key, expected_value in zip(keys2, values):
retrieved_data = self.store.get(key)
self.assertEqual(retrieved_data, expected_value)
# Clean up
time.sleep(default_kv_lease_ttl / 1000)
for key in keys2:
self.assertEqual(self.store.remove(key), 0)
def test_put_from_with_config_parameter(self):
"""Test put_from method with config parameter."""
import ctypes
test_data = b"Hello, put_from config world!"
key = "test_put_from_config_key"
buffer_size = len(test_data)
# Allocate and register buffer
buffer = (ctypes.c_ubyte * buffer_size)()
buffer_ptr = ctypes.addressof(buffer)
result = self.store.register_buffer(buffer_ptr, buffer_size)
self.assertEqual(result, 0)
# Copy test data to buffer
ctypes.memmove(buffer, test_data, len(test_data))
# Test with default config (backward compatibility)
result = self.store.put_from(key=key, buffer_ptr=buffer_ptr, size=len(test_data))
self.assertEqual(result, 0)
# Verify data
retrieved_data = self.store.get(key)
self.assertEqual(retrieved_data, test_data)
# Clean up
time.sleep(default_kv_lease_ttl / 1000)
self.assertEqual(self.store.remove(key), 0)
# Test with custom config
config = ReplicateConfig()
config.replica_num = self.max_replicate_num
config.with_soft_pin = False
key2 = "test_put_from_config_key2"
result = self.store.put_from(key=key2, buffer_ptr=buffer_ptr, size=len(test_data), config=config)
self.assertEqual(result, 0)
# Verify data
retrieved_data = self.store.get(key2)
self.assertEqual(retrieved_data, test_data)
# Clean up
time.sleep(default_kv_lease_ttl / 1000)
self.assertEqual(self.store.unregister_buffer(buffer_ptr), 0)
self.assertEqual(self.store.remove(key2), 0)
def test_batch_put_from_with_config_parameter(self):
"""Test batch_put_from method with config parameter."""
import ctypes
# Test data
test_data = [
b"Batch Config Data 1",
b"Batch Config Data 2",
b"Batch Config Data 3"
]
keys = ["test_batch_put_from_config_key1", "test_batch_put_from_config_key2", "test_batch_put_from_config_key3"]
# Use large spacing between buffers
buffer_spacing = 1024 * 1024 # 1MB spacing
total_buffer_size = buffer_spacing * len(test_data)
large_buffer = (ctypes.c_ubyte * total_buffer_size)()
large_buffer_ptr = ctypes.addressof(large_buffer)
# Register buffer
result = self.store.register_buffer(large_buffer_ptr, total_buffer_size)
self.assertEqual(result, 0)
# Prepare individual buffers
buffer_ptrs = []
buffer_sizes = []
for i, data in enumerate(test_data):
offset = i * buffer_spacing
buffer_ptr = large_buffer_ptr + offset
# Copy test data to buffer
ctypes.memmove(ctypes.c_void_p(buffer_ptr), data, len(data))
buffer_ptrs.append(buffer_ptr)
buffer_sizes.append(len(data))
# Test with default config (backward compatibility)
results = self.store.batch_put_from(keys=keys, buffer_ptrs=buffer_ptrs, sizes=buffer_sizes)
self.assertEqual(len(results), len(keys))
for result in results:
self.assertEqual(result, 0)
# Verify data
for key, expected_data in zip(keys, test_data):
retrieved_data = self.store.get(key)
self.assertEqual(retrieved_data, expected_data)
# Clean up
time.sleep(default_kv_lease_ttl / 1000)
for key in keys:
self.assertEqual(self.store.remove(key), 0)
# Test with custom config
config = ReplicateConfig()
config.replica_num = self.max_replicate_num
config.with_soft_pin = False
keys2 = ["test_batch_put_from_config_key4", "test_batch_put_from_config_key5", "test_batch_put_from_config_key6"]
results = self.store.batch_put_from(keys=keys2, buffer_ptrs=buffer_ptrs, sizes=buffer_sizes, config=config)
self.assertEqual(len(results), len(keys2))
for result in results:
self.assertEqual(result, 0)
# Verify data
for key, expected_data in zip(keys2, test_data):
retrieved_data = self.store.get(key)
self.assertEqual(retrieved_data, expected_data)
# Clean up
time.sleep(default_kv_lease_ttl / 1000)
self.assertEqual(self.store.unregister_buffer(large_buffer_ptr), 0)
for key in keys2:
self.assertEqual(self.store.remove(key), 0)
def test_replication_failure_tolerance(self):
"""Test that replicated data remains accessible after main store failure and reinit."""
test_data = b"Replicated failure tolerance test data!"
key = "test_replication_failure_key"
# Create config with replication
config = ReplicateConfig()
config.replica_num = self.max_replicate_num
# Put data with replication
result = self.store.put(key=key, value=test_data, config=config)
self.assertEqual(result, 0, "Put with replication should succeed")
# Verify data is initially accessible
retrieved_data = self.store.get(key)
self.assertEqual(retrieved_data, test_data, "Data should be accessible after put")
# Teardown the main store (simulate failure)
result = self.store.close()
self.assertEqual(result, 0, "Store teardown should succeed")
time.sleep(1) # Allow time for teardown to complete
# Verify data is still accessible from replica stores
# Since main store is down, read from one of the additional stores
replica_store = self.additional_stores[0] # Use first replica
retrieved_data = replica_store.get(key)
self.assertEqual(retrieved_data, test_data, "Data should remain accessible from replica after main store teardown")
# Reinitialize the main store
get_client(self.store)
# Verify data is still accessible after main store reinit
retrieved_data = self.store.get(key)
self.assertEqual(retrieved_data, test_data, "Data should remain accessible after main store reinit")
# Clean up
time.sleep(default_kv_lease_ttl / 1000)
self.assertEqual(self.store.remove(key), 0)
if __name__ == '__main__':
unittest.main()

View File

@ -28,6 +28,7 @@ mooncake_master --default_kv_lease_ttl=500 &
MASTER_PID=$!
sleep 1
MC_METADATA_SERVER=http://127.0.0.1:8080/metadata DEFAULT_KV_LEASE_TTL=500 python test_distributed_object_store.py
MC_METADATA_SERVER=http://127.0.0.1:8080/metadata DEFAULT_KV_LEASE_TTL=500 python test_replicated_distributed_object_store.py
sleep 1
pip install torch numpy