Mooncake/mooncake-store/tests/master_service_test.cpp

6567 lines
264 KiB
C++

#include "master_service.h"
#include "rpc_service.h"
#include <glog/logging.h>
#include <gtest/gtest.h>
#include <ylt/struct_json/json_reader.h>
#include <algorithm>
#include <atomic>
#include <chrono>
#include <functional>
#include <memory>
#include <random>
#include <thread>
#include <vector>
#include <unordered_set>
#include <utility>
#include "types.h"
namespace mooncake::test {
class MasterServiceTest : public ::testing::Test {
protected:
void SetUp() override {
google::InitGoogleLogging("MasterServiceTest");
FLAGS_logtostderr = true;
}
struct MountedSegmentContext {
UUID segment_id;
UUID client_id;
};
static constexpr size_t kDefaultSegmentBase = 0x300000000;
static constexpr size_t kDefaultSegmentSize = 1024 * 1024 * 16;
Segment MakeSegment(std::string name = "test_segment",
size_t base = kDefaultSegmentBase,
size_t size = kDefaultSegmentSize) const {
Segment segment;
segment.id = generate_uuid();
segment.name = std::move(name);
segment.base = base;
segment.size = size;
segment.te_endpoint = segment.name;
return segment;
}
#ifdef USE_NOF
NoFSegment MakeNoFSegment(
std::string name = "test_nof_segment",
std::string endpoint = "test_nof_segment_endpoint",
size_t base = kDefaultSegmentBase + kDefaultSegmentSize,
size_t size = kDefaultSegmentSize) const {
NoFSegment segment;
segment.id = generate_uuid();
segment.name = std::move(name);
segment.base = base;
segment.size = size;
segment.te_endpoint = std::move(endpoint);
return segment;
}
#endif
MountedSegmentContext PrepareSimpleSegment(
MasterService& service, std::string name = "test_segment",
size_t base = kDefaultSegmentBase,
size_t size = kDefaultSegmentSize) const {
Segment segment = MakeSegment(std::move(name), base, size);
UUID client_id = generate_uuid();
auto mount_result = service.MountSegment(segment, client_id);
EXPECT_TRUE(mount_result.has_value());
return {.segment_id = segment.id, .client_id = client_id};
}
std::string PutObjectOnSegment(MasterService& service,
const UUID& client_id,
const std::string& segment_name,
size_t slice_length = 1024) const {
static std::atomic<uint64_t> counter{0};
std::string key =
"drain_job_key_" + std::to_string(counter.fetch_add(1));
ReplicateConfig config;
config.replica_num = 1;
config.preferred_segment = segment_name;
auto put_start = service.PutStart(client_id, key, slice_length, config);
EXPECT_TRUE(put_start.has_value());
EXPECT_TRUE(
service.PutEnd(client_id, key, ReplicaType::MEMORY).has_value());
return key;
}
std::string FindGroupIdOnDifferentShard(const std::string& key) const {
static constexpr size_t kMetadataShardCountForTest = 1024;
const size_t key_shard =
std::hash<std::string>{}(key) % kMetadataShardCountForTest;
for (int i = 0; i < 10000; ++i) {
std::string group_id = key + "_group_" + std::to_string(i);
if (std::hash<std::string>{}(group_id) %
kMetadataShardCountForTest !=
key_shard) {
return group_id;
}
}
return key + "_fallback_group";
}
void PutCompletedObject(MasterService& service, const UUID& client_id,
const std::string& key,
const ReplicateConfig& config,
uint64_t slice_length = 1024) const {
auto put_start = service.PutStart(client_id, key, slice_length, config);
ASSERT_TRUE(put_start.has_value())
<< "PutStart failed for key=" << key
<< ", error=" << toString(put_start.error());
ASSERT_TRUE(
service.PutEnd(client_id, key, ReplicaType::MEMORY).has_value());
}
bool ExecutePendingMoveTasks(MasterService& service,
const UUID& client_id) const {
auto fetched = service.FetchTasks(client_id, /*batch_size=*/16);
EXPECT_TRUE(fetched.has_value());
if (!fetched.has_value() || fetched->empty()) {
return false;
}
bool processed = false;
for (const auto& assignment : *fetched) {
if (assignment.type != TaskType::REPLICA_MOVE) {
continue;
}
ReplicaMovePayload payload;
struct_json::from_json(payload, assignment.payload);
auto move_start = service.MoveStart(client_id, payload.key,
payload.source, payload.target);
EXPECT_TRUE(move_start.has_value());
EXPECT_TRUE(service.MoveEnd(client_id, payload.key).has_value());
TaskCompleteRequest complete_request;
complete_request.id = assignment.id;
complete_request.status = TaskStatus::SUCCESS;
complete_request.message = "move_done";
EXPECT_TRUE(service.MarkTaskToComplete(client_id, complete_request)
.has_value());
processed = true;
}
return processed;
}
bool FailPendingMoveTasks(MasterService& service,
const UUID& client_id) const {
auto fetched = service.FetchTasks(client_id, /*batch_size=*/16);
EXPECT_TRUE(fetched.has_value());
if (!fetched.has_value() || fetched->empty()) {
return false;
}
bool processed = false;
for (const auto& assignment : *fetched) {
if (assignment.type != TaskType::REPLICA_MOVE) {
continue;
}
TaskCompleteRequest complete_request;
complete_request.id = assignment.id;
complete_request.status = TaskStatus::FAILED;
complete_request.message = "move_failed";
EXPECT_TRUE(service.MarkTaskToComplete(client_id, complete_request)
.has_value());
processed = true;
}
return processed;
}
template <typename Predicate>
void WaitUntil(
Predicate&& predicate,
std::chrono::milliseconds timeout = std::chrono::milliseconds(4000),
std::chrono::milliseconds interval =
std::chrono::milliseconds(50)) const {
const auto deadline = std::chrono::steady_clock::now() + timeout;
while (std::chrono::steady_clock::now() < deadline) {
if (predicate()) {
return;
}
std::this_thread::sleep_for(interval);
}
EXPECT_TRUE(predicate());
}
std::vector<Replica::Descriptor> replica_list;
void TearDown() override { google::ShutdownGoogleLogging(); }
};
std::string GenerateKeyForSegment(const UUID& client_id,
const std::unique_ptr<MasterService>& service,
const std::string& segment_name) {
static std::atomic<uint64_t> counter(0);
while (true) {
std::string key = "key_" + std::to_string(counter.fetch_add(1));
std::vector<Replica::Descriptor> replica_list;
// Check if the key already exists.
auto exist_result = service->ExistKey(key);
if (exist_result.has_value() && exist_result.value()) {
continue; // Retry if the key already exists
}
// Attempt to put the key.
auto put_result =
service->PutStart(client_id, key, {1024}, {.replica_num = 1});
if (put_result.has_value()) {
replica_list = std::move(put_result.value());
}
ErrorCode code =
put_result.has_value() ? ErrorCode::OK : put_result.error();
if (code == ErrorCode::OBJECT_ALREADY_EXISTS) {
continue; // Retry if the key already exists
}
if (code != ErrorCode::OK) {
throw std::runtime_error("PutStart failed with code: " +
std::to_string(static_cast<int>(code)));
}
auto put_end_result =
service->PutEnd(client_id, key, ReplicaType::MEMORY);
if (!put_end_result.has_value()) {
throw std::runtime_error("PutEnd failed");
}
if (replica_list[0]
.get_memory_descriptor()
.buffer_descriptor.transport_endpoint_ == segment_name) {
return key;
}
// Clean up failed attempt
auto remove_result = service->Remove(key);
if (!remove_result.has_value()) {
// Ignore cleanup failure
}
}
}
TEST_F(MasterServiceTest, MountUnmountSegmentWithCachelibAllocator) {
// Create a MasterService instance for testing.
auto service_config =
MasterServiceConfig::builder()
.set_memory_allocator(BufferAllocatorType::CACHELIB)
.build();
std::unique_ptr<MasterService> service_(new MasterService(service_config));
auto segment = MakeSegment();
UUID client_id = generate_uuid();
const auto original_base = segment.base;
const auto original_size = segment.size;
// Test invalid parameters.
// Invalid buffer address (0).
segment.base = 0;
segment.size = original_size;
auto mount_result1 = service_->MountSegment(segment, client_id);
EXPECT_FALSE(mount_result1.has_value());
EXPECT_EQ(ErrorCode::INVALID_PARAMS, mount_result1.error());
// Invalid segment size (0).
segment.base = original_base;
segment.size = 0;
auto mount_result2 = service_->MountSegment(segment, client_id);
EXPECT_FALSE(mount_result2.has_value());
EXPECT_EQ(ErrorCode::INVALID_PARAMS, mount_result2.error());
// Base is not aligned
segment.base = original_base + 1;
segment.size = original_size;
auto mount_result3 = service_->MountSegment(segment, client_id);
EXPECT_FALSE(mount_result3.has_value());
EXPECT_EQ(ErrorCode::INVALID_PARAMS, mount_result3.error());
// Size is not aligned
segment.base = original_base;
segment.size = original_size + 1;
auto mount_result4 = service_->MountSegment(segment, client_id);
EXPECT_FALSE(mount_result4.has_value());
EXPECT_EQ(ErrorCode::INVALID_PARAMS, mount_result4.error());
// Test normal mount operation.
segment.base = original_base;
segment.size = original_size;
auto mount_result5 = service_->MountSegment(segment, client_id);
EXPECT_TRUE(mount_result5.has_value());
// Test mounting the same segment again (idempotent request should succeed).
auto mount_result6 = service_->MountSegment(segment, client_id);
EXPECT_TRUE(mount_result6.has_value());
// Test unmounting the segment.
auto unmount_result1 = service_->UnmountSegment(segment.id, client_id);
EXPECT_TRUE(unmount_result1.has_value());
// Test unmounting the same segment again (idempotent request should
// succeed).
auto unmount_result2 = service_->UnmountSegment(segment.id, client_id);
EXPECT_TRUE(unmount_result2.has_value());
// Test unmounting a non-existent segment (idempotent request should
// succeed).
UUID non_existent_id = generate_uuid();
auto unmount_result3 = service_->UnmountSegment(non_existent_id, client_id);
EXPECT_TRUE(unmount_result3.has_value());
// Test remounting after unmount.
auto mount_result7 = service_->MountSegment(segment, client_id);
EXPECT_TRUE(mount_result7.has_value());
auto unmount_result4 = service_->UnmountSegment(segment.id, client_id);
EXPECT_TRUE(unmount_result4.has_value());
}
TEST_F(MasterServiceTest, MountUnmountSegmentWithOffsetAllocator) {
// Create a MasterService instance for testing.
auto service_config = MasterServiceConfig::builder()
.set_memory_allocator(BufferAllocatorType::OFFSET)
.build();
std::unique_ptr<MasterService> service_(new MasterService(service_config));
auto segment = MakeSegment();
UUID client_id = generate_uuid();
const auto original_base = segment.base;
const auto original_size = segment.size;
// Test invalid parameters.
// Invalid buffer address (0).
segment.base = 0;
segment.size = original_size;
auto mount_result1 = service_->MountSegment(segment, client_id);
EXPECT_FALSE(mount_result1.has_value());
EXPECT_EQ(ErrorCode::INVALID_PARAMS, mount_result1.error());
// Invalid segment size (0).
segment.base = original_base;
segment.size = 0;
auto mount_result2 = service_->MountSegment(segment, client_id);
EXPECT_FALSE(mount_result2.has_value());
EXPECT_EQ(ErrorCode::INVALID_PARAMS, mount_result2.error());
// Test normal mount operation.
segment.base = original_base;
segment.size = original_size;
auto mount_result5 = service_->MountSegment(segment, client_id);
EXPECT_TRUE(mount_result5.has_value());
// Test mounting the same segment again (idempotent request should succeed).
auto mount_result6 = service_->MountSegment(segment, client_id);
EXPECT_TRUE(mount_result6.has_value());
// Test unmounting the segment.
auto unmount_result1 = service_->UnmountSegment(segment.id, client_id);
EXPECT_TRUE(unmount_result1.has_value());
// Test unmounting the same segment again (idempotent request should
// succeed).
auto unmount_result2 = service_->UnmountSegment(segment.id, client_id);
EXPECT_TRUE(unmount_result2.has_value());
// Test unmounting a non-existent segment (idempotent request should
// succeed).
UUID non_existent_id = generate_uuid();
auto unmount_result3 = service_->UnmountSegment(non_existent_id, client_id);
EXPECT_TRUE(unmount_result3.has_value());
// Test remounting after unmount.
auto mount_result7 = service_->MountSegment(segment, client_id);
EXPECT_TRUE(mount_result7.has_value());
auto unmount_result4 = service_->UnmountSegment(segment.id, client_id);
EXPECT_TRUE(unmount_result4.has_value());
}
TEST_F(MasterServiceTest, RandomMountUnmountSegment) {
// Create a MasterService instance for testing.
std::unique_ptr<MasterService> service_(new MasterService());
// Define a constant buffer address for the segment.
constexpr size_t kBufferAddress = 0x300000000;
// Define the name of the test segment.
std::string segment_name = "test_random_segment";
UUID segment_id = generate_uuid();
UUID client_id = generate_uuid();
size_t times = 10;
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<> dis(1, 10);
while (times--) {
int random_number = dis(gen);
// Define the size of the segment (16MB).
size_t kSegmentSize = 1024 * 1024 * 16 * random_number;
auto segment = MakeSegment(segment_name, kBufferAddress, kSegmentSize);
segment.id = segment_id;
// Test remounting after unmount.
auto mount_result = service_->MountSegment(segment, client_id);
EXPECT_TRUE(mount_result.has_value());
auto unmount_result = service_->UnmountSegment(segment.id, client_id);
EXPECT_TRUE(unmount_result.has_value());
}
}
TEST_F(MasterServiceTest, ConcurrentMountUnmount) {
std::unique_ptr<MasterService> service_(new MasterService());
constexpr size_t num_threads = 4;
constexpr size_t iterations = 100;
std::vector<std::thread> threads;
std::atomic<int> success_count{0};
// Launch multiple threads to mount/unmount segments concurrently
for (size_t i = 0; i < num_threads; i++) {
threads.emplace_back([&service_, i, &success_count, this]() {
auto segment =
MakeSegment("segment_" + std::to_string(i),
0x300000000 + i * 0x10000000, 16 * 1024 * 1024);
UUID client_id = generate_uuid();
for (size_t j = 0; j < iterations; j++) {
auto mount_result = service_->MountSegment(segment, client_id);
if (mount_result.has_value()) {
auto unmount_result =
service_->UnmountSegment(segment.id, client_id);
EXPECT_TRUE(unmount_result.has_value());
success_count++;
}
}
});
}
// Wait for all threads to complete
for (auto& thread : threads) {
thread.join();
}
// Verify that some mount/unmount operations succeeded
EXPECT_GT(success_count, 0);
}
TEST_F(MasterServiceTest, PutStartInvalidParams) {
std::unique_ptr<MasterService> service_(new MasterService());
[[maybe_unused]] const auto context = PrepareSimpleSegment(*service_);
const UUID client_id = generate_uuid();
std::string key = "test_key";
ReplicateConfig config;
// Test invalid replica config
config.replica_num = 0;
config.nof_replica_num = 0;
auto put_result1 = service_->PutStart(client_id, key, 1024, config);
EXPECT_FALSE(put_result1.has_value());
EXPECT_EQ(ErrorCode::INVALID_PARAMS, put_result1.error());
// Test zero slice_length
config.replica_num = 1;
auto put_result2 = service_->PutStart(client_id, key, 0, config);
EXPECT_FALSE(put_result2.has_value());
EXPECT_EQ(ErrorCode::INVALID_PARAMS, put_result2.error());
// Test prefer_alloc_in_same_node with nof replicas
config.nof_replica_num = 1;
config.prefer_alloc_in_same_node = true;
auto put_result3 = service_->PutStart(client_id, key, 1024, config);
EXPECT_FALSE(put_result3.has_value());
EXPECT_EQ(ErrorCode::INVALID_PARAMS, put_result3.error());
}
#ifdef USE_NOF
TEST_F(MasterServiceTest, PutEndAllCompletesMemoryAndNoFReplicas) {
std::unique_ptr<MasterService> service_(new MasterService());
[[maybe_unused]] const auto mem_context = PrepareSimpleSegment(*service_);
NoFSegment nof_segment = MakeNoFSegment();
const UUID client_id = generate_uuid();
ASSERT_TRUE(service_->MountNoFSegment(nof_segment, client_id).has_value());
ReplicateConfig config;
config.replica_num = 1;
config.nof_replica_num = 1;
auto put_start_result =
service_->PutStart(client_id, "test_key_all", 1024, config);
ASSERT_TRUE(put_start_result.has_value());
auto put_end_result =
service_->PutEnd(client_id, "test_key_all", ReplicaType::ALL);
ASSERT_TRUE(put_end_result.has_value());
auto get_replica_result = service_->GetReplicaList("test_key_all");
ASSERT_TRUE(get_replica_result.has_value());
bool has_complete_memory = false;
bool has_complete_nof = false;
for (const auto& replica : get_replica_result->replicas) {
if (replica.is_memory_replica() &&
replica.status == ReplicaStatus::COMPLETE) {
has_complete_memory = true;
}
if (replica.is_nof_replica() &&
replica.status == ReplicaStatus::COMPLETE) {
has_complete_nof = true;
}
}
EXPECT_TRUE(has_complete_memory);
EXPECT_TRUE(has_complete_nof);
}
TEST_F(MasterServiceTest, PutEndMemoryDoesNotCompleteNoFReplica) {
std::unique_ptr<MasterService> service_(new MasterService());
[[maybe_unused]] const auto mem_context = PrepareSimpleSegment(*service_);
NoFSegment nof_segment =
MakeNoFSegment("test_nof_segment_2", "test_nof_segment_endpoint_2");
const UUID client_id = generate_uuid();
ASSERT_TRUE(service_->MountNoFSegment(nof_segment, client_id).has_value());
ReplicateConfig config;
config.replica_num = 1;
config.nof_replica_num = 1;
auto put_start_result =
service_->PutStart(client_id, "test_key_split", 1024, config);
ASSERT_TRUE(put_start_result.has_value());
auto put_end_result =
service_->PutEnd(client_id, "test_key_split", ReplicaType::MEMORY);
ASSERT_TRUE(put_end_result.has_value());
auto get_replica_result = service_->GetReplicaList("test_key_split");
ASSERT_TRUE(get_replica_result.has_value());
ASSERT_EQ(get_replica_result->replicas.size(), 1u);
EXPECT_TRUE(get_replica_result->replicas[0].is_memory_replica());
EXPECT_EQ(get_replica_result->replicas[0].status, ReplicaStatus::COMPLETE);
auto put_revoke_result =
service_->PutRevoke(client_id, "test_key_split", ReplicaType::NOF_SSD);
ASSERT_TRUE(put_revoke_result.has_value());
auto final_replica_result = service_->GetReplicaList("test_key_split");
ASSERT_TRUE(final_replica_result.has_value());
ASSERT_EQ(final_replica_result->replicas.size(), 1u);
EXPECT_TRUE(final_replica_result->replicas[0].is_memory_replica());
EXPECT_EQ(final_replica_result->replicas[0].status,
ReplicaStatus::COMPLETE);
}
TEST_F(MasterServiceTest, PutStartOnePlusOneAllowsSingleAllocatedReplica) {
std::unique_ptr<MasterService> service_(new MasterService());
[[maybe_unused]] const auto mem_context = PrepareSimpleSegment(*service_);
const UUID client_id = generate_uuid();
ReplicateConfig config;
config.replica_num = 1;
config.nof_replica_num = 1;
auto put_start_result =
service_->PutStart(client_id, "test_key_one_plus_one", 1024, config);
ASSERT_TRUE(put_start_result.has_value());
ASSERT_EQ(put_start_result->size(), 1u);
EXPECT_TRUE(put_start_result->front().is_memory_replica());
}
#endif
TEST_F(MasterServiceTest, PutStartGroupIdsValidation) {
std::unique_ptr<MasterService> service_(new MasterService());
[[maybe_unused]] const auto context = PrepareSimpleSegment(*service_);
const UUID client_id = generate_uuid();
ReplicateConfig config;
config.replica_num = 1;
config.group_ids = std::vector<std::string>{};
auto empty_group_ids =
service_->PutStart(client_id, "empty_group_ids", 1024, config);
EXPECT_FALSE(empty_group_ids.has_value());
EXPECT_EQ(ErrorCode::INVALID_PARAMS, empty_group_ids.error());
config.group_ids = std::vector<std::string>{"g0", "g1"};
auto too_many_group_ids =
service_->PutStart(client_id, "too_many_group_ids", 1024, config);
EXPECT_FALSE(too_many_group_ids.has_value());
EXPECT_EQ(ErrorCode::INVALID_PARAMS, too_many_group_ids.error());
config.group_ids = std::vector<std::string>{""};
auto ungrouped =
service_->PutStart(client_id, "explicit_ungrouped", 1024, config);
ASSERT_TRUE(ungrouped.has_value());
ASSERT_TRUE(
service_->PutEnd(client_id, "explicit_ungrouped", ReplicaType::MEMORY)
.has_value());
auto exists = service_->ExistKey("explicit_ungrouped");
ASSERT_TRUE(exists.has_value());
EXPECT_TRUE(exists.value());
}
TEST_F(MasterServiceTest, GroupedObjectRoutesKeyLevelLookupAndRemove) {
std::unique_ptr<MasterService> service_(new MasterService());
[[maybe_unused]] const auto context = PrepareSimpleSegment(*service_);
const UUID client_id = generate_uuid();
const std::string key = "grouped_route_key";
ReplicateConfig config;
config.replica_num = 1;
config.group_ids =
std::vector<std::string>{FindGroupIdOnDifferentShard(key)};
PutCompletedObject(*service_, client_id, key, config);
auto exists = service_->ExistKey(key);
ASSERT_TRUE(exists.has_value());
EXPECT_TRUE(exists.value());
EXPECT_TRUE(service_->GetReplicaList(key).has_value());
ASSERT_TRUE(service_->Remove(key, /*force=*/true).has_value());
auto exists_after_remove = service_->ExistKey(key);
ASSERT_TRUE(exists_after_remove.has_value());
EXPECT_FALSE(exists_after_remove.value());
}
TEST_F(MasterServiceTest, GroupRoutingIsTenantScopedForSameUserKey) {
std::unique_ptr<MasterService> service_(new MasterService());
[[maybe_unused]] const auto context = PrepareSimpleSegment(*service_);
const UUID client_id = generate_uuid();
const std::string key = "tenant_grouped_shared_user_key";
const std::string tenant_a = "tenant_group_route_a";
const std::string tenant_b = "tenant_group_route_b";
const std::string group_a = FindGroupIdOnDifferentShard(key);
std::string group_b;
for (int i = 0; i < 10000; ++i) {
group_b = key + "_tenant_b_group_" + std::to_string(i);
if (std::hash<std::string>{}(group_b) % 1024 !=
std::hash<std::string>{}(group_a) % 1024) {
break;
}
}
ReplicateConfig config_a;
config_a.replica_num = 1;
config_a.group_ids = std::vector<std::string>{group_a};
ReplicateConfig config_b;
config_b.replica_num = 1;
config_b.group_ids = std::vector<std::string>{group_b};
ASSERT_TRUE(service_->PutStart(client_id, key, tenant_a, 1024, config_a)
.has_value());
ASSERT_TRUE(service_->PutEnd(client_id, key, tenant_a, ReplicaType::MEMORY)
.has_value());
ASSERT_TRUE(service_->PutStart(client_id, key, tenant_b, 2048, config_b)
.has_value());
ASSERT_TRUE(service_->PutEnd(client_id, key, tenant_b, ReplicaType::MEMORY)
.has_value());
EXPECT_TRUE(service_->ExistKey(key, tenant_a).value_or(false));
EXPECT_TRUE(service_->ExistKey(key, tenant_b).value_or(false));
EXPECT_TRUE(service_->GetReplicaList(key, tenant_a).has_value());
EXPECT_TRUE(service_->GetReplicaList(key, tenant_b).has_value());
ASSERT_TRUE(service_->Remove(key, tenant_a, /*force=*/true).has_value());
EXPECT_FALSE(service_->GetReplicaList(key, tenant_a).has_value());
EXPECT_TRUE(service_->GetReplicaList(key, tenant_b).has_value());
}
TEST_F(MasterServiceTest,
ConcurrentGroupedAndUngroupedFirstCreateDoesNotDuplicateMetadata) {
std::unique_ptr<MasterService> service_(new MasterService());
[[maybe_unused]] const auto context = PrepareSimpleSegment(*service_);
const UUID client_id = generate_uuid();
const std::string key = "concurrent_grouped_ungrouped_first_create";
const std::string tenant_id = "tenant_concurrent_first_create";
ReplicateConfig ungrouped_config;
ungrouped_config.replica_num = 1;
ReplicateConfig grouped_config;
grouped_config.replica_num = 1;
grouped_config.group_ids =
std::vector<std::string>{FindGroupIdOnDifferentShard(key)};
static constexpr size_t kThreadCount = 16;
std::atomic<size_t> ready{0};
std::atomic<bool> start{false};
std::vector<int> put_start_success(kThreadCount, 0);
std::vector<int> put_end_success(kThreadCount, 0);
std::vector<std::thread> threads;
threads.reserve(kThreadCount);
for (size_t i = 0; i < kThreadCount; ++i) {
threads.emplace_back([&, i]() {
ready.fetch_add(1, std::memory_order_acq_rel);
while (!start.load(std::memory_order_acquire)) {
std::this_thread::yield();
}
const auto& config =
(i % 2 == 0) ? grouped_config : ungrouped_config;
auto put_start =
service_->PutStart(client_id, key, tenant_id, 1024, config);
put_start_success[i] = put_start.has_value() ? 1 : 0;
if (put_start.has_value()) {
put_end_success[i] = service_->PutEnd(client_id, key, tenant_id,
ReplicaType::MEMORY)
.has_value()
? 1
: 0;
} else {
EXPECT_EQ(ErrorCode::OBJECT_ALREADY_EXISTS, put_start.error());
}
});
}
while (ready.load(std::memory_order_acquire) < kThreadCount) {
std::this_thread::yield();
}
start.store(true, std::memory_order_release);
for (auto& thread : threads) {
thread.join();
}
EXPECT_EQ(std::count(put_start_success.begin(), put_start_success.end(), 1),
1);
EXPECT_EQ(std::count(put_end_success.begin(), put_end_success.end(), 1), 1);
EXPECT_EQ(service_->GetKeyCount(), 1u);
EXPECT_TRUE(service_->GetReplicaList(key, tenant_id).has_value());
}
TEST_F(MasterServiceTest,
ConcurrentDifferentGroupedFirstCreateDoesNotDuplicateMetadata) {
std::unique_ptr<MasterService> service_(new MasterService());
[[maybe_unused]] const auto context = PrepareSimpleSegment(*service_);
const UUID client_id = generate_uuid();
const std::string key = "concurrent_different_grouped_first_create";
const std::string tenant_id = "tenant_concurrent_grouped_first_create";
const std::string group_a = FindGroupIdOnDifferentShard(key);
std::string group_b;
for (int i = 0; i < 10000; ++i) {
group_b = key + "_other_group_" + std::to_string(i);
if (std::hash<std::string>{}(group_b) % 1024 !=
std::hash<std::string>{}(group_a) % 1024) {
break;
}
}
ReplicateConfig config_a;
config_a.replica_num = 1;
config_a.group_ids = std::vector<std::string>{group_a};
ReplicateConfig config_b;
config_b.replica_num = 1;
config_b.group_ids = std::vector<std::string>{group_b};
static constexpr size_t kThreadCount = 16;
std::atomic<size_t> ready{0};
std::atomic<bool> start{false};
std::vector<int> put_start_success(kThreadCount, 0);
std::vector<int> put_end_success(kThreadCount, 0);
std::vector<std::thread> threads;
threads.reserve(kThreadCount);
for (size_t i = 0; i < kThreadCount; ++i) {
threads.emplace_back([&, i]() {
ready.fetch_add(1, std::memory_order_acq_rel);
while (!start.load(std::memory_order_acquire)) {
std::this_thread::yield();
}
const auto& config = (i % 2 == 0) ? config_a : config_b;
auto put_start =
service_->PutStart(client_id, key, tenant_id, 1024, config);
put_start_success[i] = put_start.has_value() ? 1 : 0;
if (put_start.has_value()) {
put_end_success[i] = service_->PutEnd(client_id, key, tenant_id,
ReplicaType::MEMORY)
.has_value()
? 1
: 0;
} else {
EXPECT_EQ(ErrorCode::OBJECT_ALREADY_EXISTS, put_start.error());
}
});
}
while (ready.load(std::memory_order_acquire) < kThreadCount) {
std::this_thread::yield();
}
start.store(true, std::memory_order_release);
for (auto& thread : threads) {
thread.join();
}
EXPECT_EQ(std::count(put_start_success.begin(), put_start_success.end(), 1),
1);
EXPECT_EQ(std::count(put_end_success.begin(), put_end_success.end(), 1), 1);
EXPECT_EQ(service_->GetKeyCount(), 1u);
EXPECT_TRUE(service_->GetReplicaList(key, tenant_id).has_value());
}
TEST_F(MasterServiceTest, ExpiredGroupedPutCanBeReplacedByUngroupedPut) {
auto service_config = MasterServiceConfig::builder()
.set_put_start_discard_timeout_sec(0)
.set_put_start_release_timeout_sec(1)
.build();
std::unique_ptr<MasterService> service_(new MasterService(service_config));
const auto context = PrepareSimpleSegment(*service_);
const UUID client_id = context.client_id;
const std::string key = "expired_grouped_put_to_ungrouped";
ReplicateConfig grouped_config;
grouped_config.replica_num = 1;
grouped_config.group_ids =
std::vector<std::string>{FindGroupIdOnDifferentShard(key)};
ASSERT_TRUE(
service_->PutStart(client_id, key, 1024, grouped_config).has_value());
std::this_thread::sleep_for(std::chrono::milliseconds(2));
ReplicateConfig ungrouped_config;
ungrouped_config.replica_num = 1;
auto put_start = service_->PutStart(client_id, key, 1024, ungrouped_config);
ASSERT_TRUE(put_start.has_value()) << toString(put_start.error());
ASSERT_TRUE(
service_->PutEnd(client_id, key, ReplicaType::MEMORY).has_value());
EXPECT_TRUE(service_->ExistKey(key).value_or(false));
}
TEST_F(MasterServiceTest, BatchRemoveUnregistersGroupedRoute) {
std::unique_ptr<MasterService> service_(new MasterService());
[[maybe_unused]] const auto context = PrepareSimpleSegment(*service_);
const UUID client_id = generate_uuid();
const std::string key = "batch_remove_grouped_route";
ReplicateConfig grouped_config;
grouped_config.replica_num = 1;
grouped_config.group_ids =
std::vector<std::string>{FindGroupIdOnDifferentShard(key)};
PutCompletedObject(*service_, client_id, key, grouped_config);
auto remove_results =
service_->BatchRemove(std::vector<std::string>{key}, /*force=*/true);
ASSERT_EQ(remove_results.size(), 1u);
ASSERT_TRUE(remove_results[0].has_value());
ReplicateConfig ungrouped_config;
ungrouped_config.replica_num = 1;
PutCompletedObject(*service_, client_id, key, ungrouped_config);
EXPECT_TRUE(service_->GetReplicaList(key).has_value());
}
TEST_F(MasterServiceTest, RemoveByRegexUnregistersGroupedRoute) {
std::unique_ptr<MasterService> service_(new MasterService());
[[maybe_unused]] const auto context = PrepareSimpleSegment(*service_);
const UUID client_id = generate_uuid();
const std::string key = "regex_remove_grouped_route";
ReplicateConfig grouped_config;
grouped_config.replica_num = 1;
grouped_config.group_ids =
std::vector<std::string>{FindGroupIdOnDifferentShard(key)};
PutCompletedObject(*service_, client_id, key, grouped_config);
auto removed = service_->RemoveByRegex("^regex_remove_grouped_route$",
/*force=*/true);
ASSERT_TRUE(removed.has_value());
EXPECT_EQ(removed.value(), 1);
ReplicateConfig ungrouped_config;
ungrouped_config.replica_num = 1;
PutCompletedObject(*service_, client_id, key, ungrouped_config);
EXPECT_TRUE(service_->GetReplicaList(key).has_value());
}
TEST_F(MasterServiceTest, GroupedLeaseRefreshNearExpiryProtectsCurrentMembers) {
auto service_config =
MasterServiceConfig::builder().set_default_kv_lease_ttl(200).build();
std::unique_ptr<MasterService> service_(new MasterService(service_config));
[[maybe_unused]] const auto context = PrepareSimpleSegment(*service_);
const UUID client_id = generate_uuid();
const std::string key_a = "lease_group_key_a";
const std::string key_b = "lease_group_key_b";
const std::string group_id = FindGroupIdOnDifferentShard(key_a);
ReplicateConfig config_a;
config_a.replica_num = 1;
config_a.group_ids = std::vector<std::string>{group_id};
ReplicateConfig config_b = config_a;
PutCompletedObject(*service_, client_id, key_a, config_a);
PutCompletedObject(*service_, client_id, key_b, config_b);
auto exists = service_->ExistKey(key_a);
ASSERT_TRUE(exists.has_value());
ASSERT_TRUE(exists.value());
std::this_thread::sleep_for(std::chrono::milliseconds(120));
exists = service_->ExistKey(key_a);
ASSERT_TRUE(exists.has_value());
ASSERT_TRUE(exists.value());
std::this_thread::sleep_for(std::chrono::milliseconds(100));
auto remove_group_peer = service_->Remove(key_b);
ASSERT_FALSE(remove_group_peer.has_value());
EXPECT_EQ(ErrorCode::OBJECT_HAS_LEASE, remove_group_peer.error());
EXPECT_TRUE(service_->Remove(key_a, /*force=*/true).has_value());
EXPECT_TRUE(service_->Remove(key_b, /*force=*/true).has_value());
}
TEST_F(MasterServiceTest,
GroupedLeaseRefreshAfterMembershipChangeDoesNotWaitForTriggerExpiry) {
auto service_config =
MasterServiceConfig::builder().set_default_kv_lease_ttl(500).build();
std::unique_ptr<MasterService> service_(new MasterService(service_config));
[[maybe_unused]] const auto context = PrepareSimpleSegment(*service_);
const UUID client_id = generate_uuid();
const std::string key_a = "lease_group_dirty_key_a";
const std::string key_b = "lease_group_dirty_key_b";
const std::string group_id = FindGroupIdOnDifferentShard(key_a);
ReplicateConfig config;
config.replica_num = 1;
config.group_ids = std::vector<std::string>{group_id};
PutCompletedObject(*service_, client_id, key_a, config);
ASSERT_TRUE(service_->ExistKey(key_a).value_or(false));
PutCompletedObject(*service_, client_id, key_b, config);
std::this_thread::sleep_for(std::chrono::milliseconds(150));
auto exists = service_->ExistKey(key_a);
ASSERT_TRUE(exists.has_value());
ASSERT_TRUE(exists.value());
std::this_thread::sleep_for(std::chrono::milliseconds(390));
auto remove_group_peer = service_->Remove(key_b);
ASSERT_FALSE(remove_group_peer.has_value());
EXPECT_EQ(ErrorCode::OBJECT_HAS_LEASE, remove_group_peer.error());
EXPECT_TRUE(service_->Remove(key_a, /*force=*/true).has_value());
EXPECT_TRUE(service_->Remove(key_b, /*force=*/true).has_value());
}
TEST_F(MasterServiceTest, RemoveGroupedMemberPreservesOtherMembers) {
std::unique_ptr<MasterService> service_(new MasterService());
[[maybe_unused]] const auto context = PrepareSimpleSegment(*service_);
const UUID client_id = generate_uuid();
const std::string key_a = "remove_group_key_a";
const std::string key_b = "remove_group_key_b";
const std::string group_id = FindGroupIdOnDifferentShard(key_a);
ReplicateConfig config;
config.replica_num = 1;
config.group_ids = std::vector<std::string>{group_id};
PutCompletedObject(*service_, client_id, key_a, config);
PutCompletedObject(*service_, client_id, key_b, config);
ASSERT_TRUE(service_->Remove(key_a, /*force=*/true).has_value());
auto removed_exists = service_->ExistKey(key_a);
ASSERT_TRUE(removed_exists.has_value());
EXPECT_FALSE(removed_exists.value());
EXPECT_TRUE(service_->GetReplicaList(key_b).has_value());
ASSERT_TRUE(service_->Remove(key_b, /*force=*/true).has_value());
auto group_empty = service_->ExistKey(key_b);
ASSERT_TRUE(group_empty.has_value());
EXPECT_FALSE(group_empty.value());
}
TEST_F(MasterServiceTest, UpsertPreservesGroupMembership) {
std::unique_ptr<MasterService> service_(new MasterService());
[[maybe_unused]] const auto context = PrepareSimpleSegment(*service_);
const UUID client_id = generate_uuid();
const std::string key = "upsert_group_key";
const std::string group_id = FindGroupIdOnDifferentShard(key);
ReplicateConfig grouped_config;
grouped_config.replica_num = 1;
grouped_config.group_ids = std::vector<std::string>{group_id};
PutCompletedObject(*service_, client_id, key, grouped_config);
ReplicateConfig unset_group_config;
unset_group_config.replica_num = 1;
auto preserve_result =
service_->UpsertStart(client_id, key, 1024, unset_group_config);
ASSERT_TRUE(preserve_result.has_value())
<< "Unset group_ids should preserve existing group membership";
ASSERT_TRUE(
service_->UpsertEnd(client_id, key, ReplicaType::MEMORY).has_value());
EXPECT_TRUE(service_->GetReplicaList(key).has_value());
ReplicateConfig different_group_config;
different_group_config.replica_num = 1;
different_group_config.group_ids =
std::vector<std::string>{FindGroupIdOnDifferentShard(key + "_other")};
auto different_group_result =
service_->UpsertStart(client_id, key, 1024, different_group_config);
ASSERT_FALSE(different_group_result.has_value());
EXPECT_EQ(ErrorCode::INVALID_PARAMS, different_group_result.error());
ReplicateConfig explicit_ungrouped_config;
explicit_ungrouped_config.replica_num = 1;
explicit_ungrouped_config.group_ids = std::vector<std::string>{""};
auto explicit_ungrouped_result =
service_->UpsertStart(client_id, key, 1024, explicit_ungrouped_config);
ASSERT_FALSE(explicit_ungrouped_result.has_value());
EXPECT_EQ(ErrorCode::INVALID_PARAMS, explicit_ungrouped_result.error());
}
TEST_F(MasterServiceTest, IncompleteGroupedUpsertCanBecomeUngrouped) {
std::unique_ptr<MasterService> service_(new MasterService());
const auto context = PrepareSimpleSegment(*service_);
const UUID client_id = context.client_id;
const std::string key = "incomplete_grouped_upsert_to_ungrouped";
ReplicateConfig grouped_config;
grouped_config.replica_num = 1;
grouped_config.group_ids =
std::vector<std::string>{FindGroupIdOnDifferentShard(key)};
ASSERT_TRUE(
service_->PutStart(client_id, key, 1024, grouped_config).has_value());
std::this_thread::sleep_for(std::chrono::milliseconds(2));
ReplicateConfig ungrouped_config;
ungrouped_config.replica_num = 1;
auto upsert_start =
service_->UpsertStart(client_id, key, 1024, ungrouped_config);
ASSERT_TRUE(upsert_start.has_value()) << toString(upsert_start.error());
ASSERT_TRUE(
service_->UpsertEnd(client_id, key, ReplicaType::MEMORY).has_value());
EXPECT_TRUE(service_->ExistKey(key).value_or(false));
}
TEST_F(MasterServiceTest, UpsertRejectsExistingUngroupedToGrouped) {
std::unique_ptr<MasterService> service_(new MasterService());
[[maybe_unused]] const auto context = PrepareSimpleSegment(*service_);
const UUID client_id = generate_uuid();
const std::string key = "upsert_ungrouped_to_grouped";
ReplicateConfig ungrouped_config;
ungrouped_config.replica_num = 1;
PutCompletedObject(*service_, client_id, key, ungrouped_config);
ReplicateConfig grouped_config;
grouped_config.replica_num = 1;
grouped_config.group_ids =
std::vector<std::string>{FindGroupIdOnDifferentShard(key)};
auto upsert_start =
service_->UpsertStart(client_id, key, 2048, grouped_config);
ASSERT_FALSE(upsert_start.has_value());
EXPECT_EQ(ErrorCode::INVALID_PARAMS, upsert_start.error());
EXPECT_TRUE(service_->GetReplicaList(key).has_value());
}
TEST_F(MasterServiceTest,
GroupedEvictionExpandsSafeMembersAndSkipsLeasedGroup) {
auto service_config =
MasterServiceConfig::builder().set_default_kv_lease_ttl(1000).build();
constexpr size_t kSegmentSize = 4 * 1024 * 1024;
constexpr size_t kObjectSize = 2 * 1024 * 1024;
{
std::unique_ptr<MasterService> service_(
new MasterService(service_config));
[[maybe_unused]] const auto context =
PrepareSimpleSegment(*service_, "grouped_evict_segment",
kDefaultSegmentBase, kSegmentSize);
const UUID client_id = generate_uuid();
const std::string evict_key_a = "grouped_evict_key_a";
const std::string evict_key_b = "grouped_evict_key_b";
ReplicateConfig evict_config;
evict_config.replica_num = 1;
evict_config.group_ids =
std::vector<std::string>{FindGroupIdOnDifferentShard(evict_key_a)};
PutCompletedObject(*service_, client_id, evict_key_a, evict_config,
kObjectSize);
PutCompletedObject(*service_, client_id, evict_key_b, evict_config,
kObjectSize);
ReplicateConfig trigger_config;
trigger_config.replica_num = 1;
auto trigger_result = service_->PutStart(
client_id, "trigger_grouped_eviction", kObjectSize, trigger_config);
ASSERT_FALSE(trigger_result.has_value());
EXPECT_EQ(ErrorCode::NO_AVAILABLE_HANDLE, trigger_result.error());
std::this_thread::sleep_for(std::chrono::milliseconds(200));
EXPECT_FALSE(service_->ExistKey(evict_key_a).value_or(true));
EXPECT_FALSE(service_->ExistKey(evict_key_b).value_or(true));
}
{
std::unique_ptr<MasterService> service_(
new MasterService(service_config));
[[maybe_unused]] const auto context =
PrepareSimpleSegment(*service_, "grouped_lease_segment",
kDefaultSegmentBase, kSegmentSize);
const UUID client_id = generate_uuid();
const std::string leased_key_a = "grouped_leased_key_a";
const std::string leased_key_b = "grouped_leased_key_b";
ReplicateConfig leased_config;
leased_config.replica_num = 1;
leased_config.group_ids =
std::vector<std::string>{FindGroupIdOnDifferentShard(leased_key_a)};
PutCompletedObject(*service_, client_id, leased_key_a, leased_config,
kObjectSize);
PutCompletedObject(*service_, client_id, leased_key_b, leased_config,
kObjectSize);
auto exists = service_->ExistKey(leased_key_a);
ASSERT_TRUE(exists.has_value());
ASSERT_TRUE(exists.value());
ReplicateConfig trigger_config;
trigger_config.replica_num = 1;
auto trigger_result =
service_->PutStart(client_id, "trigger_leased_group_eviction",
kObjectSize, trigger_config);
ASSERT_FALSE(trigger_result.has_value());
EXPECT_EQ(ErrorCode::NO_AVAILABLE_HANDLE, trigger_result.error());
std::this_thread::sleep_for(std::chrono::milliseconds(200));
EXPECT_TRUE(service_->GetReplicaList(leased_key_a).has_value());
EXPECT_TRUE(service_->GetReplicaList(leased_key_b).has_value());
}
}
TEST_F(MasterServiceTest, GroupedEvictionSkipsUnsafeMembersAndEvictsSafePeers) {
constexpr size_t kSegmentSize = 4 * 1024 * 1024;
constexpr size_t kObjectSize = 2 * 1024 * 1024;
std::unique_ptr<MasterService> service_(new MasterService());
[[maybe_unused]] const auto context =
PrepareSimpleSegment(*service_, "grouped_mixed_safety_segment",
kDefaultSegmentBase, kSegmentSize);
const UUID client_id = generate_uuid();
const std::string safe_key = "grouped_mixed_safe_key";
const std::string hard_pinned_key = "grouped_mixed_hard_pinned_key";
const std::string group_id = FindGroupIdOnDifferentShard(safe_key);
ReplicateConfig safe_config;
safe_config.replica_num = 1;
safe_config.group_ids = std::vector<std::string>{group_id};
PutCompletedObject(*service_, client_id, safe_key, safe_config,
kObjectSize);
ReplicateConfig hard_pinned_config = safe_config;
hard_pinned_config.with_hard_pin = true;
PutCompletedObject(*service_, client_id, hard_pinned_key,
hard_pinned_config, kObjectSize);
ReplicateConfig trigger_config;
trigger_config.replica_num = 1;
auto trigger_result =
service_->PutStart(client_id, "trigger_mixed_safety_group_eviction",
kObjectSize, trigger_config);
ASSERT_FALSE(trigger_result.has_value());
EXPECT_EQ(ErrorCode::NO_AVAILABLE_HANDLE, trigger_result.error());
std::this_thread::sleep_for(std::chrono::milliseconds(200));
EXPECT_FALSE(service_->ExistKey(safe_key).value_or(true));
EXPECT_TRUE(service_->GetReplicaList(hard_pinned_key).has_value());
EXPECT_TRUE(service_->Remove(hard_pinned_key, /*force=*/true).has_value());
}
TEST_F(MasterServiceTest, BatchUpsertStartMixedGroupIdsPreservesOrder) {
std::unique_ptr<MasterService> service_(new MasterService());
[[maybe_unused]] const auto context = PrepareSimpleSegment(*service_);
const UUID client_id = generate_uuid();
const std::vector<std::string> keys = {
"batch_grouped_a",
"batch_ungrouped",
"batch_grouped_b",
};
const std::vector<uint64_t> sizes = {1024, 2048, 4096};
ReplicateConfig config;
config.replica_num = 1;
config.group_ids =
std::vector<std::string>{FindGroupIdOnDifferentShard(keys[0]), "",
FindGroupIdOnDifferentShard(keys[2])};
auto results = service_->BatchUpsertStart(client_id, keys, sizes, config);
ASSERT_EQ(results.size(), keys.size());
for (const auto& result : results) {
ASSERT_TRUE(result.has_value());
}
auto end_results = service_->BatchUpsertEnd(client_id, keys);
ASSERT_EQ(end_results.size(), keys.size());
for (const auto& result : end_results) {
ASSERT_TRUE(result.has_value());
}
for (const auto& key : keys) {
EXPECT_TRUE(service_->GetReplicaList(key).has_value());
}
ReplicateConfig invalid_config = config;
invalid_config.group_ids = std::vector<std::string>{"only_one"};
auto invalid_results =
service_->BatchUpsertStart(client_id, keys, sizes, invalid_config);
ASSERT_EQ(invalid_results.size(), keys.size());
for (const auto& result : invalid_results) {
ASSERT_FALSE(result.has_value());
EXPECT_EQ(ErrorCode::INVALID_PARAMS, result.error());
}
}
TEST_F(MasterServiceTest, WrappedBatchPutStartMixedGroupIdsPreservesOrder) {
WrappedMasterServiceConfig service_config;
service_config.default_kv_lease_ttl = 100;
service_config.enable_metric_reporting = false;
WrappedMasterService service_(service_config);
Segment segment = MakeSegment("wrapped_batch_group_segment");
const UUID client_id = generate_uuid();
ASSERT_TRUE(service_.MountSegment(segment, client_id).has_value());
const std::vector<std::string> keys = {
"wrapped_batch_grouped_a",
"wrapped_batch_ungrouped",
"wrapped_batch_grouped_b",
};
const std::vector<uint64_t> sizes = {1024, 2048, 4096};
ReplicateConfig config;
config.replica_num = 1;
config.group_ids =
std::vector<std::string>{FindGroupIdOnDifferentShard(keys[0]), "",
FindGroupIdOnDifferentShard(keys[2])};
auto results = service_.BatchPutStart(client_id, keys, sizes, config);
ASSERT_EQ(results.size(), keys.size());
for (const auto& result : results) {
ASSERT_TRUE(result.has_value()) << toString(result.error());
}
auto end_results = service_.BatchPutEnd(client_id, keys);
ASSERT_EQ(end_results.size(), keys.size());
for (const auto& result : end_results) {
ASSERT_TRUE(result.has_value());
}
for (const auto& key : keys) {
EXPECT_TRUE(service_.GetReplicaList(key).has_value());
}
ReplicateConfig invalid_config = config;
invalid_config.group_ids = std::vector<std::string>{"only_one"};
auto invalid_group_results =
service_.BatchPutStart(client_id, keys, sizes, invalid_config);
ASSERT_EQ(invalid_group_results.size(), keys.size());
for (const auto& result : invalid_group_results) {
ASSERT_FALSE(result.has_value());
EXPECT_EQ(ErrorCode::INVALID_PARAMS, result.error());
}
auto invalid_size_results =
service_.BatchPutStart(client_id, keys, {1024}, config);
ASSERT_EQ(invalid_size_results.size(), keys.size());
for (const auto& result : invalid_size_results) {
ASSERT_FALSE(result.has_value());
EXPECT_EQ(ErrorCode::INVALID_PARAMS, result.error());
}
}
TEST_F(MasterServiceTest, PutStartEndFlow) {
std::unique_ptr<MasterService> service_(new MasterService());
[[maybe_unused]] const auto context = PrepareSimpleSegment(*service_);
const UUID client_id = generate_uuid();
const UUID invalid_client_id = generate_uuid();
ASSERT_NE(client_id, invalid_client_id);
// Test PutStart
std::string key = "test_key";
uint64_t value_length = 1024;
ReplicateConfig config;
config.replica_num = 1;
auto put_start_result =
service_->PutStart(client_id, key, value_length, config);
EXPECT_TRUE(put_start_result.has_value());
replica_list = put_start_result.value();
EXPECT_FALSE(replica_list.empty());
EXPECT_EQ(ReplicaStatus::PROCESSING, replica_list[0].status);
// During put, Get/Remove should fail
auto get_replica_result = service_->GetReplicaList(key);
EXPECT_FALSE(get_replica_result.has_value());
EXPECT_EQ(ErrorCode::REPLICA_IS_NOT_READY, get_replica_result.error());
auto remove_result = service_->Remove(key);
EXPECT_FALSE(remove_result.has_value());
EXPECT_EQ(ErrorCode::REPLICA_IS_NOT_READY, remove_result.error());
// PutEnd should fail if the client_id does not match.
auto put_end_fail_result =
service_->PutEnd(invalid_client_id, key, ReplicaType::MEMORY);
EXPECT_FALSE(put_end_fail_result.has_value());
EXPECT_EQ(put_end_fail_result.error(), ErrorCode::ILLEGAL_CLIENT);
// PutRevoke should fail if the client_id does not match.
auto put_revoke_fail_result =
service_->PutRevoke(invalid_client_id, key, ReplicaType::MEMORY);
EXPECT_FALSE(put_revoke_fail_result.has_value());
EXPECT_EQ(put_revoke_fail_result.error(), ErrorCode::ILLEGAL_CLIENT);
// Test PutEnd
auto put_end_result = service_->PutEnd(client_id, key, ReplicaType::MEMORY);
EXPECT_TRUE(put_end_result.has_value());
// Verify replica list after PutEnd
auto final_get_result = service_->GetReplicaList(key);
EXPECT_TRUE(final_get_result.has_value());
replica_list = final_get_result.value().replicas;
EXPECT_EQ(1, replica_list.size());
EXPECT_EQ(ReplicaStatus::COMPLETE, replica_list[0].status);
}
TEST_F(MasterServiceTest, TenantPutGetRemoveIsolatesSameUserKey) {
std::unique_ptr<MasterService> service_(new MasterService());
[[maybe_unused]] const auto context = PrepareSimpleSegment(*service_);
const UUID client_id = generate_uuid();
const std::string key = "shared_user_key";
const std::string tenant_a = "tenant_a";
const std::string tenant_b = "tenant_b";
ReplicateConfig config;
config.replica_num = 1;
ASSERT_TRUE(
service_->PutStart(client_id, key, tenant_a, 1024, config).has_value());
ASSERT_TRUE(service_->PutEnd(client_id, key, tenant_a, ReplicaType::MEMORY)
.has_value());
ASSERT_TRUE(
service_->PutStart(client_id, key, tenant_b, 2048, config).has_value());
ASSERT_TRUE(service_->PutEnd(client_id, key, tenant_b, ReplicaType::MEMORY)
.has_value());
EXPECT_FALSE(service_->GetReplicaList(key).has_value());
EXPECT_FALSE(service_->ExistKey(key).value());
EXPECT_TRUE(service_->ExistKey(key, tenant_a).value());
EXPECT_TRUE(service_->ExistKey(key, tenant_b).value());
EXPECT_TRUE(service_->GetReplicaList(key, tenant_a).has_value());
EXPECT_TRUE(service_->GetReplicaList(key, tenant_b).has_value());
EXPECT_EQ(service_->GetKeyCount(), 2u);
ASSERT_TRUE(service_->Remove(key, tenant_a, /*force=*/true).has_value());
EXPECT_FALSE(service_->GetReplicaList(key, tenant_a).has_value());
EXPECT_TRUE(service_->GetReplicaList(key, tenant_b).has_value());
EXPECT_EQ(service_->GetKeyCount(), 1u);
}
TEST_F(MasterServiceTest, RegexOperationsAreTenantScoped) {
std::unique_ptr<MasterService> service_(new MasterService());
[[maybe_unused]] const auto context = PrepareSimpleSegment(*service_);
const UUID client_id = generate_uuid();
const std::string key = "regex_shared_key";
const std::string tenant_a = "tenant_regex_a";
const std::string tenant_b = "tenant_regex_b";
ReplicateConfig config;
config.replica_num = 1;
ASSERT_TRUE(service_->PutStart(client_id, key, 1024, config).has_value());
ASSERT_TRUE(
service_->PutEnd(client_id, key, ReplicaType::MEMORY).has_value());
ASSERT_TRUE(
service_->PutStart(client_id, key, tenant_a, 1024, config).has_value());
ASSERT_TRUE(service_->PutEnd(client_id, key, tenant_a, ReplicaType::MEMORY)
.has_value());
ASSERT_TRUE(
service_->PutStart(client_id, key, tenant_b, 1024, config).has_value());
ASSERT_TRUE(service_->PutEnd(client_id, key, tenant_b, ReplicaType::MEMORY)
.has_value());
auto default_matches = service_->GetReplicaListByRegex("^regex_shared");
ASSERT_TRUE(default_matches.has_value());
EXPECT_EQ(default_matches->size(), 1);
auto remove_default =
service_->RemoveByRegex("^regex_shared", /*force=*/true);
ASSERT_TRUE(remove_default.has_value());
EXPECT_EQ(remove_default.value(), 1);
EXPECT_FALSE(service_->GetReplicaList(key).has_value());
EXPECT_TRUE(service_->GetReplicaList(key, tenant_a).has_value());
EXPECT_TRUE(service_->GetReplicaList(key, tenant_b).has_value());
auto remove_tenant_a =
service_->RemoveByRegex("^regex_shared", tenant_a, /*force=*/true);
ASSERT_TRUE(remove_tenant_a.has_value());
EXPECT_EQ(remove_tenant_a.value(), 1);
EXPECT_FALSE(service_->GetReplicaList(key, tenant_a).has_value());
EXPECT_TRUE(service_->GetReplicaList(key, tenant_b).has_value());
}
TEST_F(MasterServiceTest, TenantBatchUpsertAndRevokeAreScoped) {
auto svc = std::make_unique<MasterService>();
[[maybe_unused]] const auto context = PrepareSimpleSegment(*svc);
const UUID client_id = generate_uuid();
const std::vector<std::string> keys = {"tenant_batch_upsert_key_a",
"tenant_batch_upsert_key_b"};
const std::vector<uint64_t> sizes = {1024, 2048};
const std::string tenant_a = "tenant_batch_upsert_a";
const std::string tenant_b = "tenant_batch_upsert_b";
ReplicateConfig config;
config.replica_num = 1;
auto tenant_a_results =
svc->BatchUpsertStart(client_id, keys, tenant_a, sizes, config);
ASSERT_EQ(tenant_a_results.size(), keys.size());
for (const auto& result : tenant_a_results) {
ASSERT_TRUE(result.has_value());
}
auto tenant_a_end = svc->BatchUpsertEnd(client_id, keys, tenant_a);
ASSERT_EQ(tenant_a_end.size(), keys.size());
for (const auto& result : tenant_a_end) {
ASSERT_TRUE(result.has_value());
}
auto tenant_b_results =
svc->BatchUpsertStart(client_id, keys, tenant_b, sizes, config);
ASSERT_EQ(tenant_b_results.size(), keys.size());
for (const auto& result : tenant_b_results) {
ASSERT_TRUE(result.has_value());
}
auto tenant_b_end = svc->BatchUpsertEnd(client_id, keys, tenant_b);
ASSERT_EQ(tenant_b_end.size(), keys.size());
for (const auto& result : tenant_b_end) {
ASSERT_TRUE(result.has_value());
}
for (const auto& key : keys) {
EXPECT_FALSE(svc->GetReplicaList(key).has_value());
EXPECT_TRUE(svc->GetReplicaList(key, tenant_a).has_value());
EXPECT_TRUE(svc->GetReplicaList(key, tenant_b).has_value());
}
const std::string revoke_key = "tenant_batch_upsert_revoke_key";
auto revoke_start =
svc->UpsertStart(client_id, revoke_key, tenant_a, 1024, config);
ASSERT_TRUE(revoke_start.has_value());
ASSERT_TRUE(
svc->UpsertRevoke(client_id, revoke_key, tenant_a, ReplicaType::MEMORY)
.has_value());
EXPECT_FALSE(svc->GetReplicaList(revoke_key, tenant_a).has_value());
}
TEST_F(MasterServiceTest, TenantBatchRemoveAndRemoveAllAreScoped) {
auto svc = std::make_unique<MasterService>();
[[maybe_unused]] const auto context = PrepareSimpleSegment(*svc);
const UUID client_id = generate_uuid();
const std::string shared_key = "tenant_batch_remove_shared_key";
const std::string tenant_a = "tenant_batch_remove_a";
const std::string tenant_b = "tenant_batch_remove_b";
ReplicateConfig config;
config.replica_num = 1;
ASSERT_TRUE(svc->PutStart(client_id, shared_key, 1024, config).has_value());
ASSERT_TRUE(
svc->PutEnd(client_id, shared_key, ReplicaType::MEMORY).has_value());
ASSERT_TRUE(svc->PutStart(client_id, shared_key, tenant_a, 1024, config)
.has_value());
ASSERT_TRUE(
svc->PutEnd(client_id, shared_key, tenant_a, ReplicaType::MEMORY)
.has_value());
ASSERT_TRUE(svc->PutStart(client_id, shared_key, tenant_b, 1024, config)
.has_value());
ASSERT_TRUE(
svc->PutEnd(client_id, shared_key, tenant_b, ReplicaType::MEMORY)
.has_value());
auto remove_a = svc->BatchRemove({shared_key}, tenant_a, /*force=*/true);
ASSERT_EQ(remove_a.size(), 1u);
ASSERT_TRUE(remove_a[0].has_value());
EXPECT_FALSE(svc->GetReplicaList(shared_key, tenant_a).has_value());
EXPECT_TRUE(svc->GetReplicaList(shared_key).has_value());
EXPECT_TRUE(svc->GetReplicaList(shared_key, tenant_b).has_value());
EXPECT_EQ(svc->RemoveAll(tenant_b, /*force=*/true), 1);
EXPECT_FALSE(svc->GetReplicaList(shared_key, tenant_b).has_value());
EXPECT_TRUE(svc->GetReplicaList(shared_key).has_value());
EXPECT_EQ(svc->RemoveAll(/*force=*/true), 1);
EXPECT_FALSE(svc->GetReplicaList(shared_key).has_value());
}
TEST_F(MasterServiceTest, LegacyRemoveAllRemovesAllTenants) {
auto svc = std::make_unique<MasterService>();
[[maybe_unused]] const auto context = PrepareSimpleSegment(*svc);
const UUID client_id = generate_uuid();
const std::string key = "legacy_remove_all_shared_key";
const std::string tenant_a = "legacy_remove_all_a";
const std::string tenant_b = "legacy_remove_all_b";
ReplicateConfig config;
config.replica_num = 1;
ASSERT_TRUE(svc->PutStart(client_id, key, 1024, config).has_value());
ASSERT_TRUE(svc->PutEnd(client_id, key, ReplicaType::MEMORY).has_value());
ASSERT_TRUE(
svc->PutStart(client_id, key, tenant_a, 1024, config).has_value());
ASSERT_TRUE(
svc->PutEnd(client_id, key, tenant_a, ReplicaType::MEMORY).has_value());
ASSERT_TRUE(
svc->PutStart(client_id, key, tenant_b, 1024, config).has_value());
ASSERT_TRUE(
svc->PutEnd(client_id, key, tenant_b, ReplicaType::MEMORY).has_value());
EXPECT_EQ(svc->RemoveAll(/*force=*/true), 3);
EXPECT_FALSE(svc->GetReplicaList(key).has_value());
EXPECT_FALSE(svc->GetReplicaList(key, tenant_a).has_value());
EXPECT_FALSE(svc->GetReplicaList(key, tenant_b).has_value());
EXPECT_EQ(svc->RemoveAll(/*force=*/true), 0);
}
TEST_F(MasterServiceTest, PutWithPreferredSegment) {
// For backward compatibility, test the deprecated single preferred_segment
std::unique_ptr<MasterService> service_(new MasterService());
const UUID client_id = generate_uuid();
// Mount 3 segments, each 16MB
constexpr size_t kBaseAddr = 0x300000000;
constexpr size_t kSegmentSize = 1024 * 1024 * 16; // 16MB
for (int i = 0; i < 3; ++i) {
[[maybe_unused]] const auto context = PrepareSimpleSegment(
*service_, "segment_" + std::to_string(i),
kBaseAddr + static_cast<size_t>(i) * kSegmentSize, kSegmentSize);
}
// Prepare preferred segments
std::string preferred_segment = "segment_1";
// Test PutStart with multiple preferred segments
std::string key = "test_key";
uint64_t value_length = 1024;
ReplicateConfig config;
config.replica_num = 1;
config.preferred_segment = preferred_segment;
auto put_start_result =
service_->PutStart(client_id, key, value_length, config);
EXPECT_TRUE(put_start_result.has_value());
replica_list = put_start_result.value();
EXPECT_EQ(1, replica_list.size());
EXPECT_EQ(ReplicaStatus::PROCESSING, replica_list[0].status);
const auto& mem_desc = replica_list[0].get_memory_descriptor();
EXPECT_EQ(preferred_segment,
mem_desc.buffer_descriptor.transport_endpoint_);
// Complete the Put operation
auto put_end_result = service_->PutEnd(client_id, key, ReplicaType::MEMORY);
EXPECT_TRUE(put_end_result.has_value());
}
TEST_F(MasterServiceTest, PutWithPreferredSegments) {
std::unique_ptr<MasterService> service_(new MasterService());
const UUID client_id = generate_uuid();
// Mount 3 segments, each 16MB
constexpr size_t kBaseAddr = 0x300000000;
constexpr size_t kSegmentSize = 1024 * 1024 * 16; // 16MB
for (int i = 0; i < 3; ++i) {
[[maybe_unused]] const auto context = PrepareSimpleSegment(
*service_, "segment_" + std::to_string(i),
kBaseAddr + static_cast<size_t>(i) * kSegmentSize, kSegmentSize);
}
// Prepare preferred segments
std::vector<std::string> preferred_segments = {"segment_0", "segment_1"};
// Test PutStart with multiple preferred segments
std::string key = "test_key";
uint64_t value_length = 1024;
ReplicateConfig config;
config.replica_num = 2;
config.preferred_segments = preferred_segments;
auto put_start_result =
service_->PutStart(client_id, key, value_length, config);
EXPECT_TRUE(put_start_result.has_value());
replica_list = put_start_result.value();
EXPECT_EQ(2, replica_list.size());
EXPECT_EQ(ReplicaStatus::PROCESSING, replica_list[0].status);
EXPECT_EQ(ReplicaStatus::PROCESSING, replica_list[1].status);
const auto& mem_desc1 = replica_list[0].get_memory_descriptor();
const auto& mem_desc2 = replica_list[1].get_memory_descriptor();
std::unordered_set<std::string> used_segments = {
mem_desc1.buffer_descriptor.transport_endpoint_,
mem_desc2.buffer_descriptor.transport_endpoint_};
EXPECT_TRUE(used_segments.find("segment_0") != used_segments.end());
EXPECT_TRUE(used_segments.find("segment_1") != used_segments.end());
// Complete the Put operation
auto put_end_result = service_->PutEnd(client_id, key, ReplicaType::MEMORY);
EXPECT_TRUE(put_end_result.has_value());
}
TEST_F(MasterServiceTest, RandomPutStartEndFlow) {
std::unique_ptr<MasterService> service_(new MasterService());
const UUID client_id = generate_uuid();
// Mount 5 segments, each 16MB
constexpr size_t kBaseAddr = 0x300000000;
constexpr size_t kSegmentSize = 1024 * 1024 * 16; // 16MB
for (int i = 0; i < 5; ++i) {
[[maybe_unused]] const auto context = PrepareSimpleSegment(
*service_, "segment_" + std::to_string(i),
kBaseAddr + static_cast<size_t>(i) * kSegmentSize, kSegmentSize);
}
// Test PutStart
std::string key = "test_key";
uint64_t value_length = 1024;
ReplicateConfig config;
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<> dis(1, 5);
int random_number = dis(gen);
config.replica_num = random_number;
auto put_start_result =
service_->PutStart(client_id, key, value_length, config);
EXPECT_TRUE(put_start_result.has_value());
replica_list = put_start_result.value();
EXPECT_FALSE(replica_list.empty());
EXPECT_EQ(ReplicaStatus::PROCESSING, replica_list[0].status);
// During put, Get/Remove should fail
auto get_result = service_->GetReplicaList(key);
EXPECT_FALSE(get_result.has_value());
EXPECT_EQ(ErrorCode::REPLICA_IS_NOT_READY, get_result.error());
auto remove_result = service_->Remove(key);
EXPECT_FALSE(remove_result.has_value());
EXPECT_EQ(ErrorCode::REPLICA_IS_NOT_READY, remove_result.error());
// Test PutEnd
auto put_end_result = service_->PutEnd(client_id, key, ReplicaType::MEMORY);
EXPECT_TRUE(put_end_result.has_value());
// Verify replica list after PutEnd
auto get_result2 = service_->GetReplicaList(key);
EXPECT_TRUE(get_result2.has_value());
replica_list = get_result2.value().replicas;
EXPECT_EQ(random_number, replica_list.size());
for (int i = 0; i < random_number; ++i) {
EXPECT_EQ(ReplicaStatus::COMPLETE, replica_list[i].status);
}
}
TEST_F(MasterServiceTest, GetReplicaListByRegex) {
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));
const UUID client_id = generate_uuid();
// Test getting non-existent key
auto get_result = service_->GetReplicaList(".*non_existent.*");
EXPECT_FALSE(get_result.has_value());
EXPECT_EQ(ErrorCode::OBJECT_NOT_FOUND, get_result.error());
[[maybe_unused]] const auto context = PrepareSimpleSegment(*service_);
int times = 10;
while (times--) {
std::string key = "test_key" + std::to_string(times);
uint64_t value_length = 1024;
ReplicateConfig config;
config.replica_num = 1;
auto put_start_result =
service_->PutStart(client_id, key, value_length, config);
ASSERT_TRUE(put_start_result.has_value());
auto put_end_result =
service_->PutEnd(client_id, key, ReplicaType::MEMORY);
ASSERT_TRUE(put_end_result.has_value());
auto exist_result = service_->ExistKey(key);
ASSERT_TRUE(exist_result.has_value());
}
// wait for all the lease to expire
std::this_thread::sleep_for(std::chrono::milliseconds(kv_lease_ttl));
// Test getting existing key
auto get_result2 = service_->GetReplicaListByRegex("^test_key");
EXPECT_TRUE(get_result2.has_value());
auto replica_list_local = get_result2.value();
EXPECT_EQ(10, replica_list_local.size());
}
// Helper function to put an object, making the test cleaner
void put_object(MasterService& service, const UUID& client_id,
const std::string& key) {
uint64_t value_length = 1024;
ReplicateConfig config;
config.replica_num = 1;
auto put_start_result =
service.PutStart(client_id, key, value_length, config);
ASSERT_TRUE(put_start_result.has_value())
<< "Failed to PutStart for key: " << key;
auto put_end_result = service.PutEnd(client_id, key, ReplicaType::MEMORY);
ASSERT_TRUE(put_end_result.has_value())
<< "Failed to PutEnd for key: " << key;
auto exist_result = service.ExistKey(key);
ASSERT_TRUE(exist_result.has_value())
<< "Key does not exist after put: " << key;
}
TEST_F(MasterServiceTest, GetReplicaListByRegexComplex) {
const uint64_t kv_lease_ttl = 100;
auto service_config = MasterServiceConfig::builder()
.set_default_kv_lease_ttl(kv_lease_ttl)
.build();
auto service_ = std::make_unique<MasterService>(service_config);
const UUID client_id = generate_uuid();
// 1. Mount segment
[[maybe_unused]] const auto context = PrepareSimpleSegment(*service_);
// 2. Prepare a diverse set of keys
std::vector<std::string> keys_to_put = {
// Basic keys for prefix matching
"test_key_01", "test_key_02", "test_key_10",
// Keys with different prefixes
"prod_key_alpha", "prod_key_beta",
// Keys with numbers in the middle
"data_part_1_chunk_a", "data_part_2_chunk_b",
// Keys with special characters (if your system supports them)
"config/user/settings.json", "logs/app-2025-08-13.log",
// Keys with varying lengths
"short", "a_very_very_very_long_key_that_tests_length_limits",
// Keys that look similar but should not match certain regex
"test-key-extra", "another_key"};
for (const auto& key : keys_to_put) {
put_object(*service_, client_id, key);
}
// Wait for all leases to be written to the underlying KV store.
// In a real system, you might not need this if PutEnd is synchronous.
// For this test, let's assume it's needed for consistency.
std::this_thread::sleep_for(std::chrono::milliseconds(kv_lease_ttl));
// 3. Run a series of regex tests
// Test 3.1: Simple prefix matching
{
auto result = service_->GetReplicaListByRegex("^test_key_");
ASSERT_TRUE(result.has_value());
EXPECT_EQ(result.value().size(),
3); // Matches test_key_01, test_key_02, test_key_10
}
// Test 3.2: Matching with a wildcard for any number
{
auto result = service_->GetReplicaListByRegex("^test_key_\\d+$");
ASSERT_TRUE(result.has_value());
EXPECT_EQ(result.value().size(), 3);
}
// Test 3.3: Matching a specific pattern with wildcards
{
// Matches "data_part_1_chunk_a" and "data_part_2_chunk_b"
auto result =
service_->GetReplicaListByRegex("^data_part_\\d_chunk_.$");
ASSERT_TRUE(result.has_value());
EXPECT_EQ(result.value().size(), 2);
}
// Test 3.4: Matching keys containing a specific substring
{
// Matches all keys with "key" in them
auto result = service_->GetReplicaListByRegex("key");
ASSERT_TRUE(result.has_value());
// Expected: test_key_01, test_key_02, test_key_10,
// prod_key_alpha, prod_key_beta,
// a_very_very_very_long_key_that_tests_length_limits,
// test-key-extra, another_key
EXPECT_EQ(result.value().size(), 8);
}
// Test 3.5: Matching based on file-like paths
{
// Match all .log files
auto result = service_->GetReplicaListByRegex("\\.log$");
ASSERT_TRUE(result.has_value());
EXPECT_EQ(result.value().size(), 1);
EXPECT_EQ(result.value().begin()->first, "logs/app-2025-08-13.log");
}
// Test 3.6: OR condition using |
{
// Match keys starting with "prod" OR ending with "json"
auto result = service_->GetReplicaListByRegex("^prod|\\.json$");
ASSERT_TRUE(result.has_value());
// Expected: prod_key_alpha, prod_key_beta, config/user/settings.json
EXPECT_EQ(result.value().size(), 3);
}
// Test 3.7: Regex that should not match anything
{
auto result = service_->GetReplicaListByRegex("^non_existent_prefix_");
// This should succeed but return an empty map.
ASSERT_TRUE(result.has_value());
EXPECT_TRUE(result.value().empty());
}
// Test 3.8: Exact match regex
{
auto result = service_->GetReplicaListByRegex("^short$");
ASSERT_TRUE(result.has_value());
EXPECT_EQ(result.value().size(), 1);
EXPECT_EQ(result.value().begin()->first, "short");
}
// Test 3.9: Initial test for non-existent key (as a sanity check)
{
auto get_result =
service_->GetReplicaListByRegex(".*absolutely_non_existent.*");
// Depending on implementation, this could return an empty map or an
// error. Let's assume it returns an empty map for a valid regex with no
// matches.
ASSERT_TRUE(get_result.has_value());
EXPECT_TRUE(get_result.value().empty());
}
}
TEST_F(MasterServiceTest, GetReplicaList) {
std::unique_ptr<MasterService> service_(new MasterService());
const UUID client_id = generate_uuid();
// Test getting non-existent key
auto get_result = service_->GetReplicaList("non_existent");
EXPECT_FALSE(get_result.has_value());
EXPECT_EQ(ErrorCode::OBJECT_NOT_FOUND, get_result.error());
[[maybe_unused]] const auto context = PrepareSimpleSegment(*service_);
std::string key = "test_key";
uint64_t value_length = 1024;
ReplicateConfig config;
config.replica_num = 1;
auto put_start_result =
service_->PutStart(client_id, key, value_length, config);
ASSERT_TRUE(put_start_result.has_value());
auto put_end_result = service_->PutEnd(client_id, key, ReplicaType::MEMORY);
ASSERT_TRUE(put_end_result.has_value());
// Test getting existing key
auto get_result2 = service_->GetReplicaList(key);
EXPECT_TRUE(get_result2.has_value());
auto replica_list_local = get_result2.value().replicas;
EXPECT_FALSE(replica_list_local.empty());
}
TEST_F(MasterServiceTest, RemoveObject) {
std::unique_ptr<MasterService> service_(new MasterService());
[[maybe_unused]] const auto context = PrepareSimpleSegment(*service_);
const UUID client_id = generate_uuid();
std::string key = "test_key";
uint64_t value_length = 1024;
ReplicateConfig config;
config.replica_num = 1;
auto put_start_result =
service_->PutStart(client_id, key, value_length, config);
ASSERT_TRUE(put_start_result.has_value());
auto put_end_result = service_->PutEnd(client_id, key, ReplicaType::MEMORY);
ASSERT_TRUE(put_end_result.has_value());
// Test removing the object
auto remove_result = service_->Remove(key);
EXPECT_TRUE(remove_result.has_value());
// Verify object is removed
auto get_result = service_->GetReplicaList(key);
EXPECT_FALSE(get_result.has_value());
EXPECT_EQ(ErrorCode::OBJECT_NOT_FOUND, get_result.error());
// Test removing non-existent object
auto remove_result2 = service_->Remove("non_existent");
EXPECT_FALSE(remove_result2.has_value());
EXPECT_EQ(ErrorCode::OBJECT_NOT_FOUND, remove_result2.error());
}
TEST_F(MasterServiceTest, RandomRemoveObject) {
std::unique_ptr<MasterService> service_(new MasterService());
[[maybe_unused]] const auto context = PrepareSimpleSegment(*service_);
const UUID client_id = generate_uuid();
int times = 10;
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<> dis(1, 1000);
while (times--) {
std::string key = "test_key" + std::to_string(dis(gen));
uint64_t value_length = 1024;
ReplicateConfig config;
config.replica_num = 1;
auto put_start_result =
service_->PutStart(client_id, key, value_length, config);
ASSERT_TRUE(put_start_result.has_value());
auto put_end_result =
service_->PutEnd(client_id, key, ReplicaType::MEMORY);
ASSERT_TRUE(put_end_result.has_value());
// Test removing the object
auto remove_result = service_->Remove(key);
EXPECT_TRUE(remove_result.has_value());
// Verify object is removed
auto get_result = service_->GetReplicaList(key);
EXPECT_FALSE(get_result.has_value());
EXPECT_EQ(ErrorCode::OBJECT_NOT_FOUND, get_result.error());
}
}
TEST_F(MasterServiceTest, RemoveByRegex) {
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));
[[maybe_unused]] const auto context = PrepareSimpleSegment(*service_);
const UUID client_id = generate_uuid();
int times = 10;
while (times--) {
std::string key = "test_key" + std::to_string(times);
uint64_t value_length = 1024;
ReplicateConfig config;
config.replica_num = 1;
auto put_start_result =
service_->PutStart(client_id, key, value_length, config);
ASSERT_TRUE(put_start_result.has_value());
auto put_end_result =
service_->PutEnd(client_id, key, ReplicaType::MEMORY);
ASSERT_TRUE(put_end_result.has_value());
auto exist_result = service_->ExistKey(key);
ASSERT_TRUE(exist_result.has_value());
}
// wait for all the lease to expire
std::this_thread::sleep_for(std::chrono::milliseconds(kv_lease_ttl));
auto res = service_->RemoveByRegex("^test_key");
ASSERT_TRUE(res.has_value());
ASSERT_EQ(10, res.value());
times = 10;
while (times--) {
std::string key = "test_key" + std::to_string(times);
auto exist_result = service_->ExistKey(key);
ASSERT_TRUE(exist_result.has_value());
ASSERT_FALSE(exist_result.value());
}
}
TEST_F(MasterServiceTest, CopyStart) {
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 4 segments (segment_1, segment_2, segment_3, segment_4) with
// PrepareSimpleSegment
[[maybe_unused]] const auto context1 =
PrepareSimpleSegment(*service_, "segment_1");
[[maybe_unused]] const auto context2 =
PrepareSimpleSegment(*service_, "segment_2");
[[maybe_unused]] const auto context3 =
PrepareSimpleSegment(*service_, "segment_3");
[[maybe_unused]] const auto context4 =
PrepareSimpleSegment(*service_, "segment_4");
UUID client_id = generate_uuid();
// Test Case 1: CopyStart a non-existent key, should fail.
auto copy_result = service_->CopyStart(client_id, "non_existent_key",
"segment_1", {"segment_2"});
EXPECT_FALSE(copy_result.has_value());
EXPECT_EQ(ErrorCode::OBJECT_NOT_FOUND, copy_result.error());
// PutStart an object with 1 replica and preferred_segment=segment_1 for
// testing
std::string key = "test_key";
uint64_t slice_length = 1024;
ReplicateConfig config;
config.replica_num = 1;
config.preferred_segment = "segment_1";
auto put_start_result =
service_->PutStart(client_id, key, slice_length, config);
ASSERT_TRUE(put_start_result.has_value());
// Test Case 2: CopyStart to segment_2 and segment_3, should fail because
// the only replica is not completed.
copy_result = service_->CopyStart(client_id, key, "segment_1",
{"segment_2", "segment_3"});
EXPECT_FALSE(copy_result.has_value());
EXPECT_EQ(ErrorCode::REPLICA_NOT_FOUND, copy_result.error());
// PutEnd the object.
auto put_end_result = service_->PutEnd(client_id, key, ReplicaType::MEMORY);
ASSERT_TRUE(put_end_result.has_value());
// Test Case 3: CopyStart to segment_2 and segment_3, should success.
copy_result = service_->CopyStart(client_id, key, "segment_1",
{"segment_2", "segment_3"});
EXPECT_TRUE(copy_result.has_value());
auto copy_response = copy_result.value();
EXPECT_EQ("segment_1", copy_response.source.get_memory_descriptor()
.buffer_descriptor.transport_endpoint_);
EXPECT_EQ(2, copy_response.targets.size());
// Test Case 4: Try remove the object, should fail because it is copying.
auto remove_result = service_->Remove(key);
EXPECT_FALSE(remove_result.has_value());
EXPECT_EQ(ErrorCode::REPLICA_IS_NOT_READY, remove_result.error());
// Test Case 5: CopyStart to segment_4, should fail because there is an
// ongoing copy task.
copy_result =
service_->CopyStart(client_id, key, "segment_1", {"segment_4"});
EXPECT_FALSE(copy_result.has_value());
EXPECT_EQ(ErrorCode::OBJECT_HAS_REPLICATION_TASK, copy_result.error());
// Test Case 6: CopyEnd, should success and the object now has 3 replicas.
auto copy_end_result = service_->CopyEnd(client_id, key);
EXPECT_TRUE(copy_end_result.has_value());
auto get_result = service_->GetReplicaList(key);
EXPECT_TRUE(get_result.has_value());
EXPECT_EQ(3, get_result.value().replicas.size());
// Test Case 7: Copy from a non-existent replica to segment_3 and
// segment_4, should fail.
copy_result = service_->CopyStart(client_id, key, "non_existent_segment",
{"segment_3", "segment_4"});
EXPECT_FALSE(copy_result.has_value());
EXPECT_EQ(ErrorCode::REPLICA_NOT_FOUND, copy_result.error());
// Test Case 8: Copy to segment_4 and a non-existent segment, should fail.
copy_result = service_->CopyStart(client_id, key, "segment_1",
{"segment_4", "non_existent_segment"});
EXPECT_FALSE(copy_result.has_value());
EXPECT_EQ(ErrorCode::SEGMENT_NOT_FOUND, copy_result.error());
// Test Case 9: Copy to segment_3 and segment_4, should skip segment_3 and
// successfully copy to segment_4.
copy_result = service_->CopyStart(client_id, key, "segment_1",
{"segment_3", "segment_4"});
EXPECT_TRUE(copy_result.has_value());
copy_response = copy_result.value();
EXPECT_EQ("segment_1", copy_response.source.get_memory_descriptor()
.buffer_descriptor.transport_endpoint_);
EXPECT_EQ(1, copy_response.targets
.size()); // Only 1 replica since segment_3 is skipped
EXPECT_EQ("segment_4", copy_response.targets[0]
.get_memory_descriptor()
.buffer_descriptor.transport_endpoint_);
// End the copy operation to clean up state
copy_end_result = service_->CopyEnd(client_id, key);
EXPECT_TRUE(copy_end_result.has_value());
get_result = service_->GetReplicaList(key);
EXPECT_TRUE(get_result.has_value());
EXPECT_EQ(4, get_result.value().replicas.size());
// Test Case 10: Copy to segment_4 again, should skip because it's already
// used.
copy_result =
service_->CopyStart(client_id, key, "segment_1", {"segment_4"});
EXPECT_TRUE(copy_result.has_value());
copy_response = copy_result.value();
EXPECT_EQ("segment_1", copy_response.source.get_memory_descriptor()
.buffer_descriptor.transport_endpoint_);
EXPECT_EQ(0,
copy_response.targets
.size()); // No replicas since segment_4 is already used
// Wait for the lease to expire
std::this_thread::sleep_for(std::chrono::milliseconds(kv_lease_ttl * 2));
// Test Case 11: Try remove the object, should fail because it is copying.
remove_result = service_->Remove(key);
EXPECT_FALSE(remove_result.has_value());
EXPECT_EQ(ErrorCode::OBJECT_HAS_REPLICATION_TASK, remove_result.error());
// Clean up the copy operation
copy_end_result = service_->CopyEnd(client_id, key);
EXPECT_TRUE(copy_end_result.has_value());
// Wait for the lease to expire
std::this_thread::sleep_for(std::chrono::milliseconds(kv_lease_ttl * 2));
// Test Case 12: Try remove the object, should success.
remove_result = service_->Remove(key);
EXPECT_TRUE(remove_result.has_value());
}
TEST_F(MasterServiceTest, CopyEnd) {
std::unique_ptr<MasterService> service_(new MasterService());
// Mount 3 segments (segment_1, segment_2, segment_3) with
// PrepareSimpleSegment
[[maybe_unused]] const auto context1 =
PrepareSimpleSegment(*service_, "segment_1");
[[maybe_unused]] const auto context2 =
PrepareSimpleSegment(*service_, "segment_2");
[[maybe_unused]] const auto context3 =
PrepareSimpleSegment(*service_, "segment_3");
UUID client_id = generate_uuid();
UUID invalid_client_id = generate_uuid();
// Test Case 1: CopyEnd a non-existent key, should fail.
auto copy_end_result = service_->CopyEnd(client_id, "non_existent_key");
EXPECT_FALSE(copy_end_result.has_value());
EXPECT_EQ(ErrorCode::OBJECT_NOT_FOUND, copy_end_result.error());
// Put an object with 1 replica and preferred_segment=segment_1 for testing
std::string key = "test_key";
uint64_t slice_length = 1024;
ReplicateConfig config;
config.replica_num = 1;
config.preferred_segment = "segment_1";
auto put_start_result =
service_->PutStart(client_id, key, slice_length, config);
ASSERT_TRUE(put_start_result.has_value());
auto put_end_result = service_->PutEnd(client_id, key, ReplicaType::MEMORY);
ASSERT_TRUE(put_end_result.has_value());
// Test Case 2: CopyEnd the object, should fail because there is no ongoing
// copy task.
copy_end_result = service_->CopyEnd(client_id, key);
EXPECT_FALSE(copy_end_result.has_value());
EXPECT_EQ(ErrorCode::OBJECT_NO_REPLICATION_TASK, copy_end_result.error());
// CopyStart the object to segment_2
auto copy_start_result =
service_->CopyStart(client_id, key, "segment_1", {"segment_2"});
ASSERT_TRUE(copy_start_result.has_value());
// Test Case 3: CopyEnd with an invalid client id, should fail.
copy_end_result = service_->CopyEnd(invalid_client_id, key);
EXPECT_FALSE(copy_end_result.has_value());
EXPECT_EQ(ErrorCode::ILLEGAL_CLIENT, copy_end_result.error());
// Test Case 4: MoveEnd the object, should fail because the ongoing task is
// Copy.
auto move_end_result = service_->MoveEnd(client_id, key);
EXPECT_FALSE(move_end_result.has_value());
EXPECT_EQ(ErrorCode::INVALID_PARAMS, move_end_result.error());
// Test Case 5: CopyEnd, should success.
copy_end_result = service_->CopyEnd(client_id, key);
EXPECT_TRUE(copy_end_result.has_value());
// Verify we now have 2 replicas
auto get_result = service_->GetReplicaList(key);
EXPECT_TRUE(get_result.has_value());
EXPECT_EQ(2, get_result.value().replicas.size());
// CopyStart the object from segment_1 to segment_3, then unmount segment_1
copy_start_result =
service_->CopyStart(client_id, key, "segment_1", {"segment_3"});
ASSERT_TRUE(copy_start_result.has_value());
// Unmount segment_1 to simulate source gone
auto unmount_result =
service_->UnmountSegment(context1.segment_id, context1.client_id);
ASSERT_TRUE(unmount_result.has_value());
// Test Case 6: CopyEnd, should fail because the source is gone, the object
// should have only 1 replica from segment_2.
copy_end_result = service_->CopyEnd(client_id, key);
EXPECT_FALSE(copy_end_result.has_value());
EXPECT_EQ(ErrorCode::REPLICA_IS_GONE, copy_end_result.error());
get_result = service_->GetReplicaList(key);
EXPECT_TRUE(get_result.has_value());
auto& replicas = get_result.value().replicas;
EXPECT_EQ(1, replicas.size());
EXPECT_EQ("segment_2", replicas[0]
.get_memory_descriptor()
.buffer_descriptor.transport_endpoint_);
// CopyStart the object from segment_2 to segment_3, then unmount segment_3
copy_start_result =
service_->CopyStart(client_id, key, "segment_2", {"segment_3"});
ASSERT_TRUE(copy_start_result.has_value());
// Unmount segment_3 to simulate target gone
unmount_result =
service_->UnmountSegment(context3.segment_id, context3.client_id);
ASSERT_TRUE(unmount_result.has_value());
// Test Case 7: CopyEnd, should fail because the target is gone, the object
// should have only 1 replica from segment_2.
copy_end_result = service_->CopyEnd(client_id, key);
EXPECT_FALSE(copy_end_result.has_value());
EXPECT_EQ(ErrorCode::REPLICA_IS_GONE, copy_end_result.error());
get_result = service_->GetReplicaList(key);
EXPECT_TRUE(get_result.has_value());
replicas = get_result.value().replicas;
EXPECT_EQ(1, replicas.size());
EXPECT_EQ("segment_2", replicas[0]
.get_memory_descriptor()
.buffer_descriptor.transport_endpoint_);
}
TEST_F(MasterServiceTest, CopyRevoke) {
std::unique_ptr<MasterService> service_(new MasterService());
// Mount 2 segments (segment_1, segment_2) with
// PrepareSimpleSegment
[[maybe_unused]] const auto context1 =
PrepareSimpleSegment(*service_, "segment_1");
[[maybe_unused]] const auto context2 =
PrepareSimpleSegment(*service_, "segment_2");
UUID client_id = generate_uuid();
UUID invalid_client_id = generate_uuid();
// Test Case 1: CopyRevoke a non-existent key, should fail.
auto copy_revoke_result =
service_->CopyRevoke(client_id, "non_existent_key");
EXPECT_FALSE(copy_revoke_result.has_value());
EXPECT_EQ(ErrorCode::OBJECT_NOT_FOUND, copy_revoke_result.error());
// Put an object with 1 replica and preferred_segment=segment_1 for testing
std::string key = "test_key";
uint64_t slice_length = 1024;
ReplicateConfig config;
config.replica_num = 1;
config.preferred_segment = "segment_1";
auto put_start_result =
service_->PutStart(client_id, key, slice_length, config);
ASSERT_TRUE(put_start_result.has_value());
auto put_end_result = service_->PutEnd(client_id, key, ReplicaType::MEMORY);
ASSERT_TRUE(put_end_result.has_value());
// Test Case 2: CopyRevoke the object, should fail because there is no
// ongoing copy task.
copy_revoke_result = service_->CopyRevoke(client_id, key);
EXPECT_FALSE(copy_revoke_result.has_value());
EXPECT_EQ(ErrorCode::OBJECT_NO_REPLICATION_TASK,
copy_revoke_result.error());
// CopyStart the object to segment_2
auto copy_start_result =
service_->CopyStart(client_id, key, "segment_1", {"segment_2"});
ASSERT_TRUE(copy_start_result.has_value());
// Test Case 3: CopyRevoke with an invalid client id, should fail.
copy_revoke_result = service_->CopyRevoke(invalid_client_id, key);
EXPECT_FALSE(copy_revoke_result.has_value());
EXPECT_EQ(ErrorCode::ILLEGAL_CLIENT, copy_revoke_result.error());
// Test Case 4: MoveRevoke the object, should fail because the ongoing task
// is Copy.
auto move_revoke_result = service_->MoveRevoke(client_id, key);
EXPECT_FALSE(move_revoke_result.has_value());
EXPECT_EQ(ErrorCode::INVALID_PARAMS, move_revoke_result.error());
// Test Case 5: CopyRevoke, should success.
copy_revoke_result = service_->CopyRevoke(client_id, key);
EXPECT_TRUE(copy_revoke_result.has_value());
// Verify we still have 1 replica (the copy was revoked)
auto get_result = service_->GetReplicaList(key);
EXPECT_TRUE(get_result.has_value());
EXPECT_EQ(1, get_result.value().replicas.size());
// CopyStart the object from segment_1 to segment_2 again, then unmount
// segment_1
copy_start_result =
service_->CopyStart(client_id, key, "segment_1", {"segment_2"});
ASSERT_TRUE(copy_start_result.has_value());
// Unmount segment_1 to simulate source gone
auto unmount_result =
service_->UnmountSegment(context1.segment_id, context1.client_id);
ASSERT_TRUE(unmount_result.has_value());
// Test Case 6: CopyRevoke, should success even though the source is gone,
// the object should be erased too.
copy_revoke_result = service_->CopyRevoke(client_id, key);
EXPECT_TRUE(copy_revoke_result.has_value());
// Verify the object has been removed.
get_result = service_->GetReplicaList(key);
EXPECT_FALSE(get_result.has_value());
}
TEST_F(MasterServiceTest, MoveStart) {
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 3 segments (segment_1, segment_2, segment_3) with
// PrepareSimpleSegment
[[maybe_unused]] const auto context1 =
PrepareSimpleSegment(*service_, "segment_1");
[[maybe_unused]] const auto context2 =
PrepareSimpleSegment(*service_, "segment_2");
[[maybe_unused]] const auto context3 =
PrepareSimpleSegment(*service_, "segment_3");
UUID client_id = generate_uuid();
// Test Case 1: MoveStart a non-existent key, should fail.
auto move_start_result = service_->MoveStart(client_id, "non_existent_key",
"segment_1", "segment_2");
EXPECT_FALSE(move_start_result.has_value());
EXPECT_EQ(ErrorCode::OBJECT_NOT_FOUND, move_start_result.error());
// Put an object with 1 replica and preferred_segment=segment_1 for testing
std::string key = "test_key";
uint64_t slice_length = 1024;
ReplicateConfig config;
config.replica_num = 1;
config.preferred_segment = "segment_1";
auto put_start_result =
service_->PutStart(client_id, key, slice_length, config);
ASSERT_TRUE(put_start_result.has_value());
// Test Case 2: MoveStart the object, should fail because the only replica
// is not completed.
move_start_result =
service_->MoveStart(client_id, key, "segment_1", "segment_2");
EXPECT_FALSE(move_start_result.has_value());
EXPECT_EQ(ErrorCode::REPLICA_NOT_FOUND, move_start_result.error());
// PutEnd the object.
auto put_end_result = service_->PutEnd(client_id, key, ReplicaType::MEMORY);
ASSERT_TRUE(put_end_result.has_value());
// Copy the object to segment_3.
auto copy_start_result =
service_->CopyStart(client_id, key, "segment_1", {"segment_3"});
ASSERT_TRUE(copy_start_result.has_value());
auto copy_end_result = service_->CopyEnd(client_id, key);
ASSERT_TRUE(copy_end_result.has_value());
// Test Case 3: MoveStart with source and target be the same, should fail.
move_start_result =
service_->MoveStart(client_id, key, "segment_1", "segment_1");
EXPECT_FALSE(move_start_result.has_value());
EXPECT_EQ(move_start_result.error(), ErrorCode::INVALID_PARAMS);
// Test Case 4: MoveStart to segment_2, should succeed.
move_start_result =
service_->MoveStart(client_id, key, "segment_1", "segment_2");
EXPECT_TRUE(move_start_result.has_value());
auto move_response = move_start_result.value();
EXPECT_EQ("segment_1", move_response.source.get_memory_descriptor()
.buffer_descriptor.transport_endpoint_);
EXPECT_TRUE(move_response.target.has_value());
EXPECT_EQ("segment_2", move_response.target.value()
.get_memory_descriptor()
.buffer_descriptor.transport_endpoint_);
// Test Case 5: Try remove the object, should fail because it is moving.
auto remove_result = service_->Remove(key);
EXPECT_FALSE(remove_result.has_value());
EXPECT_EQ(ErrorCode::REPLICA_IS_NOT_READY, remove_result.error());
// Test Case 6: MoveStart again, should fail because there is an ongoing
// move task.
move_start_result =
service_->MoveStart(client_id, key, "segment_1", "segment_3");
EXPECT_FALSE(move_start_result.has_value());
EXPECT_EQ(ErrorCode::OBJECT_HAS_REPLICATION_TASK,
move_start_result.error());
// Test Case 7: MoveEnd, should succeed and the object now has 2 replicas
// from segment_2 and segment_3
auto move_end_result = service_->MoveEnd(client_id, key);
EXPECT_TRUE(move_end_result.has_value());
auto get_result = service_->GetReplicaList(key);
EXPECT_TRUE(get_result.has_value());
auto& replicas = get_result.value().replicas;
EXPECT_EQ(2, replicas.size());
// Test Case 8: Move from a non-existent replica to segment_1, should fail.
move_start_result = service_->MoveStart(
client_id, key, "non_existent_segment", "segment_1");
EXPECT_FALSE(move_start_result.has_value());
EXPECT_EQ(ErrorCode::REPLICA_NOT_FOUND, move_start_result.error());
// Test Case 8.5: Move to a non-existent target segment, should fail.
move_start_result = service_->MoveStart(client_id, key, "segment_2",
"non_existent_segment");
EXPECT_FALSE(move_start_result.has_value());
EXPECT_EQ(ErrorCode::SEGMENT_NOT_FOUND, move_start_result.error());
// Test Case 9: Move to an already existing segment, should succeed but
// return nullopt.
move_start_result =
service_->MoveStart(client_id, key, "segment_2", "segment_3");
EXPECT_TRUE(move_start_result.has_value());
move_response = move_start_result.value();
EXPECT_EQ("segment_2", move_response.source.get_memory_descriptor()
.buffer_descriptor.transport_endpoint_);
EXPECT_FALSE(move_response.target.has_value());
// Test Case 10: Try remove the object, should fail because it is moving.
std::this_thread::sleep_for(std::chrono::milliseconds(kv_lease_ttl * 2));
remove_result = service_->Remove(key);
EXPECT_FALSE(remove_result.has_value());
EXPECT_EQ(ErrorCode::OBJECT_HAS_REPLICATION_TASK, remove_result.error());
// End the move.
move_end_result = service_->MoveEnd(client_id, key);
EXPECT_TRUE(move_end_result.has_value());
// Now the object should have only 1 replica on segment_3.
get_result = service_->GetReplicaList(key);
EXPECT_TRUE(get_result.has_value());
replicas = get_result.value().replicas;
EXPECT_EQ(1, replicas.size());
EXPECT_EQ("segment_3", replicas[0]
.get_memory_descriptor()
.buffer_descriptor.transport_endpoint_);
// Test Case 11: Try remove the object, should succeed after lease expires.
std::this_thread::sleep_for(std::chrono::milliseconds(kv_lease_ttl * 2));
remove_result = service_->Remove(key);
EXPECT_TRUE(remove_result.has_value());
}
TEST_F(MasterServiceTest, MoveEnd) {
std::unique_ptr<MasterService> service_(new MasterService());
// Mount 2 segments (segment_1, segment_2) with
// PrepareSimpleSegment
[[maybe_unused]] const auto context1 =
PrepareSimpleSegment(*service_, "segment_1");
[[maybe_unused]] const auto context2 =
PrepareSimpleSegment(*service_, "segment_2");
UUID client_id = generate_uuid();
UUID invalid_client_id = generate_uuid();
// Test Case 1: MoveEnd a non-existent key, should fail.
auto move_end_result = service_->MoveEnd(client_id, "non_existent_key");
EXPECT_FALSE(move_end_result.has_value());
EXPECT_EQ(ErrorCode::OBJECT_NOT_FOUND, move_end_result.error());
// Put an object with 1 replica and preferred_segment=segment_1 for testing
std::string key = "test_key";
uint64_t slice_length = 1024;
ReplicateConfig config;
config.replica_num = 1;
config.preferred_segment = "segment_1";
auto put_start_result =
service_->PutStart(client_id, key, slice_length, config);
ASSERT_TRUE(put_start_result.has_value());
auto put_end_result = service_->PutEnd(client_id, key, ReplicaType::MEMORY);
ASSERT_TRUE(put_end_result.has_value());
// Test Case 2: MoveEnd the object, should fail because there is no ongoing
// move task.
move_end_result = service_->MoveEnd(client_id, key);
EXPECT_FALSE(move_end_result.has_value());
EXPECT_EQ(ErrorCode::OBJECT_NO_REPLICATION_TASK, move_end_result.error());
// MoveStart the object to segment_2
auto move_start_result =
service_->MoveStart(client_id, key, "segment_1", "segment_2");
ASSERT_TRUE(move_start_result.has_value());
// Test Case 3: MoveEnd with an invalid client id, should fail.
move_end_result = service_->MoveEnd(invalid_client_id, key);
EXPECT_FALSE(move_end_result.has_value());
EXPECT_EQ(ErrorCode::ILLEGAL_CLIENT, move_end_result.error());
// Test Case 4: CopyEnd the object, should fail because the ongoing task is
// Move.
auto copy_end_result = service_->CopyEnd(client_id, key);
EXPECT_FALSE(copy_end_result.has_value());
EXPECT_EQ(ErrorCode::INVALID_PARAMS, copy_end_result.error());
// Test Case 5: MoveEnd, should success.
move_end_result = service_->MoveEnd(client_id, key);
EXPECT_TRUE(move_end_result.has_value());
// Verify we still have 1 replica (the move was successful)
auto get_result = service_->GetReplicaList(key);
EXPECT_TRUE(get_result.has_value());
EXPECT_EQ(1, get_result.value().replicas.size());
// MoveStart the object from segment_2 to segment_1 again, then unmount
// segment_2
move_start_result =
service_->MoveStart(client_id, key, "segment_2", "segment_1");
ASSERT_TRUE(move_start_result.has_value());
// Unmount segment_2 to simulate source gone
auto unmount_result =
service_->UnmountSegment(context2.segment_id, context2.client_id);
ASSERT_TRUE(unmount_result.has_value());
// Test Case 6: MoveEnd, should fail because the source is gone.
move_end_result = service_->MoveEnd(client_id, key);
EXPECT_FALSE(move_end_result.has_value());
EXPECT_EQ(ErrorCode::REPLICA_IS_GONE, move_end_result.error());
}
TEST_F(MasterServiceTest, MoveRevoke) {
std::unique_ptr<MasterService> service_(new MasterService());
// Mount 2 segments (segment_1, segment_2) with PrepareSimpleSegment
[[maybe_unused]] const auto context1 =
PrepareSimpleSegment(*service_, "segment_1");
[[maybe_unused]] const auto context2 =
PrepareSimpleSegment(*service_, "segment_2");
UUID client_id = generate_uuid();
UUID invalid_client_id = generate_uuid();
// Test Case 1: MoveRevoke a non-existent key, should fail.
auto move_revoke_result =
service_->MoveRevoke(client_id, "non_existent_key");
EXPECT_FALSE(move_revoke_result.has_value());
EXPECT_EQ(ErrorCode::OBJECT_NOT_FOUND, move_revoke_result.error());
// Put an object with 1 replica and preferred_segment=segment_1 for testing
std::string key = "test_key";
uint64_t slice_length = 1024;
ReplicateConfig config;
config.replica_num = 1;
config.preferred_segment = "segment_1";
auto put_start_result =
service_->PutStart(client_id, key, slice_length, config);
ASSERT_TRUE(put_start_result.has_value());
auto put_end_result = service_->PutEnd(client_id, key, ReplicaType::MEMORY);
ASSERT_TRUE(put_end_result.has_value());
// Test Case 2: MoveRevoke the object, should fail because there is no
// ongoing move task.
move_revoke_result = service_->MoveRevoke(client_id, key);
EXPECT_FALSE(move_revoke_result.has_value());
EXPECT_EQ(ErrorCode::OBJECT_NO_REPLICATION_TASK,
move_revoke_result.error());
// MoveStart the object from segment_1 to segment_2
auto move_start_result =
service_->MoveStart(client_id, key, "segment_1", "segment_2");
ASSERT_TRUE(move_start_result.has_value());
// Test Case 3: MoveRevoke with an invalid client id, should fail.
move_revoke_result = service_->MoveRevoke(invalid_client_id, key);
EXPECT_FALSE(move_revoke_result.has_value());
EXPECT_EQ(ErrorCode::ILLEGAL_CLIENT, move_revoke_result.error());
// Test Case 4: CopyRevoke the object, should fail because the ongoing task
// is Move.
auto copy_revoke_result = service_->CopyRevoke(client_id, key);
EXPECT_FALSE(copy_revoke_result.has_value());
EXPECT_EQ(ErrorCode::INVALID_PARAMS, copy_revoke_result.error());
// Test Case 5: MoveRevoke, should succeed.
move_revoke_result = service_->MoveRevoke(client_id, key);
EXPECT_TRUE(move_revoke_result.has_value());
// Verify we still have 1 replica (the move was revoked)
auto get_result = service_->GetReplicaList(key);
EXPECT_TRUE(get_result.has_value());
auto& replicas = get_result.value().replicas;
EXPECT_EQ(1, replicas.size());
EXPECT_EQ("segment_1", replicas[0]
.get_memory_descriptor()
.buffer_descriptor.transport_endpoint_);
// MoveStart the object from segment_1 to segment_2 again, then unmount
// segment_1
move_start_result =
service_->MoveStart(client_id, key, "segment_1", "segment_2");
ASSERT_TRUE(move_start_result.has_value());
// Unmount segment_1 to simulate source gone
auto unmount_result =
service_->UnmountSegment(context1.segment_id, context1.client_id);
ASSERT_TRUE(unmount_result.has_value());
// Test Case 6: MoveRevoke, should succeed even though the source is gone.
move_revoke_result = service_->MoveRevoke(client_id, key);
EXPECT_TRUE(move_revoke_result.has_value());
// The object should be erased as there is no replica left.
get_result = service_->GetReplicaList(key);
EXPECT_FALSE(get_result.has_value());
}
TEST_F(MasterServiceTest, ProtectCopyMoveSourceFromEviction) {
const uint64_t kv_lease_ttl = 100;
const uint64_t client_live_ttl = 600;
auto service_config = MasterServiceConfig::builder()
.set_default_kv_lease_ttl(kv_lease_ttl)
.set_client_live_ttl_sec(client_live_ttl)
.build();
std::unique_ptr<MasterService> service_(new MasterService(service_config));
// Mount 2 segments (segment_1, segment_2) with PrepareSimpleSegment, each
// 16 MB
constexpr size_t kBaseAddr = 0x100000000;
constexpr size_t kSegmentSize = 16 * 1024 * 1024; // 16 MB
[[maybe_unused]] const auto context1 =
PrepareSimpleSegment(*service_, "segment_1", kBaseAddr, kSegmentSize);
[[maybe_unused]] const auto context2 =
PrepareSimpleSegment(*service_, "segment_2", kBaseAddr, kSegmentSize);
UUID client_id = generate_uuid();
const std::string copy_key = "copy_key";
const std::string move_key = "move_key";
uint64_t slice_length = 1024 * 1024;
ReplicateConfig config;
config.replica_num = 1;
config.preferred_segment = "segment_1";
// Put two objects for move and copy tests.
auto put_start_result =
service_->PutStart(client_id, copy_key, slice_length, config);
ASSERT_TRUE(put_start_result.has_value());
auto put_end_result =
service_->PutEnd(client_id, copy_key, ReplicaType::MEMORY);
ASSERT_TRUE(put_end_result.has_value());
put_start_result =
service_->PutStart(client_id, move_key, slice_length, config);
ASSERT_TRUE(put_start_result.has_value());
put_end_result = service_->PutEnd(client_id, move_key, ReplicaType::MEMORY);
ASSERT_TRUE(put_end_result.has_value());
// Start copy and move operations.
auto copy_start_result =
service_->CopyStart(client_id, copy_key, "segment_1", {"segment_2"});
ASSERT_TRUE(copy_start_result.has_value());
auto move_start_result =
service_->MoveStart(client_id, move_key, "segment_1", "segment_2");
ASSERT_TRUE(move_start_result.has_value());
// Put more objects to trigger eviction. Do not prefer any segments.
config.preferred_segment = "";
for (size_t i = 0; i < 128 * (kSegmentSize * 2 / slice_length); ++i) {
std::string key = "test_key_" + std::to_string(i);
auto put_start_result =
service_->PutStart(client_id, key, slice_length, config);
if (put_start_result.has_value()) {
auto put_end_result =
service_->PutEnd(client_id, key, ReplicaType::MEMORY);
ASSERT_TRUE(put_end_result.has_value());
} else {
// wait for eviction to work
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
}
// Wait all objects lease expiring and then remove them.
std::this_thread::sleep_for(std::chrono::milliseconds(kv_lease_ttl * 2));
auto remove_all_result = service_->RemoveAll();
ASSERT_TRUE(remove_all_result > 0);
// Try end copy and move operations, should success.
auto copy_end_result = service_->CopyEnd(client_id, copy_key);
EXPECT_TRUE(copy_end_result.has_value());
auto move_end_result = service_->MoveEnd(client_id, move_key);
EXPECT_TRUE(move_end_result.has_value());
}
TEST_F(MasterServiceTest, DiscardTimeoutCopyMove) {
const uint64_t kv_lease_ttl = 100;
const uint64_t client_live_ttl = 600;
const uint64_t put_discard_timeout = 1;
const uint64_t put_release_timeout = 2;
auto service_config =
MasterServiceConfig::builder()
.set_default_kv_lease_ttl(kv_lease_ttl)
.set_client_live_ttl_sec(client_live_ttl)
.set_put_start_discard_timeout_sec(put_discard_timeout)
.set_put_start_release_timeout_sec(put_release_timeout)
.build();
std::unique_ptr<MasterService> service_(new MasterService(service_config));
// Mount 2 segments (segment_1, segment_2) with PrepareSimpleSegment, each
// 16 MB
constexpr size_t kBaseAddr = 0x100000000;
constexpr size_t kSegmentSize = 16 * 1024 * 1024; // 16 MB
[[maybe_unused]] const auto context1 =
PrepareSimpleSegment(*service_, "segment_1", kBaseAddr, kSegmentSize);
[[maybe_unused]] const auto context2 =
PrepareSimpleSegment(*service_, "segment_2", kBaseAddr, kSegmentSize);
UUID client_id = generate_uuid();
const std::string copy_key = "copy_key";
const std::string move_key = "move_key";
uint64_t slice_length = 1024 * 1024;
ReplicateConfig config;
config.replica_num = 1;
config.preferred_segment = "segment_1";
// Put two objects for move and copy tests.
auto put_start_result =
service_->PutStart(client_id, copy_key, slice_length, config);
ASSERT_TRUE(put_start_result.has_value());
auto put_end_result =
service_->PutEnd(client_id, copy_key, ReplicaType::MEMORY);
ASSERT_TRUE(put_end_result.has_value());
put_start_result =
service_->PutStart(client_id, move_key, slice_length, config);
ASSERT_TRUE(put_start_result.has_value());
put_end_result = service_->PutEnd(client_id, move_key, ReplicaType::MEMORY);
ASSERT_TRUE(put_end_result.has_value());
// Start copy and move operations.
auto copy_start_result =
service_->CopyStart(client_id, copy_key, "segment_1", {"segment_2"});
ASSERT_TRUE(copy_start_result.has_value());
auto move_start_result =
service_->MoveStart(client_id, move_key, "segment_1", "segment_2");
ASSERT_TRUE(move_start_result.has_value());
// Wait for the operations timeout.
std::this_thread::sleep_for(std::chrono::seconds(put_release_timeout));
// Put more objects to trigger eviction. Do not prefer any segments.
config.preferred_segment = "";
for (size_t i = 0; i < 128 * (kSegmentSize * 2 / slice_length); ++i) {
std::string key = "test_key_" + std::to_string(i);
auto put_start_result =
service_->PutStart(client_id, key, slice_length, config);
if (put_start_result.has_value()) {
auto put_end_result =
service_->PutEnd(client_id, key, ReplicaType::MEMORY);
ASSERT_TRUE(put_end_result.has_value());
} else {
// wait for eviction to work
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
}
// Try end copy and move operations, should fail because the objects are
// evicted.
auto copy_end_result = service_->CopyEnd(client_id, copy_key);
EXPECT_FALSE(copy_end_result.has_value());
EXPECT_EQ(copy_end_result.error(), ErrorCode::OBJECT_NOT_FOUND);
auto move_end_result = service_->MoveEnd(client_id, move_key);
EXPECT_FALSE(move_end_result.has_value());
EXPECT_EQ(move_end_result.error(), ErrorCode::OBJECT_NOT_FOUND);
}
TEST_F(MasterServiceTest, RemoveByRegexComplex) {
const uint64_t kv_lease_ttl = 100;
auto service_config = MasterServiceConfig::builder()
.set_default_kv_lease_ttl(kv_lease_ttl)
.build();
auto service_ = std::make_unique<MasterService>(service_config);
const UUID client_id = generate_uuid();
// 1. Mount segment
[[maybe_unused]] const auto context =
PrepareSimpleSegment(*service_, "test_segment_remove");
// A helper lambda to repopulate the store for each test case
auto populate_store = [&]() {
std::vector<std::string> keys_to_put = {
"test_key_01",
"test_key_02",
"test_key_10",
"prod_key_alpha",
"prod_key_beta",
"data_part_1_chunk_a",
"data_part_2_chunk_b",
"config/user/settings.json",
"logs/app-2025-08-13.log",
"short",
"a_very_very_very_long_key_that_tests_length_limits",
"test-key-extra",
"another_key"};
for (const auto& key : keys_to_put) {
put_object(*service_, client_id, key);
}
// Wait for potential lease propagation
std::this_thread::sleep_for(std::chrono::milliseconds(kv_lease_ttl));
};
// --- Test Case 1: Remove a specific subset and verify ---
{
SCOPED_TRACE("Test Case 1: Removing keys with prefix 'test_key_'");
populate_store();
// Action: Remove keys starting with "test_key_"
auto remove_result = service_->RemoveByRegex("^test_key_");
ASSERT_TRUE(remove_result.has_value());
EXPECT_EQ(remove_result.value(), 3); // Should remove 3 keys
// Verification: Check which keys were deleted and which remain
std::vector<std::string> deleted_keys = {"test_key_01", "test_key_02",
"test_key_10"};
for (const auto& key : deleted_keys) {
auto exist_result = service_->ExistKey(key);
ASSERT_TRUE(exist_result.has_value());
EXPECT_FALSE(exist_result.value())
<< "Key " << key << " should have been deleted.";
}
std::vector<std::string> remaining_keys = {
"prod_key_alpha", "short", "test-key-extra"}; // Sample a few
for (const auto& key : remaining_keys) {
auto exist_result = service_->ExistKey(key);
ASSERT_TRUE(exist_result.has_value());
EXPECT_TRUE(exist_result.value())
<< "Key " << key << " should NOT have been deleted.";
}
}
// --- Test Case 2: Remove everything ---
{
SCOPED_TRACE("Test Case 2: Removing all keys with '.*'");
// Store is already populated from the previous (failed) test run, or we
// can repopulate For isolation, let's assume we start fresh
auto service_config = MasterServiceConfig::builder()
.set_default_kv_lease_ttl(kv_lease_ttl)
.build();
service_ = std::make_unique<MasterService>(service_config);
[[maybe_unused]] const auto context_reset =
PrepareSimpleSegment(*service_, "test_segment_remove");
populate_store();
size_t total_keys = 13; // Count from the keys_to_put vector
// Action: Remove all keys
auto remove_result = service_->RemoveByRegex(".*");
ASSERT_TRUE(remove_result.has_value());
EXPECT_EQ(remove_result.value(), total_keys);
// Verification: Check that no keys remain
auto get_all_result = service_->GetReplicaListByRegex(".*");
ASSERT_TRUE(get_all_result.has_value());
EXPECT_TRUE(get_all_result.value().empty());
}
// --- Test Case 3: Attempt to remove with a non-matching pattern ---
{
SCOPED_TRACE("Test Case 3: Removing with a non-matching pattern");
auto service_config = MasterServiceConfig::builder()
.set_default_kv_lease_ttl(kv_lease_ttl)
.build();
service_ = std::make_unique<MasterService>(
service_config); // Reset the service
[[maybe_unused]] const auto context_reset =
PrepareSimpleSegment(*service_, "test_segment_remove");
populate_store();
size_t total_keys_before_remove = 13;
// Action: Attempt to remove using a pattern that matches nothing
auto remove_result = service_->RemoveByRegex("^nonexistent-pattern-");
ASSERT_TRUE(remove_result.has_value());
EXPECT_EQ(remove_result.value(), 0); // Should remove 0 keys
// Verification: Check that all keys still exist
auto get_all_result = service_->GetReplicaListByRegex(".*");
ASSERT_TRUE(get_all_result.has_value());
EXPECT_EQ(get_all_result.value().size(), total_keys_before_remove);
}
// --- Test Case 4: Remove based on a complex pattern and verify ---
{
SCOPED_TRACE(
"Test Case 4: Removing based on file paths or containing digits");
auto service_config = MasterServiceConfig::builder()
.set_default_kv_lease_ttl(kv_lease_ttl)
.build();
service_ = std::make_unique<MasterService>(service_config); // Reset
[[maybe_unused]] const auto context_reset =
PrepareSimpleSegment(*service_, "test_segment_remove");
populate_store();
// Action: Remove all keys that contain a slash '/' OR end with a number
auto remove_result = service_->RemoveByRegex("/|\\d$");
ASSERT_TRUE(remove_result.has_value());
// Matches: "config/user/settings.json", "logs/app-2025-08-13.log",
// "test_key_01", "test_key_02", "test_key_10"
// Note: logs/app-2025-08-13.log matches both, but is counted once.
EXPECT_EQ(remove_result.value(),
5); // The two paths + test_key_01 and test_key_02.
// (test_key_10 ends with 0) wait, no, 10 ends with 0.
// Ah, \d$ matches a single digit at the end. So test_key_01,
// test_key_02. test_key_10 does NOT match \d$. Let's refine the regex.
}
// --- Test Case 4 ---
{
SCOPED_TRACE(
"Test Case 4 (Corrected): Removing based on complex pattern");
auto service_config = MasterServiceConfig::builder()
.set_default_kv_lease_ttl(kv_lease_ttl)
.build();
service_ = std::make_unique<MasterService>(service_config); // Reset
[[maybe_unused]] const auto context_reset =
PrepareSimpleSegment(*service_, "test_segment_remove");
populate_store();
// Action: Remove all keys that contain "chunk" OR "config"
auto remove_result = service_->RemoveByRegex("chunk|config");
ASSERT_TRUE(remove_result.has_value());
// Matches: "data_part_1_chunk_a", "data_part_2_chunk_b",
// "config/user/settings.json"
EXPECT_EQ(remove_result.value(), 3);
// Verification
auto exist_result_chunk = service_->ExistKey("data_part_1_chunk_a");
ASSERT_TRUE(exist_result_chunk.has_value());
EXPECT_FALSE(exist_result_chunk.value());
auto exist_result_config =
service_->ExistKey("config/user/settings.json");
ASSERT_TRUE(exist_result_config.has_value());
EXPECT_FALSE(exist_result_config.value());
auto exist_result_untouched = service_->ExistKey("prod_key_alpha");
ASSERT_TRUE(exist_result_untouched.has_value());
EXPECT_TRUE(exist_result_untouched.value());
}
}
TEST_F(MasterServiceTest, RemoveAll) {
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));
[[maybe_unused]] const auto context = PrepareSimpleSegment(*service_);
const UUID client_id = generate_uuid();
int times = 10;
while (times--) {
std::string key = "test_key" + std::to_string(times);
uint64_t value_length = 1024;
ReplicateConfig config;
config.replica_num = 1;
auto put_start_result =
service_->PutStart(client_id, key, value_length, config);
ASSERT_TRUE(put_start_result.has_value());
auto put_end_result =
service_->PutEnd(client_id, key, ReplicaType::MEMORY);
ASSERT_TRUE(put_end_result.has_value());
auto exist_result = service_->ExistKey(key);
ASSERT_TRUE(exist_result.has_value());
}
// wait for all the lease to expire
std::this_thread::sleep_for(std::chrono::milliseconds(kv_lease_ttl));
ASSERT_EQ(10, service_->RemoveAll());
times = 10;
while (times--) {
std::string key = "test_key" + std::to_string(times);
auto exist_result = service_->ExistKey(key);
ASSERT_TRUE(exist_result.has_value());
ASSERT_FALSE(exist_result.value());
}
}
TEST_F(MasterServiceTest, SingleSliceMultiReplicaFlow) {
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));
const UUID client_id = generate_uuid();
// Mount 3 segments, each 64MB
constexpr size_t kBaseAddr = 0x300000000;
constexpr size_t kSegmentSize = 1024 * 1024 * 64; // 64MB
for (int i = 0; i < 3; ++i) {
[[maybe_unused]] const auto context = PrepareSimpleSegment(
*service_, "segment_" + std::to_string(i),
kBaseAddr + static_cast<size_t>(i) * kSegmentSize, kSegmentSize);
}
// Test parameters
std::string key = "multi_slice_object";
constexpr size_t num_replicas = 3;
constexpr size_t slice_length = 1024 * 1024 * 5; // 5MB
// Configure replication
ReplicateConfig config;
config.replica_num = num_replicas;
std::vector<Replica::Descriptor> replica_list;
// Test PutStart with multiple slices and replicas
auto put_start_result =
service_->PutStart(client_id, key, slice_length, config);
ASSERT_TRUE(put_start_result.has_value());
replica_list = put_start_result.value();
// Verify replica list properties
ASSERT_EQ(num_replicas, replica_list.size());
for (const auto& replica : replica_list) {
// Verify replica status
EXPECT_EQ(ReplicaStatus::PROCESSING, replica.status);
// Verify slice length matches buffer descriptor
EXPECT_EQ(slice_length,
replica.get_memory_descriptor().buffer_descriptor.size_);
}
// Test GetReplicaList during processing (should fail)
auto get_result = service_->GetReplicaList(key);
EXPECT_FALSE(get_result.has_value());
EXPECT_EQ(ErrorCode::REPLICA_IS_NOT_READY, get_result.error());
// Complete the put operation
auto put_end_result = service_->PutEnd(client_id, key, ReplicaType::MEMORY);
ASSERT_TRUE(put_end_result.has_value());
// Test GetReplicaList after completion
auto get_result2 = service_->GetReplicaList(key);
ASSERT_TRUE(get_result2.has_value());
auto retrieved_replicas = get_result2.value().replicas;
ASSERT_EQ(num_replicas, retrieved_replicas.size());
// Verify final state of all replicas
for (const auto& replica : retrieved_replicas) {
EXPECT_EQ(ReplicaStatus::COMPLETE, replica.status);
ASSERT_EQ(slice_length,
replica.get_memory_descriptor().buffer_descriptor.size_);
}
}
TEST_F(MasterServiceTest, CleanupStaleHandlesTest) {
std::unique_ptr<MasterService> service_(new MasterService());
// Mount a segment for testing
constexpr size_t buffer = 0x300000000;
constexpr size_t size = 1024 * 1024 * 16; // 16MB
auto segment = MakeSegment("test_segment", buffer, size);
UUID client_id = generate_uuid();
// Mount the segment
auto mount_result = service_->MountSegment(segment, client_id);
ASSERT_TRUE(mount_result.has_value());
// Create an object that will be stored in the segment
std::string key = "segment_object";
uint64_t slice_length = 1024 * 1024; // One 1MB slice
ReplicateConfig config;
config.replica_num = 1; // One replica
// Create the object
auto put_start_result =
service_->PutStart(client_id, key, slice_length, config);
ASSERT_TRUE(put_start_result.has_value());
auto put_end_result = service_->PutEnd(client_id, key, ReplicaType::MEMORY);
ASSERT_TRUE(put_end_result.has_value());
// Verify object exists
auto get_result = service_->GetReplicaList(key);
ASSERT_TRUE(get_result.has_value());
auto retrieved_replicas = get_result.value().replicas;
ASSERT_EQ(1, retrieved_replicas.size());
// Unmount the segment
auto unmount_result1 = service_->UnmountSegment(segment.id, client_id);
ASSERT_TRUE(unmount_result1.has_value());
// Try to get the object - it should be automatically removed since the
// replica is invalid
auto get_result2 = service_->GetReplicaList(key);
EXPECT_FALSE(get_result2.has_value());
EXPECT_EQ(ErrorCode::OBJECT_NOT_FOUND, get_result2.error());
// Mount the segment again
mount_result = service_->MountSegment(segment, client_id);
ASSERT_TRUE(mount_result.has_value());
// Create another object
std::string key2 = "another_segment_object";
auto put_start_result2 =
service_->PutStart(client_id, key2, slice_length, config);
ASSERT_TRUE(put_start_result2.has_value());
auto put_end_result2 =
service_->PutEnd(client_id, key2, ReplicaType::MEMORY);
ASSERT_TRUE(put_end_result2.has_value());
// Verify we can get it
auto get_result3 = service_->GetReplicaList(key2);
ASSERT_TRUE(get_result3.has_value());
// Unmount the segment
auto unmount_result2 = service_->UnmountSegment(segment.id, client_id);
ASSERT_TRUE(unmount_result2.has_value());
// Try to remove the object that should already be cleaned up
auto remove_result = service_->Remove(key2);
EXPECT_FALSE(remove_result.has_value());
EXPECT_EQ(ErrorCode::OBJECT_NOT_FOUND, remove_result.error());
}
TEST_F(MasterServiceTest, ConcurrentWriteAndRemoveAll) {
std::unique_ptr<MasterService> service_(new MasterService());
constexpr size_t buffer = 0x300000000;
constexpr size_t size = 1024 * 1024 * 256; // 256MB for concurrent testing
auto segment = MakeSegment("concurrent_segment", buffer, size);
UUID client_id = generate_uuid();
auto mount_result_concurrent = service_->MountSegment(segment, client_id);
ASSERT_TRUE(mount_result_concurrent.has_value());
constexpr int num_threads = 4;
constexpr int objects_per_thread = 100;
std::atomic success_writes(0);
std::atomic remove_all_done(false);
std::atomic total_removed(0);
// Writer threads
std::vector<std::thread> writers;
for (int i = 0; i < num_threads; ++i) {
writers.emplace_back([&, i]() {
for (int j = 0; j < objects_per_thread; ++j) {
std::string key =
"key_" + std::to_string(i) + "_" + std::to_string(j);
uint64_t slice_length = 1024;
ReplicateConfig config;
config.replica_num = 1;
std::vector<Replica::Descriptor> replica_list;
auto put_start_result =
service_->PutStart(client_id, key, slice_length, config);
if (put_start_result.has_value()) {
auto put_end_result =
service_->PutEnd(client_id, key, ReplicaType::MEMORY);
if (put_end_result.has_value()) {
success_writes++;
}
}
// Random sleep to increase concurrency complexity
std::this_thread::sleep_for(
std::chrono::milliseconds(rand() % 10));
}
});
}
// RemoveAll thread
std::thread remove_thread([&]() {
std::this_thread::sleep_for(
std::chrono::milliseconds(50)); // Let some writes start
long removed = service_->RemoveAll();
LOG(INFO) << "Removed " << removed
<< " objects during concurrent writes";
ASSERT_GT(removed, 0);
remove_all_done = true;
total_removed.fetch_add(removed);
});
// Join all threads
for (auto& t : writers) {
t.join();
}
remove_thread.join();
// Verify results
EXPECT_GT(success_writes, 0);
EXPECT_TRUE(remove_all_done);
// Final RemoveAll to ensure clean state
long final_removed = service_->RemoveAll();
LOG(INFO) << "Final RemoveAll removed " << final_removed << " objects";
ASSERT_GT(final_removed, 0);
total_removed.fetch_add(final_removed);
ASSERT_EQ(total_removed, num_threads * objects_per_thread);
}
TEST_F(MasterServiceTest, ConcurrentReadAndRemoveAll) {
// set a large kv_lease_ttl so the granted lease will not quickly expire
const uint64_t kv_lease_ttl = 200;
auto service_config = MasterServiceConfig::builder()
.set_default_kv_lease_ttl(kv_lease_ttl)
.build();
std::unique_ptr<MasterService> service_(new MasterService(service_config));
constexpr size_t buffer = 0x300000000;
constexpr size_t size = 1024 * 1024 * 256; // 256MB for concurrent testing
auto segment = MakeSegment("concurrent_segment", buffer, size);
UUID client_id = generate_uuid();
auto mount_result = service_->MountSegment(segment, client_id);
ASSERT_TRUE(mount_result.has_value());
// Pre-populate with test data
constexpr int num_objects = 1000;
for (int i = 0; i < num_objects; ++i) {
std::string key = "pre_key_" + std::to_string(i);
uint64_t slice_length = 1024;
ReplicateConfig config;
config.replica_num = 1;
auto put_start_result =
service_->PutStart(client_id, key, slice_length, config);
ASSERT_TRUE(put_start_result.has_value());
auto put_end_result =
service_->PutEnd(client_id, key, ReplicaType::MEMORY);
ASSERT_TRUE(put_end_result.has_value());
}
std::atomic<int> success_reads(0);
std::atomic<bool> remove_all_done(false);
// Reader threads
std::vector<std::thread> readers;
for (int i = 0; i < 4; ++i) {
readers.emplace_back([&]() {
for (int j = 0; j < num_objects; ++j) {
std::string key = "pre_key_" + std::to_string(j);
auto get_result = service_->GetReplicaList(key);
if (get_result.has_value()) {
success_reads++;
}
// Random sleep to increase concurrency complexity
std::this_thread::sleep_for(
std::chrono::milliseconds(rand() % 5));
}
});
}
// RemoveAll thread
std::thread remove_thread([&]() {
std::this_thread::sleep_for(
std::chrono::milliseconds(10)); // Let some reads start
long removed = service_->RemoveAll();
LOG(INFO) << "Removed " << removed
<< " objects during concurrent reads";
remove_all_done = true;
});
// Join all threads
for (auto& t : readers) {
t.join();
}
remove_thread.join();
EXPECT_TRUE(remove_all_done);
// Verify 0 < success_reads < num_objects
EXPECT_GT(success_reads, 0);
EXPECT_NE(success_reads, num_objects);
// wait for all the lease to expire
std::this_thread::sleep_for(std::chrono::milliseconds(kv_lease_ttl));
long removed = service_->RemoveAll();
LOG(INFO) << "Removed " << removed << " objects after kv lease expired";
// Verify all objects were removed
for (int i = 0; i < num_objects; ++i) {
std::string key = "pre_key_" + std::to_string(i);
auto get_result = service_->GetReplicaList(key);
EXPECT_FALSE(get_result.has_value());
EXPECT_EQ(ErrorCode::OBJECT_NOT_FOUND, get_result.error());
}
}
TEST_F(MasterServiceTest, ConcurrentRemoveAllOperations) {
std::unique_ptr<MasterService> service_(new MasterService());
constexpr size_t buffer = 0x300000000;
constexpr size_t size = 1024 * 1024 * 16 * 100;
auto segment = MakeSegment("concurrent_segment", buffer, size);
UUID client_id = generate_uuid();
auto mount_result = service_->MountSegment(segment, client_id);
ASSERT_TRUE(mount_result.has_value());
// Pre-populate with test data
constexpr int num_objects = 1000;
for (int i = 0; i < num_objects; ++i) {
std::string key = "pre_key_" + std::to_string(i);
uint64_t slice_length = 1024;
ReplicateConfig config;
config.replica_num = 1;
auto put_start_result =
service_->PutStart(client_id, key, slice_length, config);
ASSERT_TRUE(put_start_result.has_value());
auto put_end_result =
service_->PutEnd(client_id, key, ReplicaType::MEMORY);
ASSERT_TRUE(put_end_result.has_value());
}
std::atomic<int> remove_all_count(0);
// Two RemoveAll threads
std::vector<std::thread> remove_threads;
for (int i = 0; i < 2; ++i) {
remove_threads.emplace_back([&]() {
long removed = service_->RemoveAll();
LOG(INFO) << "RemoveAll removed " << removed << " objects";
remove_all_count += removed;
});
}
// Join all threads
for (auto& t : remove_threads) {
t.join();
}
// Verify results - one RemoveAll should return num_objects, the other 0
EXPECT_EQ(num_objects, remove_all_count);
// Verify all objects were removed
for (int i = 0; i < num_objects; ++i) {
std::string key = "pre_key_" + std::to_string(i);
auto get_result = service_->GetReplicaList(key);
EXPECT_FALSE(get_result.has_value());
EXPECT_EQ(ErrorCode::OBJECT_NOT_FOUND, get_result.error());
}
}
TEST_F(MasterServiceTest, UnmountSegmentImmediateCleanup) {
std::unique_ptr<MasterService> service_(new MasterService());
// Mount two segments for testing
constexpr size_t buffer1 = 0x300000000;
constexpr size_t buffer2 = 0x400000000;
constexpr size_t size = 1024 * 1024 * 16;
auto segment1 = MakeSegment("segment1", buffer1, size);
auto segment2 = MakeSegment("segment2", buffer2, size);
UUID client_id = generate_uuid();
auto mount_result1 = service_->MountSegment(segment1, client_id);
ASSERT_TRUE(mount_result1.has_value());
auto mount_result2 = service_->MountSegment(segment2, client_id);
ASSERT_TRUE(mount_result2.has_value());
// Create two objects in the two segments
std::string key1 =
GenerateKeyForSegment(client_id, service_, segment1.name);
std::string key2 =
GenerateKeyForSegment(client_id, service_, segment2.name);
uint64_t slice_length = 1024;
ReplicateConfig config;
config.replica_num = 1;
// Unmount segment1
auto unmount_result1 = service_->UnmountSegment(segment1.id, client_id);
ASSERT_TRUE(unmount_result1.has_value());
// Umount will remove all objects in the segment, include the key1
ASSERT_EQ(1, service_->GetKeyCount());
// Verify objects in segment1 is gone
auto get_result1 = service_->GetReplicaList(key1);
ASSERT_FALSE(get_result1.has_value());
ASSERT_EQ(ErrorCode::OBJECT_NOT_FOUND, get_result1.error());
// Verify objects in segment2 is still there
auto get_result2 = service_->GetReplicaList(key2);
ASSERT_TRUE(get_result2.has_value());
// Verify put key1 will put into segment2 rather than segment1
auto put_start_result =
service_->PutStart(client_id, key1, slice_length, config);
ASSERT_TRUE(put_start_result.has_value());
replica_list = put_start_result.value();
auto put_end_result =
service_->PutEnd(client_id, key1, ReplicaType::MEMORY);
ASSERT_TRUE(put_end_result.has_value());
auto get_result3 = service_->GetReplicaList(key1);
ASSERT_TRUE(get_result3.has_value());
auto retrieved = get_result3.value();
ASSERT_EQ(replica_list[0]
.get_memory_descriptor()
.buffer_descriptor.transport_endpoint_,
segment2.name);
}
TEST_F(MasterServiceTest, ReadableAfterPartialUnmountWithReplication) {
std::unique_ptr<MasterService> service_(new MasterService());
// Mount two large segments
constexpr size_t buffer1 = 0x300000000;
constexpr size_t buffer2 = 0x400000000;
constexpr size_t segment_size = 1024 * 1024 * 64; // 64MB
constexpr size_t object_size = 1024 * 1024; // 1MB
auto segment1 = MakeSegment("segment1", buffer1, segment_size);
auto segment2 = MakeSegment("segment2", buffer2, segment_size);
UUID client_id = generate_uuid();
auto mount_result1 = service_->MountSegment(segment1, client_id);
ASSERT_TRUE(mount_result1.has_value());
auto mount_result2 = service_->MountSegment(segment2, client_id);
ASSERT_TRUE(mount_result2.has_value());
// Put a key with 2 replicas
std::string key = "replicated_key";
uint64_t slice_length = object_size;
ReplicateConfig config;
config.replica_num = 2;
auto put_start_result =
service_->PutStart(client_id, key, slice_length, config);
ASSERT_TRUE(put_start_result.has_value());
ASSERT_EQ(2u, put_start_result->size());
ASSERT_TRUE(
service_->PutEnd(client_id, key, ReplicaType::MEMORY).has_value());
// Verify two replicas exist and they are on distinct segments
auto get_result = service_->GetReplicaList(key);
ASSERT_TRUE(get_result.has_value());
auto replicas = get_result.value().replicas;
ASSERT_EQ(2u, replicas.size());
std::unordered_set<std::string> seg_names;
for (const auto& rep : replicas) {
ASSERT_EQ(ReplicaStatus::COMPLETE, rep.status);
const auto& mem = rep.get_memory_descriptor();
ASSERT_EQ(slice_length, mem.buffer_descriptor.size_);
seg_names.insert(mem.buffer_descriptor.transport_endpoint_);
}
ASSERT_EQ(2u, seg_names.size())
<< "Replicas should be on different segments";
// Unmount one segment
ASSERT_TRUE(service_->UnmountSegment(segment1.id, client_id).has_value());
// Key should still be readable via the remaining replica
auto get_after_unmount = service_->GetReplicaList(key);
ASSERT_TRUE(get_after_unmount.has_value())
<< "Object should remain accessible with surviving replica";
}
TEST_F(MasterServiceTest, UnmountSegmentPerformance) {
std::unique_ptr<MasterService> service_(new MasterService());
constexpr size_t kBufferAddress = 0x300000000;
constexpr size_t kSegmentSize = 1024 * 1024 * 256; // 256MB
std::string segment_name = "perf_test_segment";
auto segment = MakeSegment(segment_name, kBufferAddress, kSegmentSize);
UUID client_id = generate_uuid();
// Mount a segment for testing
auto mount_result = service_->MountSegment(segment, client_id);
ASSERT_TRUE(mount_result.has_value());
// Create 10000 keys for testing
constexpr int kNumKeys = 1000;
std::vector<std::string> keys;
keys.reserve(kNumKeys);
auto start = std::chrono::steady_clock::now();
// Create `kNumKeys` keys
for (int i = 0; i < kNumKeys; ++i) {
std::string key =
GenerateKeyForSegment(client_id, service_, segment_name);
keys.push_back(key);
}
auto create_end = std::chrono::steady_clock::now();
// Execute unmount operation and record operation time
auto unmount_start = std::chrono::steady_clock::now();
auto unmount_result = service_->UnmountSegment(segment.id, client_id);
EXPECT_TRUE(unmount_result.has_value());
auto unmount_end = std::chrono::steady_clock::now();
auto unmount_duration =
std::chrono::duration_cast<std::chrono::milliseconds>(unmount_end -
unmount_start);
// Unmount operation should be very fast, so we set 1s limit
EXPECT_LE(unmount_duration.count(), 1000)
<< "Unmount operation took " << unmount_duration.count()
<< "ms which exceeds 1 second limit";
// Verify all keys are gone
for (const auto& key : keys) {
auto get_result = service_->GetReplicaList(key);
EXPECT_FALSE(get_result.has_value());
EXPECT_EQ(ErrorCode::OBJECT_NOT_FOUND, get_result.error());
}
// Output performance report
auto total_create_duration =
std::chrono::duration_cast<std::chrono::milliseconds>(create_end -
start);
std::cout << "\nPerformance Metrics:\n"
<< "Keys created: " << kNumKeys << "\n"
<< "Creation time: " << total_create_duration.count() << "ms\n"
<< "Unmount time: " << unmount_duration.count() << "ms\n";
}
TEST_F(MasterServiceTest, RemoveLeasedObject) {
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));
[[maybe_unused]] const auto context = PrepareSimpleSegment(*service_);
const UUID client_id = generate_uuid();
std::string key = "test_key";
uint64_t slice_length = 1024;
ReplicateConfig config;
config.replica_num = 1;
// Verify lease is granted on ExistsKey
auto put_start_result =
service_->PutStart(client_id, key, slice_length, config);
ASSERT_TRUE(put_start_result.has_value());
auto put_end_result = service_->PutEnd(client_id, key, ReplicaType::MEMORY);
ASSERT_TRUE(put_end_result.has_value());
auto exist_result = service_->ExistKey(key);
ASSERT_TRUE(exist_result.has_value());
auto remove_result = service_->Remove(key);
EXPECT_FALSE(remove_result.has_value());
EXPECT_EQ(ErrorCode::OBJECT_HAS_LEASE, remove_result.error());
std::this_thread::sleep_for(std::chrono::milliseconds(kv_lease_ttl));
auto remove_result2 = service_->Remove(key);
EXPECT_TRUE(remove_result2.has_value());
// Verify lease is extended on successive ExistsKey
auto put_start_result2 =
service_->PutStart(client_id, key, slice_length, config);
ASSERT_TRUE(put_start_result2.has_value());
auto put_end_result2 =
service_->PutEnd(client_id, key, ReplicaType::MEMORY);
ASSERT_TRUE(put_end_result2.has_value());
auto exist_result2 = service_->ExistKey(key);
ASSERT_TRUE(exist_result2.has_value());
std::this_thread::sleep_for(std::chrono::milliseconds(kv_lease_ttl));
auto exist_result3 = service_->ExistKey(key);
ASSERT_TRUE(exist_result3.has_value());
auto remove_result3 = service_->Remove(key);
EXPECT_FALSE(remove_result3.has_value());
EXPECT_EQ(ErrorCode::OBJECT_HAS_LEASE, remove_result3.error());
std::this_thread::sleep_for(std::chrono::milliseconds(kv_lease_ttl));
auto remove_result4 = service_->Remove(key);
EXPECT_TRUE(remove_result4.has_value());
// Verify lease is granted on GetReplicaList
auto put_start_result3 =
service_->PutStart(client_id, key, slice_length, config);
ASSERT_TRUE(put_start_result3.has_value());
auto put_end_result3 =
service_->PutEnd(client_id, key, ReplicaType::MEMORY);
ASSERT_TRUE(put_end_result3.has_value());
auto get_result = service_->GetReplicaList(key);
ASSERT_TRUE(get_result.has_value());
auto remove_result5 = service_->Remove(key);
EXPECT_FALSE(remove_result5.has_value());
EXPECT_EQ(ErrorCode::OBJECT_HAS_LEASE, remove_result5.error());
std::this_thread::sleep_for(std::chrono::milliseconds(kv_lease_ttl));
auto remove_result6 = service_->Remove(key);
EXPECT_TRUE(remove_result6.has_value());
// Verify lease is extended on successive GetReplicaList
auto put_start_result4 =
service_->PutStart(client_id, key, slice_length, config);
ASSERT_TRUE(put_start_result4.has_value());
auto put_end_result4 =
service_->PutEnd(client_id, key, ReplicaType::MEMORY);
ASSERT_TRUE(put_end_result4.has_value());
auto get_result2 = service_->GetReplicaList(key);
ASSERT_TRUE(get_result2.has_value());
std::this_thread::sleep_for(std::chrono::milliseconds(kv_lease_ttl));
auto get_result3 = service_->GetReplicaList(key);
ASSERT_TRUE(get_result3.has_value());
auto remove_result7 = service_->Remove(key);
EXPECT_FALSE(remove_result7.has_value());
EXPECT_EQ(ErrorCode::OBJECT_HAS_LEASE, remove_result7.error());
std::this_thread::sleep_for(std::chrono::milliseconds(kv_lease_ttl));
auto remove_result8 = service_->Remove(key);
EXPECT_TRUE(remove_result8.has_value());
// Verify object is removed
auto get_result4 = service_->GetReplicaList(key);
EXPECT_FALSE(get_result4.has_value());
EXPECT_EQ(ErrorCode::OBJECT_NOT_FOUND, get_result4.error());
}
TEST_F(MasterServiceTest, RemoveAllLeasedObject) {
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));
[[maybe_unused]] const auto context = PrepareSimpleSegment(*service_);
const UUID client_id = generate_uuid();
for (int i = 0; i < 10; ++i) {
std::string key = "test_key" + std::to_string(i);
uint64_t slice_length = 1024;
ReplicateConfig config;
config.replica_num = 1;
auto put_start_result =
service_->PutStart(client_id, key, slice_length, config);
ASSERT_TRUE(put_start_result.has_value());
auto put_end_result =
service_->PutEnd(client_id, key, ReplicaType::MEMORY);
ASSERT_TRUE(put_end_result.has_value());
if (i >= 5) {
auto exist_result = service_->ExistKey(key);
ASSERT_TRUE(exist_result.has_value());
}
}
ASSERT_EQ(5, service_->RemoveAll());
for (int i = 0; i < 5; ++i) {
std::string key = "test_key" + std::to_string(i);
auto exist_result = service_->ExistKey(key);
ASSERT_FALSE(exist_result.value());
}
// wait for all the lease to expire
std::this_thread::sleep_for(std::chrono::milliseconds(kv_lease_ttl));
ASSERT_EQ(5, service_->RemoveAll());
for (int i = 5; i < 10; ++i) {
std::string key = "test_key" + std::to_string(i);
auto exist_result = service_->ExistKey(key);
ASSERT_FALSE(exist_result.value());
}
}
TEST_F(MasterServiceTest, EvictObject) {
// set a large kv_lease_ttl so the granted lease will not quickly expire
const uint64_t kv_lease_ttl = 2000;
auto service_config = MasterServiceConfig::builder()
.set_default_kv_lease_ttl(kv_lease_ttl)
.build();
std::unique_ptr<MasterService> service_(new MasterService(service_config));
const UUID client_id = generate_uuid();
// Mount a segment that can hold about 1024 * 16 objects.
// As the eviction is processed separately for each shard,
// we need to fill each shard with enough objects to thoroughly
// test the eviction process.
constexpr size_t buffer = 0x300000000;
constexpr size_t size = 1024 * 1024 * 16 * 15;
constexpr size_t object_size = 1024 * 15;
[[maybe_unused]] const auto context =
PrepareSimpleSegment(*service_, "test_segment", buffer, size);
// Verify if we can put objects more than the segment can hold
int success_puts = 0;
for (int i = 0; i < 1024 * 16 + 50; ++i) {
std::string key = "test_key" + std::to_string(i);
uint64_t slice_length = object_size;
ReplicateConfig config;
config.replica_num = 1;
auto put_start_result =
service_->PutStart(client_id, key, slice_length, config);
if (put_start_result.has_value()) {
auto put_end_result =
service_->PutEnd(client_id, key, ReplicaType::MEMORY);
ASSERT_TRUE(put_end_result.has_value());
success_puts++;
} else {
// wait for eviction to work
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
}
ASSERT_GT(success_puts, 1024 * 16);
std::this_thread::sleep_for(std::chrono::milliseconds(kv_lease_ttl));
service_->RemoveAll();
}
TEST_F(MasterServiceTest, TryEvictLeasedObject) {
// set a large kv_lease_ttl so the granted lease will not quickly expire
const uint64_t kv_lease_ttl = 500;
auto service_config = MasterServiceConfig::builder()
.set_default_kv_lease_ttl(kv_lease_ttl)
.build();
std::unique_ptr<MasterService> service_(new MasterService(service_config));
const UUID client_id = generate_uuid();
constexpr size_t buffer = 0x300000000;
constexpr size_t size = 1024 * 1024 * 16;
constexpr size_t object_size = 1024 * 1024;
[[maybe_unused]] const auto context =
PrepareSimpleSegment(*service_, "test_segment", buffer, size);
// Verify leased object will not be evicted.
int success_puts = 0;
int failed_puts = 0;
std::vector<std::string> leased_keys;
for (int i = 0; i < 16 + 10; ++i) {
std::string key = "test_key" + std::to_string(i);
uint64_t slice_length = object_size;
ReplicateConfig config;
config.replica_num = 1;
auto put_start_result =
service_->PutStart(client_id, key, slice_length, config);
if (put_start_result.has_value()) {
auto put_end_result =
service_->PutEnd(client_id, key, ReplicaType::MEMORY);
ASSERT_TRUE(put_end_result.has_value());
// the object is leased
auto get_result = service_->GetReplicaList(key);
ASSERT_TRUE(get_result.has_value());
leased_keys.push_back(key);
success_puts++;
} else {
failed_puts++;
}
}
ASSERT_GT(success_puts, 0);
ASSERT_GT(failed_puts, 0);
// wait for eviction to do eviction
std::this_thread::sleep_for(std::chrono::milliseconds(50));
// All leased objects should be accessible
for (const auto& key : leased_keys) {
auto get_result = service_->GetReplicaList(key);
ASSERT_TRUE(get_result.has_value());
}
std::this_thread::sleep_for(std::chrono::milliseconds(kv_lease_ttl));
service_->RemoveAll();
}
TEST_F(MasterServiceTest, RemoveSoftPinObject) {
const uint64_t kv_lease_ttl = 200;
// set a large soft_pin_ttl so the granted soft pin will not quickly expire
const uint64_t kv_soft_pin_ttl = 10000;
const bool allow_evict_soft_pinned_objects = true;
auto service_config = MasterServiceConfig::builder()
.set_default_kv_lease_ttl(kv_lease_ttl)
.set_default_kv_soft_pin_ttl(kv_soft_pin_ttl)
.set_allow_evict_soft_pinned_objects(
allow_evict_soft_pinned_objects)
.build();
std::unique_ptr<MasterService> service_(new MasterService(service_config));
const UUID client_id = generate_uuid();
// Mount segment and put an object
constexpr size_t buffer = 0x300000000;
constexpr size_t size = 1024 * 1024 * 16;
[[maybe_unused]] const auto context =
PrepareSimpleSegment(*service_, "test_segment", buffer, size);
std::string key = "test_key";
uint64_t slice_length = 1024;
ReplicateConfig config;
config.replica_num = 1;
config.with_soft_pin = true;
// Verify soft pin does not block remove
ASSERT_TRUE(
service_->PutStart(client_id, key, slice_length, config).has_value());
ASSERT_TRUE(
service_->PutEnd(client_id, key, ReplicaType::MEMORY).has_value());
EXPECT_TRUE(service_->Remove(key).has_value());
// Verify soft pin does not block RemoveAll
ASSERT_TRUE(
service_->PutStart(client_id, key, slice_length, config).has_value());
ASSERT_TRUE(
service_->PutEnd(client_id, key, ReplicaType::MEMORY).has_value());
EXPECT_EQ(1, service_->RemoveAll());
}
TEST_F(MasterServiceTest, SoftPinObjectsNotEvictedBeforeOtherObjects) {
const uint64_t kv_lease_ttl = 200;
// set a large soft_pin_ttl so the granted soft pin will not quickly expire
const uint64_t kv_soft_pin_ttl = 10000;
const double eviction_ratio = 0.5;
const bool allow_evict_soft_pinned_objects = true;
auto service_config = MasterServiceConfig::builder()
.set_default_kv_lease_ttl(kv_lease_ttl)
.set_default_kv_soft_pin_ttl(kv_soft_pin_ttl)
.set_allow_evict_soft_pinned_objects(
allow_evict_soft_pinned_objects)
.set_eviction_ratio(eviction_ratio)
.build();
std::unique_ptr<MasterService> service_(new MasterService(service_config));
const UUID client_id = generate_uuid();
// Mount segment and put an object
constexpr size_t buffer = 0x300000000;
constexpr size_t segment_size = 1024 * 1024 * 16;
constexpr size_t value_size = 1024 * 1024;
[[maybe_unused]] const auto context =
PrepareSimpleSegment(*service_, "test_segment", buffer, segment_size);
// The eviction has random factors, so test 5 times
for (int test_i = 0; test_i < 5; test_i++) {
// Put pin_key first
for (int i = 0; i < 2; i++) {
std::string pin_key = "pin_key" + std::to_string(i);
uint64_t slice_length = value_size;
ReplicateConfig soft_pin_config;
soft_pin_config.replica_num = 1;
soft_pin_config.with_soft_pin = true;
ASSERT_TRUE(service_
->PutStart(client_id, pin_key, slice_length,
soft_pin_config)
.has_value());
ASSERT_TRUE(
service_->PutEnd(client_id, pin_key, ReplicaType::MEMORY)
.has_value());
}
// Fill the segment to trigger eviction
int failed_puts = 0;
for (int i = 0; i < 20; i++) {
std::string key = "key" + std::to_string(i);
uint64_t slice_length = value_size;
ReplicateConfig config;
config.replica_num = 1;
if (service_->PutStart(client_id, key, slice_length, config)
.has_value()) {
ASSERT_TRUE(
service_->PutEnd(client_id, key, ReplicaType::MEMORY)
.has_value());
} else {
failed_puts++;
}
}
ASSERT_GT(failed_puts, 0);
// wait for eviction to do eviction
std::this_thread::sleep_for(
std::chrono::milliseconds(kv_lease_ttl + 1000));
// pin_key should still be accessible
for (int i = 0; i < 2; i++) {
std::string pin_key = "pin_key" + std::to_string(i);
ASSERT_TRUE(service_->GetReplicaList(pin_key).has_value());
}
// wait for the lease to expire
std::this_thread::sleep_for(std::chrono::milliseconds(kv_lease_ttl));
// remove all objects before the next turn
service_->RemoveAll();
}
}
TEST_F(MasterServiceTest, SoftPinObjectsCanBeEvicted) {
const uint64_t kv_lease_ttl = 200;
// set a large soft_pin_ttl so the granted soft pin will not quickly expire
const uint64_t kv_soft_pin_ttl = 10000;
const bool allow_evict_soft_pinned_objects = true;
auto service_config = MasterServiceConfig::builder()
.set_default_kv_lease_ttl(kv_lease_ttl)
.set_default_kv_soft_pin_ttl(kv_soft_pin_ttl)
.set_allow_evict_soft_pinned_objects(
allow_evict_soft_pinned_objects)
.build();
std::unique_ptr<MasterService> service_(new MasterService(service_config));
const UUID client_id = generate_uuid();
// Mount segment and put an object
constexpr size_t buffer = 0x300000000;
constexpr size_t segment_size = 1024 * 1024 * 16;
constexpr size_t value_size = 1024 * 1024;
[[maybe_unused]] const auto context =
PrepareSimpleSegment(*service_, "test_segment", buffer, segment_size);
// Verify if we can put objects more than the segment can hold
int success_puts = 0;
for (int i = 0; i < 16 + 50; ++i) {
std::string key = "test_key" + std::to_string(i);
uint64_t slice_length = value_size;
ReplicateConfig config;
config.replica_num = 1;
config.with_soft_pin = true;
if (service_->PutStart(client_id, key, slice_length, config)
.has_value()) {
ASSERT_TRUE(service_->PutEnd(client_id, key, ReplicaType::MEMORY)
.has_value());
success_puts++;
} else {
// wait for eviction to work
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
}
ASSERT_GT(success_puts, 16);
std::this_thread::sleep_for(std::chrono::milliseconds(kv_lease_ttl));
service_->RemoveAll();
}
TEST_F(MasterServiceTest, SoftPinExtendedOnGet) {
const uint64_t kv_lease_ttl = 200;
// The soft pin ttl shall not be too large, otherwise the test will take too
// long
const uint64_t kv_soft_pin_ttl = 1000;
static_assert(
kv_soft_pin_ttl > kv_lease_ttl,
"kv_soft_pin_ttl must be larger than kv_lease_ttl in this test");
const double eviction_ratio = 0.5;
const bool allow_evict_soft_pinned_objects = true;
auto service_config = MasterServiceConfig::builder()
.set_default_kv_lease_ttl(kv_lease_ttl)
.set_default_kv_soft_pin_ttl(kv_soft_pin_ttl)
.set_allow_evict_soft_pinned_objects(
allow_evict_soft_pinned_objects)
.set_eviction_ratio(eviction_ratio)
.build();
std::unique_ptr<MasterService> service_(new MasterService(service_config));
const UUID client_id = generate_uuid();
// Mount segment and put an object
constexpr size_t buffer = 0x300000000;
constexpr size_t segment_size = 1024 * 1024 * 16;
constexpr size_t value_size = 1024 * 1024;
[[maybe_unused]] const auto context =
PrepareSimpleSegment(*service_, "test_segment", buffer, segment_size);
// The eviction has random factors, so test 3 times
for (int test_i = 0; test_i < 3; test_i++) {
// Put pin_key first
for (int i = 0; i < 2; i++) {
std::string pin_key = "pin_key" + std::to_string(i);
uint64_t slice_length = value_size;
ReplicateConfig soft_pin_config;
soft_pin_config.replica_num = 1;
soft_pin_config.with_soft_pin = true;
ASSERT_TRUE(service_->PutStart(client_id, pin_key, slice_length,
soft_pin_config));
ASSERT_TRUE(
service_->PutEnd(client_id, pin_key, ReplicaType::MEMORY)
.has_value());
}
// Wait for the soft pin to expire
std::this_thread::sleep_for(std::chrono::milliseconds(kv_soft_pin_ttl));
// Get the pin_key to extend the soft pin
for (int i = 0; i < 2; i++) {
std::string pin_key = "pin_key" + std::to_string(i);
ASSERT_TRUE(service_->GetReplicaList(pin_key).has_value());
}
// Fill the segment to trigger eviction
int failed_puts = 0;
for (int i = 0; i < 16; i++) {
std::string key = "key" + std::to_string(i);
uint64_t slice_length = value_size;
ReplicateConfig config;
config.replica_num = 1;
if (service_->PutStart(client_id, key, slice_length, config)
.has_value()) {
ASSERT_TRUE(
service_->PutEnd(client_id, key, ReplicaType::MEMORY)
.has_value());
} else {
failed_puts++;
}
}
ASSERT_GT(failed_puts, 0);
// wait for eviction
std::this_thread::sleep_for(std::chrono::milliseconds(kv_lease_ttl));
// pin_key should still be accessible
for (int i = 0; i < 2; i++) {
std::string pin_key = "pin_key" + std::to_string(i);
ASSERT_TRUE(service_->GetReplicaList(pin_key).has_value());
}
// wait for the lease to expire
std::this_thread::sleep_for(std::chrono::milliseconds(kv_lease_ttl));
// remove all objects before the next turn
service_->RemoveAll();
}
}
TEST_F(MasterServiceTest, SoftPinObjectsNotAllowEvict) {
const uint64_t kv_lease_ttl = 200;
// set a large soft_pin_ttl so the granted soft pin will not quickly expire
const uint64_t kv_soft_pin_ttl = 10000;
// set allow_evict_soft_pinned_objects to false to disable eviction of soft
// pinned objects
const bool allow_evict_soft_pinned_objects = false;
auto service_config = MasterServiceConfig::builder()
.set_default_kv_lease_ttl(kv_lease_ttl)
.set_default_kv_soft_pin_ttl(kv_soft_pin_ttl)
.set_allow_evict_soft_pinned_objects(
allow_evict_soft_pinned_objects)
.build();
std::unique_ptr<MasterService> service_(new MasterService(service_config));
const UUID client_id = generate_uuid();
// Mount segment and put an object
constexpr size_t buffer = 0x300000000;
constexpr size_t segment_size = 1024 * 1024 * 16;
constexpr size_t value_size = 1024 * 1024;
[[maybe_unused]] const auto context =
PrepareSimpleSegment(*service_, "test_segment", buffer, segment_size);
// Put objects more than the segment can hold
std::vector<std::string> success_keys;
for (int i = 0; i < 16 + 50; ++i) {
std::string key = "test_key" + std::to_string(i);
uint64_t slice_length = value_size;
ReplicateConfig config;
config.replica_num = 1;
config.with_soft_pin = true;
if (service_->PutStart(client_id, key, slice_length, config)
.has_value()) {
ASSERT_TRUE(service_->PutEnd(client_id, key, ReplicaType::MEMORY)
.has_value());
success_keys.push_back(key);
} else {
// wait for eviction to work
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
}
ASSERT_LE(success_keys.size(), 17);
// All soft pinned objects should be accessible
for (const auto& key : success_keys) {
ASSERT_TRUE(service_->GetReplicaList(key).has_value());
}
std::this_thread::sleep_for(std::chrono::milliseconds(kv_lease_ttl));
service_->RemoveAll();
}
TEST_F(MasterServiceTest, ReplicaSegmentsAreUnique) {
std::unique_ptr<MasterService> service_(new MasterService());
const UUID client_id = generate_uuid();
// Mount 20 segments, each 16MB and slab-aligned
constexpr size_t kBaseAddr = 0x300000000;
constexpr size_t kSegmentSize = 1024 * 1024 * 16; // 16MB
for (int i = 0; i < 20; ++i) {
[[maybe_unused]] const auto context = PrepareSimpleSegment(
*service_, "segment_" + std::to_string(i),
kBaseAddr + static_cast<size_t>(i) * kSegmentSize, kSegmentSize);
}
// Object with 16 slices of ~1MB and replication factor 10
const std::string key = "replica_uniqueness_test_key";
uint64_t slice_length = 1024 * 1024 - 16;
ReplicateConfig config;
config.replica_num = 10;
auto put_start_result =
service_->PutStart(client_id, key, slice_length, 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());
// Segment names across replicas must be unique
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_length, mem.buffer_descriptor.size_);
segment_names.insert(mem.buffer_descriptor.transport_endpoint_);
}
EXPECT_EQ(segment_names.size(), config.replica_num)
<< "Duplicate segment found";
ASSERT_TRUE(
service_->PutEnd(client_id, key, ReplicaType::MEMORY).has_value());
}
TEST_F(MasterServiceTest, ReplicationFactorTwoWithSingleSegment) {
std::unique_ptr<MasterService> service_(new MasterService());
const UUID client_id = generate_uuid();
// Mount a single 16MB segment
constexpr size_t kBaseAddr = 0x300000000;
constexpr size_t kSegmentSize = 1024 * 1024 * 16; // 16MB
[[maybe_unused]] const auto context = PrepareSimpleSegment(
*service_, "single_segment", kBaseAddr, kSegmentSize);
// 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";
uint64_t slice_length = 1024;
ReplicateConfig config;
config.replica_num = 2;
auto put_start_result =
service_->PutStart(client_id, key, slice_length, 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("single_segment", mem_desc.buffer_descriptor.transport_endpoint_);
EXPECT_EQ(1024u, mem_desc.buffer_descriptor.size_);
}
TEST_F(MasterServiceTest, BatchExistKeyTest) {
std::unique_ptr<MasterService> service_(new MasterService());
const UUID client_id = generate_uuid();
// Mount a segment
constexpr size_t buffer = 0x300000000;
constexpr size_t size = 1024 * 1024 * 128;
constexpr size_t value_size = 1024;
[[maybe_unused]] const auto context =
PrepareSimpleSegment(*service_, "test_segment", buffer, size);
int test_object_num = 10;
std::vector<std::string> test_keys;
for (int i = 0; i < test_object_num; ++i) {
test_keys.push_back("test_key" + std::to_string(i));
ReplicateConfig config;
config.replica_num = 1;
uint64_t slice_length = value_size;
auto put_start_result =
service_->PutStart(client_id, test_keys[i], slice_length, config);
ASSERT_TRUE(put_start_result.has_value());
auto put_end_result =
service_->PutEnd(client_id, test_keys[i], ReplicaType::MEMORY);
ASSERT_TRUE(put_end_result.has_value());
}
// Test individual ExistKey calls to verify the underlying functionality
for (int i = 0; i < test_object_num; ++i) {
auto exist_result = service_->ExistKey(test_keys[i]);
EXPECT_TRUE(exist_result.value());
}
// Tets batch
test_keys.push_back("non_existent_key");
auto exist_resp = service_->BatchExistKey(test_keys);
for (int i = 0; i < test_object_num; ++i) {
ASSERT_TRUE(exist_resp[i].value());
}
ASSERT_FALSE(exist_resp[test_object_num].value());
}
TEST_F(MasterServiceTest, BatchQueryIpTest) {
std::unique_ptr<MasterService> service_(new MasterService());
const UUID client_id = generate_uuid();
// Mount a segment with a specific te_endpoint (IP:Port format)
constexpr size_t buffer = 0x300000000;
constexpr size_t size = 1024 * 1024 * 16;
Segment segment = MakeSegment("test_segment", buffer, size);
segment.te_endpoint = "127.0.0.1:12345"; // Set IP:Port format for testing
auto mount_result = service_->MountSegment(segment, client_id);
ASSERT_TRUE(mount_result.has_value());
// Test BatchQueryIp with a single client_id
std::vector<UUID> client_ids = {client_id};
auto query_result = service_->BatchQueryIp(client_ids);
ASSERT_TRUE(query_result.has_value())
<< "BatchQueryIp failed: " << toString(query_result.error());
const auto& results = query_result.value();
ASSERT_FALSE(results.empty()) << "BatchQueryIp returned empty results";
auto it = results.find(client_id);
ASSERT_NE(it, results.end()) << "Client ID not found in results";
const auto& ip_addresses = it->second;
ASSERT_FALSE(ip_addresses.empty()) << "No IP addresses found for client";
ASSERT_EQ(1u, ip_addresses.size()) << "Expected exactly 1 IP address";
EXPECT_EQ("127.0.0.1", ip_addresses[0]) << "IP address mismatch";
// Test BatchQueryIp with multiple client_ids (one valid, one invalid)
UUID non_existent_client_id = generate_uuid();
std::vector<UUID> mixed_client_ids = {client_id, non_existent_client_id};
auto mixed_query_result = service_->BatchQueryIp(mixed_client_ids);
ASSERT_TRUE(mixed_query_result.has_value());
const auto& mixed_results = mixed_query_result.value();
// Valid client_id should be in results
ASSERT_NE(mixed_results.find(client_id), mixed_results.end())
<< "Valid client_id should be in results";
// Invalid client_id should not be in results (silently skipped)
EXPECT_EQ(mixed_results.find(non_existent_client_id), mixed_results.end())
<< "Invalid client_id should not be in results";
}
TEST_F(MasterServiceTest, BatchQueryIpMultipleSegmentsTest) {
std::unique_ptr<MasterService> service_(new MasterService());
const UUID client_id = generate_uuid();
// Mount multiple segments with different IPs for the same client
constexpr size_t buffer1 = 0x300000000;
constexpr size_t buffer2 = 0x400000000;
constexpr size_t size = 1024 * 1024 * 16;
Segment segment1 = MakeSegment("segment1", buffer1, size);
segment1.te_endpoint = "127.0.0.1:12345";
auto mount_result1 = service_->MountSegment(segment1, client_id);
ASSERT_TRUE(mount_result1.has_value());
Segment segment2 = MakeSegment("segment2", buffer2, size);
segment2.te_endpoint = "127.0.0.1:12346"; // Same IP, different port
auto mount_result2 = service_->MountSegment(segment2, client_id);
ASSERT_TRUE(mount_result2.has_value());
Segment segment3 = MakeSegment("segment3", 0x500000000, size);
segment3.te_endpoint = "192.168.1.1:12345"; // Different IP
auto mount_result3 = service_->MountSegment(segment3, client_id);
ASSERT_TRUE(mount_result3.has_value());
// Test BatchQueryIp - should return unique IPs
std::vector<UUID> client_ids = {client_id};
auto query_result = service_->BatchQueryIp(client_ids);
ASSERT_TRUE(query_result.has_value());
const auto& results = query_result.value();
auto it = results.find(client_id);
ASSERT_NE(it, results.end());
const auto& ip_addresses = it->second;
// Should have 2 unique IPs: 127.0.0.1 and 192.168.1.1
ASSERT_EQ(2u, ip_addresses.size()) << "Expected 2 unique IP addresses";
// Verify both IPs are present
std::unordered_set<std::string> ip_set(ip_addresses.begin(),
ip_addresses.end());
EXPECT_NE(ip_set.find("127.0.0.1"), ip_set.end());
EXPECT_NE(ip_set.find("192.168.1.1"), ip_set.end());
}
TEST_F(MasterServiceTest, BatchQueryIpEmptyClientIdTest) {
std::unique_ptr<MasterService> service_(new MasterService());
// Test with empty client_ids list
std::vector<UUID> empty_client_ids;
auto query_result = service_->BatchQueryIp(empty_client_ids);
ASSERT_TRUE(query_result.has_value());
const auto& results = query_result.value();
EXPECT_TRUE(results.empty())
<< "Empty client_ids should return empty results";
}
TEST_F(MasterServiceTest, BatchQueryIpMultipleSegmentsEmptyTeEndpointTest) {
std::unique_ptr<MasterService> service_(new MasterService());
const UUID client_id = generate_uuid();
// Mount multiple segments, all with empty te_endpoint
constexpr size_t buffer1 = 0x300000000;
constexpr size_t buffer2 = 0x400000000;
constexpr size_t size = 1024 * 1024 * 16;
Segment segment1 = MakeSegment("segment1", buffer1, size);
segment1.te_endpoint = ""; // Empty te_endpoint
auto mount_result1 = service_->MountSegment(segment1, client_id);
ASSERT_TRUE(mount_result1.has_value());
Segment segment2 = MakeSegment("segment2", buffer2, size);
segment2.te_endpoint = ""; // Empty te_endpoint
auto mount_result2 = service_->MountSegment(segment2, client_id);
ASSERT_TRUE(mount_result2.has_value());
// Test BatchQueryIp - should return client with empty IP vector
std::vector<UUID> client_ids = {client_id};
auto query_result = service_->BatchQueryIp(client_ids);
ASSERT_TRUE(query_result.has_value());
const auto& results = query_result.value();
ASSERT_FALSE(results.empty())
<< "BatchQueryIp should include client in results even with empty IPs";
auto it = results.find(client_id);
ASSERT_NE(it, results.end()) << "Client ID should be found in results even "
"with all empty te_endpoints";
// Verify the IP vector is empty
const auto& ip_addresses = it->second;
EXPECT_TRUE(ip_addresses.empty())
<< "Client with all empty te_endpoints should have empty IP vector";
}
TEST_F(MasterServiceTest, BatchQueryIpBracketedIpv6Test) {
std::unique_ptr<MasterService> service_(new MasterService());
const UUID client_id = generate_uuid();
// Mount a segment with a bracketed IPv6 endpoint
constexpr size_t buffer = 0x300000000;
constexpr size_t size = 1024 * 1024 * 16;
Segment segment = MakeSegment("test_segment", buffer, size);
segment.te_endpoint = "[::1]:17813";
auto mount_result = service_->MountSegment(segment, client_id);
ASSERT_TRUE(mount_result.has_value());
std::vector<UUID> client_ids = {client_id};
auto query_result = service_->BatchQueryIp(client_ids);
ASSERT_TRUE(query_result.has_value());
const auto& results = query_result.value();
auto it = results.find(client_id);
ASSERT_NE(it, results.end());
const auto& ip_addresses = it->second;
ASSERT_EQ(1u, ip_addresses.size());
EXPECT_EQ("::1", ip_addresses[0]);
}
TEST_F(MasterServiceTest, BatchQueryIpLinkLocalIpv6WithScopeTest) {
std::unique_ptr<MasterService> service_(new MasterService());
const UUID client_id = generate_uuid();
// Mount a segment with a link-local IPv6 address with scope ID
constexpr size_t buffer = 0x300000000;
constexpr size_t size = 1024 * 1024 * 16;
Segment segment = MakeSegment("test_segment", buffer, size);
segment.te_endpoint = "fe80::a236:bcff:fecb:a1be%eno2:15773";
auto mount_result = service_->MountSegment(segment, client_id);
ASSERT_TRUE(mount_result.has_value());
std::vector<UUID> client_ids = {client_id};
auto query_result = service_->BatchQueryIp(client_ids);
ASSERT_TRUE(query_result.has_value());
const auto& results = query_result.value();
auto it = results.find(client_id);
ASSERT_NE(it, results.end());
const auto& ip_addresses = it->second;
ASSERT_EQ(1u, ip_addresses.size());
EXPECT_EQ("fe80::a236:bcff:fecb:a1be%eno2", ip_addresses[0]);
}
TEST_F(MasterServiceTest, BatchQueryIpIpv6NoPortTest) {
std::unique_ptr<MasterService> service_(new MasterService());
const UUID client_id = generate_uuid();
// Mount a segment with an IPv6 address without port
constexpr size_t buffer = 0x300000000;
constexpr size_t size = 1024 * 1024 * 16;
Segment segment = MakeSegment("test_segment", buffer, size);
segment.te_endpoint = "::1";
auto mount_result = service_->MountSegment(segment, client_id);
ASSERT_TRUE(mount_result.has_value());
std::vector<UUID> client_ids = {client_id};
auto query_result = service_->BatchQueryIp(client_ids);
ASSERT_TRUE(query_result.has_value());
const auto& results = query_result.value();
auto it = results.find(client_id);
ASSERT_NE(it, results.end());
const auto& ip_addresses = it->second;
ASSERT_EQ(1u, ip_addresses.size());
EXPECT_EQ("::1", ip_addresses[0]);
}
TEST_F(MasterServiceTest, BatchQueryIpMixedIpv4AndIpv6Test) {
std::unique_ptr<MasterService> service_(new MasterService());
const UUID client_id = generate_uuid();
// Mount segments with IPv4 and IPv6 endpoints for the same client
constexpr size_t buffer1 = 0x300000000;
constexpr size_t buffer2 = 0x400000000;
constexpr size_t size = 1024 * 1024 * 16;
Segment segment1 = MakeSegment("segment1", buffer1, size);
segment1.te_endpoint = "192.168.1.1:12345";
auto mount_result1 = service_->MountSegment(segment1, client_id);
ASSERT_TRUE(mount_result1.has_value());
Segment segment2 = MakeSegment("segment2", buffer2, size);
segment2.te_endpoint = "[::1]:17813";
auto mount_result2 = service_->MountSegment(segment2, client_id);
ASSERT_TRUE(mount_result2.has_value());
std::vector<UUID> client_ids = {client_id};
auto query_result = service_->BatchQueryIp(client_ids);
ASSERT_TRUE(query_result.has_value());
const auto& results = query_result.value();
auto it = results.find(client_id);
ASSERT_NE(it, results.end());
const auto& ip_addresses = it->second;
ASSERT_EQ(2u, ip_addresses.size());
std::unordered_set<std::string> ip_set(ip_addresses.begin(),
ip_addresses.end());
EXPECT_NE(ip_set.find("192.168.1.1"), ip_set.end());
EXPECT_NE(ip_set.find("::1"), ip_set.end());
}
TEST_F(MasterServiceTest, PutStartExpiringTest) {
// Reset storage space metrics.
MasterMetricManager::instance().reset_allocated_mem_size();
MasterMetricManager::instance().reset_total_mem_capacity();
MasterServiceConfig master_config;
master_config.put_start_discard_timeout_sec = 3;
master_config.put_start_release_timeout_sec = 5;
std::unique_ptr<MasterService> service_(new MasterService(master_config));
constexpr size_t kReplicaCnt = 3;
constexpr size_t kBaseAddr = 0x300000000;
constexpr size_t kSegmentSize = 1024 * 1024 * 16; // 16MB
// Mount 3 segments.
std::vector<MountedSegmentContext> contexts;
contexts.reserve(kReplicaCnt);
for (size_t i = 0; i < kReplicaCnt; ++i) {
auto context = PrepareSimpleSegment(
*service_, "segment_" + std::to_string(i),
kBaseAddr + static_cast<size_t>(i) * kSegmentSize, kSegmentSize);
contexts.push_back(context);
}
// The client_id used to put objects.
auto client_id = generate_uuid();
std::string key_1 = "test_key_1", key_2 = "test_key_2";
uint64_t value_length = 6 * 1024 * 1024; // 6MB
uint64_t slice_length = value_length;
ReplicateConfig config;
config.replica_num = kReplicaCnt;
// Put key_1, should success.
auto put_start_result =
service_->PutStart(client_id, key_1, slice_length, config);
EXPECT_TRUE(put_start_result.has_value());
replica_list = put_start_result.value();
EXPECT_EQ(replica_list.size(), kReplicaCnt);
for (size_t i = 0; i < kReplicaCnt; i++) {
EXPECT_EQ(ReplicaStatus::PROCESSING, replica_list[i].status);
}
// Put key_1 again, should fail because the key exists.
put_start_result =
service_->PutStart(client_id, key_1, slice_length, config);
EXPECT_FALSE(put_start_result.has_value());
EXPECT_EQ(put_start_result.error(), ErrorCode::OBJECT_ALREADY_EXISTS);
// Wait for a while until the put-start expired.
for (size_t i = 0; i <= master_config.put_start_discard_timeout_sec; i++) {
for (auto& context : contexts) {
auto result = service_->Ping(context.client_id);
EXPECT_TRUE(result.has_value());
}
std::this_thread::sleep_for(std::chrono::seconds(1));
}
// Put key_1 again, should success because the old one has expired and will
// be discarded by this put.
put_start_result =
service_->PutStart(client_id, key_1, slice_length, config);
EXPECT_TRUE(put_start_result.has_value());
replica_list = put_start_result.value();
EXPECT_EQ(replica_list.size(), kReplicaCnt);
for (size_t i = 0; i < kReplicaCnt; i++) {
EXPECT_EQ(ReplicaStatus::PROCESSING, replica_list[i].status);
}
// Complete key_1.
auto put_end_result =
service_->PutEnd(client_id, key_1, ReplicaType::MEMORY);
EXPECT_TRUE(put_end_result.has_value());
// Protect key_1 from eviction.
auto get_result = service_->GetReplicaList(key_1);
EXPECT_TRUE(get_result.has_value());
// Put key_2, should fail because the key_1 occupied 12MB (6MB processing,
// 6MB discarded but not yet released) on each segment.
put_start_result =
service_->PutStart(client_id, key_2, slice_length, config);
EXPECT_FALSE(put_start_result.has_value());
EXPECT_EQ(put_start_result.error(), ErrorCode::NO_AVAILABLE_HANDLE);
// Wait for a while until the discarded replicas are released.
for (size_t i = 0; i <= master_config.put_start_release_timeout_sec -
master_config.put_start_discard_timeout_sec;
i++) {
for (auto& context : contexts) {
auto result = service_->Ping(context.client_id);
EXPECT_TRUE(result.has_value());
}
// Protect key_1 from eviction.
auto get_result = service_->GetReplicaList(key_1);
EXPECT_TRUE(get_result.has_value());
std::this_thread::sleep_for(std::chrono::seconds(1));
}
// Put key_2 again, should success because the discarded replica has been
// released.
put_start_result =
service_->PutStart(client_id, key_2, slice_length, config);
EXPECT_TRUE(put_start_result.has_value());
replica_list = put_start_result.value();
EXPECT_EQ(replica_list.size(), kReplicaCnt);
for (size_t i = 0; i < kReplicaCnt; i++) {
EXPECT_EQ(ReplicaStatus::PROCESSING, replica_list[i].status);
}
// Wait for a while until key_2 can be discarded and released.
for (size_t i = 0; i <= master_config.put_start_release_timeout_sec; i++) {
for (auto& context : contexts) {
auto result = service_->Ping(context.client_id);
EXPECT_TRUE(result.has_value());
}
// Protect key_1 from eviction.
auto get_result = service_->GetReplicaList(key_1);
EXPECT_TRUE(get_result.has_value());
std::this_thread::sleep_for(std::chrono::seconds(1));
}
// Put key_2 again, should fail because eviction has not been triggered. And
// this PutStart should trigger the eviction.
put_start_result =
service_->PutStart(client_id, key_2, slice_length, config);
EXPECT_FALSE(put_start_result.has_value());
EXPECT_EQ(put_start_result.error(), ErrorCode::NO_AVAILABLE_HANDLE);
// Wait a moment for the eviction to complete.
std::this_thread::sleep_for(std::chrono::milliseconds(50));
// Put key_2 again, should success because the previous one has been
// discarded and released.
put_start_result =
service_->PutStart(client_id, key_2, slice_length, config);
EXPECT_TRUE(put_start_result.has_value());
replica_list = put_start_result.value();
EXPECT_EQ(replica_list.size(), kReplicaCnt);
for (size_t i = 0; i < kReplicaCnt; i++) {
EXPECT_EQ(ReplicaStatus::PROCESSING, replica_list[i].status);
}
// Complete key_2.
put_end_result = service_->PutEnd(client_id, key_2, ReplicaType::MEMORY);
EXPECT_TRUE(put_end_result.has_value());
}
TEST_F(MasterServiceTest, ConcurrentMountLocalDiskSegment) {
MasterServiceConfig config;
config.enable_offload = true;
std::unique_ptr<MasterService> service_(new MasterService(config));
constexpr size_t num_threads = 100;
std::vector<std::thread> threads;
std::atomic<int> success_count{0};
// Launch multiple threads to mount local disk segments concurrently
for (size_t i = 0; i < num_threads; i++) {
threads.emplace_back([&service_, i, &success_count, this]() {
UUID client_id = generate_uuid();
auto mount_result =
service_->MountLocalDiskSegment(client_id, true);
ASSERT_TRUE(mount_result.has_value());
++success_count;
});
}
// Wait for all threads to complete
for (auto& thread : threads) {
thread.join();
}
// Verify that some mount/unmount operations succeeded
EXPECT_GT(success_count, 0);
}
TEST_F(MasterServiceTest, OffloadObjectHeartbeat) {
constexpr size_t key_cnt = 3000;
MasterServiceConfig config;
config.enable_offload = true;
std::unique_ptr<MasterService> service_(new MasterService(config));
UUID client_id = generate_uuid();
constexpr size_t buffer = 0x300000000;
constexpr size_t size = 1024 * 1024 * 16;
auto segment = MakeSegment("segment", buffer, size);
auto mount_result = service_->MountSegment(segment, client_id);
ASSERT_TRUE(mount_result.has_value());
auto mount_local_disk_result =
service_->MountLocalDiskSegment(client_id, false);
ASSERT_TRUE(mount_local_disk_result.has_value());
for (size_t i = 0; i < key_cnt; i++) {
auto key = GenerateKeyForSegment(client_id, service_, segment.name);
}
auto res = service_->OffloadObjectHeartbeat(client_id, true);
if (!res) {
LOG(ERROR) << "OffloadObjectHeartbeat failed with error: "
<< res.error();
ASSERT_TRUE(res);
}
ASSERT_EQ(res->size(), 0);
std::vector<std::string> keys;
for (size_t i = 0; i < key_cnt; i++) {
auto key = GenerateKeyForSegment(client_id, service_, segment.name);
keys.push_back(key);
}
res = service_->OffloadObjectHeartbeat(client_id, true);
if (!res) {
LOG(ERROR) << "OffloadObjectHeartbeat failed with error: "
<< res.error();
ASSERT_TRUE(res);
}
ASSERT_EQ(res->size(), keys.size());
for (auto& key : keys) {
ASSERT_TRUE(res.value().find(key) != res.value().end());
ASSERT_EQ(res.value().find(key)->second, 1024);
}
keys.clear();
for (size_t i = 0; i < key_cnt; i++) {
auto key = GenerateKeyForSegment(client_id, service_, segment.name);
keys.push_back(key);
}
res = service_->OffloadObjectHeartbeat(client_id, true);
if (!res) {
LOG(ERROR) << "OffloadObjectHeartbeat failed with error: "
<< res.error();
ASSERT_TRUE(res);
}
ASSERT_EQ(res->size(), keys.size());
for (auto& key : keys) {
ASSERT_TRUE(res.value().find(key) != res.value().end());
ASSERT_EQ(res.value().find(key)->second, 1024);
}
}
TEST_F(MasterServiceTest, BatchReplicaClearAllSegments) {
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));
[[maybe_unused]] const auto context = PrepareSimpleSegment(*service_);
const UUID client_id = generate_uuid();
// Create multiple objects
std::vector<std::string> keys;
const int num_objects = 5;
for (int i = 0; i < num_objects; ++i) {
std::string key = "batch_clear_key_" + std::to_string(i);
keys.push_back(key);
uint64_t value_length = 1024;
ReplicateConfig config;
config.replica_num = 1;
auto put_start_result =
service_->PutStart(client_id, key, value_length, config);
ASSERT_TRUE(put_start_result.has_value());
auto put_end_result =
service_->PutEnd(client_id, key, ReplicaType::MEMORY);
ASSERT_TRUE(put_end_result.has_value());
}
// Verify objects exist
for (const auto& key : keys) {
auto exist_result = service_->ExistKey(key);
ASSERT_TRUE(exist_result.has_value());
ASSERT_TRUE(exist_result.value());
}
// Wait for lease to expire
std::this_thread::sleep_for(std::chrono::milliseconds(kv_lease_ttl + 10));
// Clear all replicas (empty segment_name means clear all segments)
auto clear_result = service_->BatchReplicaClear(keys, client_id, "");
ASSERT_TRUE(clear_result.has_value());
const auto& cleared_keys = clear_result.value();
ASSERT_EQ(num_objects, cleared_keys.size()) << "All keys should be cleared";
// Verify objects are removed
for (const auto& key : keys) {
auto exist_result = service_->ExistKey(key);
ASSERT_TRUE(exist_result.has_value());
ASSERT_FALSE(exist_result.value())
<< "Key " << key << " should be removed";
}
}
TEST_F(MasterServiceTest, BatchReplicaClearSpecificSegment) {
// 1. Setup: Control the lease time
const uint64_t kv_lease_ttl = 200;
auto service_config = MasterServiceConfig::builder()
.set_default_kv_lease_ttl(kv_lease_ttl)
.build();
std::unique_ptr<MasterService> service_(new MasterService(service_config));
const UUID client_id = generate_uuid();
// 2. Setup: Mount segments
Segment segment1 = MakeSegment("segment1", 0x300000000, 1024 * 1024 * 16);
Segment segment2 = MakeSegment("segment2", 0x400000000, 1024 * 1024 * 16);
ASSERT_TRUE(service_->MountSegment(segment1, client_id).has_value());
ASSERT_TRUE(service_->MountSegment(segment2, client_id).has_value());
// 3. Setup: Create the object on segment1 using preferred_segment
std::string key = "segment_specific_key";
std::string segment_name = segment1.name;
uint64_t value_length = 1024;
ReplicateConfig config;
config.replica_num = 1;
config.preferred_segment =
segment_name; // Ensure object is placed on segment1
auto put_start_result =
service_->PutStart(client_id, key, value_length, config);
ASSERT_TRUE(put_start_result.has_value());
auto put_end_result = service_->PutEnd(client_id, key, ReplicaType::MEMORY);
ASSERT_TRUE(put_end_result.has_value());
// 4. Wait for lease to expire and verify it's actually expired
// PutEnd calls GrantLease(0, ...) which sets lease_timeout to now.
// Due to clock precision and timing, we need to ensure the lease is
// actually expired before calling BatchReplicaClear.
// Use a small delay and then poll to ensure lease is expired.
std::this_thread::sleep_for(std::chrono::milliseconds(10));
// Poll until lease is expired (with timeout to avoid infinite loop)
const auto timeout = std::chrono::seconds(5);
const auto start_time = std::chrono::steady_clock::now();
bool lease_expired = false;
std::vector<std::string> keys = {key};
tl::expected<std::vector<std::string>, ErrorCode> clear_result;
while (std::chrono::steady_clock::now() - start_time < timeout) {
// Try to clear - if it succeeds, lease is expired
clear_result =
service_->BatchReplicaClear(keys, client_id, segment_name);
ASSERT_TRUE(clear_result.has_value());
if (clear_result.value().size() == 1) {
lease_expired = true;
break;
}
// Lease not expired yet, wait a bit and retry
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
ASSERT_TRUE(lease_expired) << "Lease did not expire within timeout period";
// 5. Verify the key was cleared
const auto& cleared_keys = clear_result.value();
ASSERT_EQ(1u, cleared_keys.size()) << "Key should be cleared";
auto exist_result = service_->ExistKey(key);
ASSERT_TRUE(exist_result.has_value());
ASSERT_FALSE(exist_result.value())
<< "Key should be removed after being cleared.";
}
TEST_F(MasterServiceTest, BatchReplicaClearWithLeaseActive) {
const uint64_t kv_lease_ttl = 2000; // Long lease
auto service_config = MasterServiceConfig::builder()
.set_default_kv_lease_ttl(kv_lease_ttl)
.build();
std::unique_ptr<MasterService> service_(new MasterService(service_config));
[[maybe_unused]] const auto context = PrepareSimpleSegment(*service_);
const UUID client_id = generate_uuid();
// Create an object
std::string key = "lease_active_key";
uint64_t value_length = 1024;
ReplicateConfig config;
config.replica_num = 1;
auto put_start_result =
service_->PutStart(client_id, key, value_length, config);
ASSERT_TRUE(put_start_result.has_value());
auto put_end_result = service_->PutEnd(client_id, key, ReplicaType::MEMORY);
ASSERT_TRUE(put_end_result.has_value());
// Grant a lease by calling GetReplicaList (similar to normal usage)
auto get_result = service_->GetReplicaList(key);
ASSERT_TRUE(get_result.has_value());
// Try to clear immediately (lease should still be active)
std::vector<std::string> keys = {key};
auto clear_result = service_->BatchReplicaClear(keys, client_id, "");
ASSERT_TRUE(clear_result.has_value());
// Should return empty list because lease is still active
const auto& cleared_keys = clear_result.value();
EXPECT_TRUE(cleared_keys.empty())
<< "No keys should be cleared when lease is active";
// Verify object still exists
auto exist_result = service_->ExistKey(key);
ASSERT_TRUE(exist_result.has_value());
ASSERT_TRUE(exist_result.value()) << "Key should still exist";
}
TEST_F(MasterServiceTest, BatchReplicaClearWithDifferentClientId) {
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));
[[maybe_unused]] const auto context = PrepareSimpleSegment(*service_);
const UUID client_id1 = generate_uuid();
const UUID client_id2 = generate_uuid();
// Create an object with client_id1
std::string key = "client_specific_key";
uint64_t value_length = 1024;
ReplicateConfig config;
config.replica_num = 1;
auto put_start_result =
service_->PutStart(client_id1, key, value_length, config);
ASSERT_TRUE(put_start_result.has_value());
auto put_end_result =
service_->PutEnd(client_id1, key, ReplicaType::MEMORY);
ASSERT_TRUE(put_end_result.has_value());
// Wait for lease to expire
std::this_thread::sleep_for(std::chrono::milliseconds(kv_lease_ttl + 10));
// Try to clear with different client_id
std::vector<std::string> keys = {key};
auto clear_result = service_->BatchReplicaClear(keys, client_id2, "");
ASSERT_TRUE(clear_result.has_value());
// Should return empty list because client_id doesn't match
const auto& cleared_keys = clear_result.value();
EXPECT_TRUE(cleared_keys.empty())
<< "No keys should be cleared for different client_id";
// Verify object still exists
auto exist_result = service_->ExistKey(key);
ASSERT_TRUE(exist_result.has_value());
ASSERT_TRUE(exist_result.value()) << "Key should still exist";
}
TEST_F(MasterServiceTest, BatchReplicaClearWithNonExistentKeys) {
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));
[[maybe_unused]] const auto context = PrepareSimpleSegment(*service_);
const UUID client_id = generate_uuid();
// Try to clear non-existent keys
std::vector<std::string> keys = {"non_existent_key1", "non_existent_key2"};
auto clear_result = service_->BatchReplicaClear(keys, client_id, "");
ASSERT_TRUE(clear_result.has_value());
// Should return empty list
const auto& cleared_keys = clear_result.value();
EXPECT_TRUE(cleared_keys.empty())
<< "No keys should be cleared for non-existent keys";
}
TEST_F(MasterServiceTest, BatchReplicaClearWithEmptyKeys) {
std::unique_ptr<MasterService> service_(new MasterService());
[[maybe_unused]] const auto context = PrepareSimpleSegment(*service_);
const UUID client_id = generate_uuid();
// Try to clear empty keys list
std::vector<std::string> empty_keys;
auto clear_result = service_->BatchReplicaClear(empty_keys, client_id, "");
ASSERT_TRUE(clear_result.has_value());
// Should return empty list
const auto& cleared_keys = clear_result.value();
EXPECT_TRUE(cleared_keys.empty())
<< "Empty keys list should return empty result";
}
TEST_F(MasterServiceTest, BatchReplicaClearWithEmptyStringKeys) {
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));
[[maybe_unused]] const auto context = PrepareSimpleSegment(*service_);
const UUID client_id = generate_uuid();
// Create a valid object
std::string valid_key = "valid_key";
uint64_t value_length = 1024;
ReplicateConfig config;
config.replica_num = 1;
auto put_start_result =
service_->PutStart(client_id, valid_key, value_length, config);
ASSERT_TRUE(put_start_result.has_value());
auto put_end_result =
service_->PutEnd(client_id, valid_key, ReplicaType::MEMORY);
ASSERT_TRUE(put_end_result.has_value());
// Wait for lease to expire
std::this_thread::sleep_for(std::chrono::milliseconds(kv_lease_ttl + 10));
// Try to clear with empty string keys mixed with valid keys
std::vector<std::string> keys = {"", valid_key, "", "another_empty"};
auto clear_result = service_->BatchReplicaClear(keys, client_id, "");
ASSERT_TRUE(clear_result.has_value());
// Should only clear the valid key, skip empty strings
const auto& cleared_keys = clear_result.value();
ASSERT_EQ(1u, cleared_keys.size()) << "Only valid key should be cleared";
EXPECT_EQ(valid_key, cleared_keys[0]);
}
TEST_F(MasterServiceTest, BatchReplicaClearMixedScenario) {
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));
[[maybe_unused]] const auto context = PrepareSimpleSegment(*service_);
const UUID client_id1 = generate_uuid();
const UUID client_id2 = generate_uuid();
// Create objects with different client_ids
std::string key1 = "mixed_key1"; // client_id1
std::string key2 = "mixed_key2"; // client_id1
std::string key3 = "mixed_key3"; // client_id2
uint64_t value_length = 1024;
ReplicateConfig config;
config.replica_num = 1;
// Create key1 and key2 with client_id1
auto put_start1 =
service_->PutStart(client_id1, key1, value_length, config);
ASSERT_TRUE(put_start1.has_value());
auto put_end1 = service_->PutEnd(client_id1, key1, ReplicaType::MEMORY);
ASSERT_TRUE(put_end1.has_value());
auto put_start2 =
service_->PutStart(client_id1, key2, value_length, config);
ASSERT_TRUE(put_start2.has_value());
auto put_end2 = service_->PutEnd(client_id1, key2, ReplicaType::MEMORY);
ASSERT_TRUE(put_end2.has_value());
// Create key3 with client_id2
auto put_start3 =
service_->PutStart(client_id2, key3, value_length, config);
ASSERT_TRUE(put_start3.has_value());
auto put_end3 = service_->PutEnd(client_id2, key3, ReplicaType::MEMORY);
ASSERT_TRUE(put_end3.has_value());
// Wait for lease to expire
std::this_thread::sleep_for(std::chrono::milliseconds(kv_lease_ttl + 10));
// Try to clear with mixed keys (some belong to client_id1, some to
// client_id2)
std::vector<std::string> keys = {key1, key2, key3, "non_existent", ""};
auto clear_result = service_->BatchReplicaClear(keys, client_id1, "");
ASSERT_TRUE(clear_result.has_value());
// Should only clear key1 and key2 (belonging to client_id1)
const auto& cleared_keys = clear_result.value();
ASSERT_EQ(2u, cleared_keys.size())
<< "Only keys belonging to client_id1 should be cleared";
// Verify key1 and key2 are cleared
auto exist1 = service_->ExistKey(key1);
ASSERT_TRUE(exist1.has_value());
ASSERT_FALSE(exist1.value()) << "key1 should be cleared";
auto exist2 = service_->ExistKey(key2);
ASSERT_TRUE(exist2.has_value());
ASSERT_FALSE(exist2.value()) << "key2 should be cleared";
// Verify key3 still exists (different client_id)
auto exist3 = service_->ExistKey(key3);
ASSERT_TRUE(exist3.has_value());
ASSERT_TRUE(exist3.value())
<< "key3 should still exist (different client_id)";
}
TEST_F(MasterServiceTest, CreateCopyTaskTest) {
// Reset storage space metrics.
MasterMetricManager::instance().reset_allocated_mem_size();
MasterMetricManager::instance().reset_total_mem_capacity();
// Create MasterService
std::unique_ptr<MasterService> service_(new MasterService());
// Mount 3 segments.
constexpr size_t kReplicaCnt = 3;
constexpr size_t kBaseAddr = 0x300000000;
constexpr size_t kSegmentSize = 1024 * 1024 * 16; // 16MB
std::vector<MountedSegmentContext> contexts;
contexts.reserve(kReplicaCnt);
for (size_t i = 0; i < kReplicaCnt; ++i) {
const auto context = PrepareSimpleSegment(
*service_, "segment_" + std::to_string(i),
kBaseAddr + static_cast<size_t>(i) * kSegmentSize, kSegmentSize);
contexts.push_back(context);
}
// The client putting the object to segment_0
auto client_id = generate_uuid();
std::string key1 = "test_key_1";
uint64_t value_length = 6 * 1024 * 1024; // 6MB
uint64_t slice_length = value_length;
ReplicateConfig config;
config.replica_num = 1;
config.preferred_segment = "segment_0";
auto put_start_result =
service_->PutStart(client_id, key1, slice_length, config);
EXPECT_TRUE(put_start_result.has_value());
auto put_end_result =
service_->PutEnd(client_id, key1, ReplicaType::MEMORY);
EXPECT_TRUE(put_end_result.has_value());
// Copy key1 to "segment_1" and "segment_2"
auto copy_result =
service_->CreateCopyTask(key1, {"segment_1", "segment_2"});
EXPECT_TRUE(copy_result.has_value());
// verify the copy task is created and assigned to the client who executed
// the copy
auto task = service_->QueryTask(copy_result.value());
EXPECT_TRUE(task.has_value());
EXPECT_EQ(TaskType::REPLICA_COPY, task.value().type);
EXPECT_EQ(contexts[0].client_id, task.value().assigned_client);
// Copy with empty targets should fail
auto copy_result1 = service_->CreateCopyTask(key1, {});
EXPECT_FALSE(copy_result1.has_value());
EXPECT_EQ(ErrorCode::INVALID_PARAMS, copy_result1.error());
// Copy not exist key should fail
auto copy_result2 =
service_->CreateCopyTask("not_exist_key", {"segment_1"});
EXPECT_FALSE(copy_result2.has_value());
EXPECT_EQ(ErrorCode::OBJECT_NOT_FOUND, copy_result2.error());
// Copy to segment that not mounted should fail
auto copy_result3 = service_->CreateCopyTask(key1, {"not_mounted_segment"});
EXPECT_FALSE(copy_result3.has_value());
EXPECT_EQ(ErrorCode::INVALID_PARAMS, copy_result3.error());
}
TEST_F(MasterServiceTest, CreateMoveTaskTest) {
// Reset storage space metrics.
MasterMetricManager::instance().reset_allocated_mem_size();
MasterMetricManager::instance().reset_total_mem_capacity();
// Create MasterService
std::unique_ptr<MasterService> service_(new MasterService());
// Mount 3 segments.
constexpr size_t kReplicaCnt = 3;
constexpr size_t kBaseAddr = 0x300000000;
constexpr size_t kSegmentSize = 1024 * 1024 * 16; // 16MB
std::vector<MountedSegmentContext> contexts;
contexts.reserve(kReplicaCnt);
for (size_t i = 0; i < kReplicaCnt; ++i) {
const auto context = PrepareSimpleSegment(
*service_, "segment_" + std::to_string(i),
kBaseAddr + static_cast<size_t>(i) * kSegmentSize, kSegmentSize);
contexts.push_back(context);
}
// The client putting the object to segment_0
auto client_id = generate_uuid();
std::string key1 = "test_key_1";
uint64_t value_length = 6 * 1024 * 1024; // 6MB
uint64_t slice_length = value_length;
ReplicateConfig config;
config.replica_num = 1;
config.preferred_segment = "segment_0";
auto put_start_result =
service_->PutStart(client_id, key1, slice_length, config);
EXPECT_TRUE(put_start_result.has_value());
auto put_end_result =
service_->PutEnd(client_id, key1, ReplicaType::MEMORY);
EXPECT_TRUE(put_end_result.has_value());
// Move key1 from "segment_0" to "segment_1"
auto move_result = service_->CreateMoveTask(key1, "segment_0", "segment_1");
EXPECT_TRUE(move_result.has_value());
// Verify the move task is created and assigned to the client owning the
// source segment
auto task = service_->QueryTask(move_result.value());
EXPECT_TRUE(task.has_value());
EXPECT_EQ(TaskType::REPLICA_MOVE, task.value().type);
EXPECT_EQ(contexts[0].client_id, task.value().assigned_client);
// Move non-existent key should fail
auto move_result1 =
service_->CreateMoveTask("not_exist_key", "segment_0", "segment_1");
EXPECT_FALSE(move_result1.has_value());
EXPECT_EQ(ErrorCode::OBJECT_NOT_FOUND, move_result1.error());
// Move to segment that is same as source should fail
auto move_result_same =
service_->CreateMoveTask(key1, "segment_1", "segment_1");
EXPECT_FALSE(move_result_same.has_value());
EXPECT_EQ(ErrorCode::INVALID_PARAMS, move_result_same.error());
// Move to segment that is not mounted should fail
auto move_result2 =
service_->CreateMoveTask(key1, "segment_0", "not_mounted_segment");
EXPECT_FALSE(move_result2.has_value());
EXPECT_EQ(ErrorCode::INVALID_PARAMS, move_result2.error());
// Move from segment that does not have the replica should fail
auto move_result3 =
service_->CreateMoveTask(key1, "segment_2", "segment_1");
EXPECT_FALSE(move_result3.has_value());
EXPECT_EQ(ErrorCode::INVALID_PARAMS, move_result3.error());
// Move from segment that is not mounted should fail
auto move_result4 =
service_->CreateMoveTask(key1, "not_mounted_segment", "segment_1");
EXPECT_FALSE(move_result4.has_value());
EXPECT_EQ(ErrorCode::INVALID_PARAMS, move_result4.error());
}
TEST_F(MasterServiceTest, QueryTaskTest) {
// Reset storage space metrics.
MasterMetricManager::instance().reset_allocated_mem_size();
MasterMetricManager::instance().reset_total_mem_capacity();
// Create MasterService
std::unique_ptr<MasterService> service_(new MasterService());
// Mount 3 segments.
constexpr size_t kReplicaCnt = 3;
constexpr size_t kBaseAddr = 0x300000000;
constexpr size_t kSegmentSize = 1024 * 1024 * 16; // 16MB
std::vector<MountedSegmentContext> contexts;
contexts.reserve(kReplicaCnt);
for (size_t i = 0; i < kReplicaCnt; ++i) {
const auto context = PrepareSimpleSegment(
*service_, "segment_" + std::to_string(i),
kBaseAddr + static_cast<size_t>(i) * kSegmentSize, kSegmentSize);
contexts.push_back(context);
}
// The client putting the object to segment_0
auto client_id = generate_uuid();
std::string key1 = "test_key_1";
uint64_t value_length = 6 * 1024 * 1024; // 6MB
uint64_t slice_length = value_length;
ReplicateConfig config;
config.replica_num = 1;
config.preferred_segment = "segment_0";
auto put_start_result =
service_->PutStart(client_id, key1, slice_length, config);
EXPECT_TRUE(put_start_result.has_value());
auto put_end_result =
service_->PutEnd(client_id, key1, ReplicaType::MEMORY);
EXPECT_TRUE(put_end_result.has_value());
// Move key1 from "segment_0" to "segment_1"
auto move_result = service_->CreateMoveTask(key1, "segment_0", "segment_1");
EXPECT_TRUE(move_result.has_value());
// Query non-existent task should fail
auto query_result = service_->QueryTask(UUID{0, 0});
EXPECT_FALSE(query_result.has_value());
EXPECT_EQ(ErrorCode::TASK_NOT_FOUND, query_result.error());
// Query the move task
auto query_result_move = service_->QueryTask(move_result.value());
EXPECT_TRUE(query_result_move.has_value());
EXPECT_EQ(TaskType::REPLICA_MOVE, query_result_move.value().type);
EXPECT_EQ(contexts[0].client_id, query_result_move.value().assigned_client);
}
TEST_F(MasterServiceTest, FetchTasksEmptyWhenNoTasks) {
std::unique_ptr<MasterService> service_(new MasterService());
const auto ctx0 = PrepareSimpleSegment(*service_, "segment_0", 0x300000000,
kDefaultSegmentSize);
auto fetch = service_->FetchTasks(ctx0.client_id, /*batch_size=*/16);
ASSERT_TRUE(fetch.has_value());
EXPECT_TRUE(fetch->empty());
}
TEST_F(MasterServiceTest, FetchTasksReturnsAssignedTasksOnlyAndDrainsQueue) {
std::unique_ptr<MasterService> service_(new MasterService());
const auto ctx0 = PrepareSimpleSegment(*service_, "segment_0", 0x300000000,
kDefaultSegmentSize);
const auto ctx1 = PrepareSimpleSegment(*service_, "segment_1", 0x400000000,
kDefaultSegmentSize);
// Put an object with its (only) replica on segment_0 so Copy/Move
// assignment is deterministic.
const UUID put_client_id = generate_uuid();
const std::string key = "fetch_tasks_key_0";
ReplicateConfig config;
config.replica_num = 1;
config.preferred_segment = "segment_0";
ASSERT_TRUE(
service_->PutStart(put_client_id, key, /*slice_length=*/1024, config)
.has_value());
ASSERT_TRUE(
service_->PutEnd(put_client_id, key, ReplicaType::MEMORY).has_value());
// Create two tasks; both should be assigned to the client owning source
// segment_0.
auto copy_task_id = service_->CreateCopyTask(key, {"segment_1"});
ASSERT_TRUE(copy_task_id.has_value());
auto move_task_id = service_->CreateMoveTask(key, "segment_0", "segment_1");
ASSERT_TRUE(move_task_id.has_value());
// Fetch from client_0 should get both tasks (order not guaranteed).
auto fetch0 = service_->FetchTasks(ctx0.client_id, /*batch_size=*/16);
ASSERT_TRUE(fetch0.has_value());
ASSERT_EQ(fetch0->size(), 2u);
std::vector<UUID> fetched_ids;
fetched_ids.reserve(fetch0->size());
for (const auto& a : *fetch0) {
fetched_ids.push_back(
a.id); // TaskAssignment is expected to carry id/type/payload
}
EXPECT_NE(
std::find(fetched_ids.begin(), fetched_ids.end(), copy_task_id.value()),
fetched_ids.end());
EXPECT_NE(
std::find(fetched_ids.begin(), fetched_ids.end(), move_task_id.value()),
fetched_ids.end());
// Fetch from client_1 should return empty (no tasks assigned to it).
auto fetch1 = service_->FetchTasks(ctx1.client_id, /*batch_size=*/16);
ASSERT_TRUE(fetch1.has_value());
EXPECT_TRUE(fetch1->empty());
// Fetch again from client_0 should be empty if pop_tasks drains pending
// queue.
auto fetch0_again = service_->FetchTasks(ctx0.client_id, /*batch_size=*/16);
ASSERT_TRUE(fetch0_again.has_value());
EXPECT_TRUE(fetch0_again->empty());
}
TEST_F(MasterServiceTest, FetchTasksRespectsBatchSize) {
std::unique_ptr<MasterService> service_(new MasterService());
const auto ctx0 = PrepareSimpleSegment(*service_, "segment_0", 0x300000000,
kDefaultSegmentSize);
[[maybe_unused]] const auto ctx1 = PrepareSimpleSegment(
*service_, "segment_1", 0x400000000, kDefaultSegmentSize);
const UUID put_client_id = generate_uuid();
const std::string key = "fetch_tasks_key_1";
ReplicateConfig config;
config.replica_num = 1;
config.preferred_segment = "segment_0";
ASSERT_TRUE(
service_->PutStart(put_client_id, key, /*slice_length=*/1024, config)
.has_value());
ASSERT_TRUE(
service_->PutEnd(put_client_id, key, ReplicaType::MEMORY).has_value());
auto t1 = service_->CreateCopyTask(key, {"segment_1"});
ASSERT_TRUE(t1.has_value());
auto t2 = service_->CreateMoveTask(key, "segment_0", "segment_1");
ASSERT_TRUE(t2.has_value());
auto fetch_first = service_->FetchTasks(ctx0.client_id, /*batch_size=*/1);
ASSERT_TRUE(fetch_first.has_value());
ASSERT_EQ(fetch_first->size(), 1u);
auto fetch_second = service_->FetchTasks(ctx0.client_id, /*batch_size=*/1);
ASSERT_TRUE(fetch_second.has_value());
ASSERT_EQ(fetch_second->size(), 1u);
// Combined should contain both task ids (order not guaranteed).
std::vector<UUID> ids;
ids.push_back(fetch_first->at(0).id);
ids.push_back(fetch_second->at(0).id);
EXPECT_NE(std::find(ids.begin(), ids.end(), t1.value()), ids.end());
EXPECT_NE(std::find(ids.begin(), ids.end(), t2.value()), ids.end());
auto fetch_third = service_->FetchTasks(ctx0.client_id, /*batch_size=*/1);
ASSERT_TRUE(fetch_third.has_value());
EXPECT_TRUE(fetch_third->empty());
}
TEST_F(MasterServiceTest, UpdateTaskSuccessFlow) {
auto service_ = std::make_unique<MasterService>();
const auto ctx0 = PrepareSimpleSegment(*service_, "segment_0", 0x300000000,
kDefaultSegmentSize);
[[maybe_unused]] const auto ctx1 = PrepareSimpleSegment(
*service_, "segment_1", 0x400000000, kDefaultSegmentSize);
// Put an object with its (only) replica on segment_0 so task assignment is
// deterministic.
const UUID put_client_id = generate_uuid();
const std::string key = "update_task_key_success";
ReplicateConfig config;
config.replica_num = 1;
config.preferred_segment = "segment_0";
ASSERT_TRUE(
service_->PutStart(put_client_id, key, /*slice_length=*/1024, config)
.has_value());
ASSERT_TRUE(
service_->PutEnd(put_client_id, key, ReplicaType::MEMORY).has_value());
// Create a task assigned to client owning segment_0.
auto task_id_res = service_->CreateCopyTask(key, {"segment_1"});
ASSERT_TRUE(task_id_res.has_value());
const UUID task_id = task_id_res.value();
// Poll once so the task transitions to PROCESSING (typical semantics).
auto fetched = service_->FetchTasks(ctx0.client_id, /*batch_size=*/16);
ASSERT_TRUE(fetched.has_value());
ASSERT_EQ(fetched->size(), 1u);
EXPECT_EQ(fetched->at(0).id, task_id);
// Update task to SUCCESS.
TaskCompleteRequest req{};
req.id = task_id;
req.status = TaskStatus::SUCCESS;
req.message = "done";
auto update_res = service_->MarkTaskToComplete(ctx0.client_id, req);
ASSERT_TRUE(update_res.has_value()) << "MarkTaskToComplete failed";
// Verify task state via QueryTask.
auto qt = service_->QueryTask(task_id);
ASSERT_TRUE(qt.has_value());
EXPECT_EQ(qt->id, task_id);
EXPECT_EQ(qt->status, TaskStatus::SUCCESS);
EXPECT_EQ(qt->assigned_client, ctx0.client_id);
EXPECT_EQ(qt->message, "done");
// Queue should be drained for that client.
auto fetched_again =
service_->FetchTasks(ctx0.client_id, /*batch_size=*/16);
ASSERT_TRUE(fetched_again.has_value());
EXPECT_TRUE(fetched_again->empty());
}
TEST_F(MasterServiceTest, UpdateTaskRejectsWrongClient) {
auto service_ = std::make_unique<MasterService>();
const auto ctx0 = PrepareSimpleSegment(*service_, "segment_0", 0x300000000,
kDefaultSegmentSize);
const auto ctx1 = PrepareSimpleSegment(*service_, "segment_1", 0x400000000,
kDefaultSegmentSize);
const UUID put_client_id = generate_uuid();
const std::string key = "update_task_wrong_client";
ReplicateConfig config;
config.replica_num = 1;
config.preferred_segment = "segment_0";
ASSERT_TRUE(
service_->PutStart(put_client_id, key, /*slice_length=*/1024, config)
.has_value());
ASSERT_TRUE(
service_->PutEnd(put_client_id, key, ReplicaType::MEMORY).has_value());
auto task_id_res = service_->CreateMoveTask(key, "segment_0", "segment_1");
ASSERT_TRUE(task_id_res.has_value());
const UUID task_id = task_id_res.value();
// Poll by the correct client to take the task.
auto fetched = service_->FetchTasks(ctx0.client_id, /*batch_size=*/16);
ASSERT_TRUE(fetched.has_value());
ASSERT_EQ(fetched->size(), 1u);
EXPECT_EQ(fetched->at(0).id, task_id);
// Try to update with a different client id, should fail.
TaskCompleteRequest req{};
req.id = task_id;
req.status = TaskStatus::SUCCESS;
req.message = "should_not_work";
auto update_res = service_->MarkTaskToComplete(ctx1.client_id, req);
ASSERT_FALSE(update_res.has_value());
EXPECT_EQ(update_res.error(), ErrorCode::ILLEGAL_CLIENT);
}
TEST_F(MasterServiceTest, UpdateTaskNotFound) {
auto service_ = std::make_unique<MasterService>();
const auto ctx0 = PrepareSimpleSegment(*service_, "segment_0", 0x300000000,
kDefaultSegmentSize);
TaskCompleteRequest req{};
req.id = generate_uuid(); // non-existent task id
req.status = TaskStatus::FAILED;
req.message = "not_found";
auto update_res = service_->MarkTaskToComplete(ctx0.client_id, req);
ASSERT_FALSE(update_res.has_value());
EXPECT_EQ(update_res.error(), ErrorCode::TASK_NOT_FOUND);
}
TEST_F(MasterServiceTest,
CreateDrainJobMarksSegmentDrainingAndSkipsAllocation) {
auto service_config =
MasterServiceConfig::builder().set_default_kv_lease_ttl(0).build();
auto service_ = std::make_unique<MasterService>(service_config);
const auto ctx0 = PrepareSimpleSegment(*service_, "segment_0", 0x300000000,
kDefaultSegmentSize);
[[maybe_unused]] const auto ctx1 = PrepareSimpleSegment(
*service_, "segment_1", 0x400000000, kDefaultSegmentSize);
CreateDrainJobRequest request;
request.segments = {"segment_0"};
request.target_segments = {"segment_1"};
request.max_concurrency = 1;
auto job_id = service_->CreateDrainJob(request);
ASSERT_TRUE(job_id.has_value());
auto segment_status = service_->QuerySegmentStatus("segment_0");
ASSERT_TRUE(segment_status.has_value());
EXPECT_EQ(segment_status.value(), SegmentStatus::DRAINING);
ReplicateConfig config;
config.replica_num = 1;
config.preferred_segment = "segment_0";
auto put_result = service_->PutStart(
ctx0.client_id, "drain_skip_allocation_key", 1024, config);
ASSERT_TRUE(put_result.has_value());
ASSERT_EQ(put_result->size(), 1u);
EXPECT_EQ(put_result->front()
.get_memory_descriptor()
.buffer_descriptor.transport_endpoint_,
"segment_1");
ASSERT_TRUE(service_
->PutEnd(ctx0.client_id, "drain_skip_allocation_key",
ReplicaType::MEMORY)
.has_value());
}
TEST_F(MasterServiceTest, DrainJobSchedulesMoveTaskAndConvergesToDrained) {
auto service_config =
MasterServiceConfig::builder().set_default_kv_lease_ttl(0).build();
auto service_ = std::make_unique<MasterService>(service_config);
const auto ctx0 = PrepareSimpleSegment(*service_, "segment_0", 0x300000000,
kDefaultSegmentSize);
[[maybe_unused]] const auto ctx1 = PrepareSimpleSegment(
*service_, "segment_1", 0x400000000, kDefaultSegmentSize);
const UUID put_client_id = generate_uuid();
const std::string key =
PutObjectOnSegment(*service_, put_client_id, "segment_0");
CreateDrainJobRequest request;
request.segments = {"segment_0"};
request.target_segments = {"segment_1"};
request.max_concurrency = 1;
auto job_id = service_->CreateDrainJob(request);
ASSERT_TRUE(job_id.has_value());
WaitUntil(
[&] { return ExecutePendingMoveTasks(*service_, ctx0.client_id); });
WaitUntil([&] {
auto query = service_->QueryDrainJob(job_id.value());
return query.has_value() && query->status == JobStatus::SUCCEEDED;
});
auto query = service_->QueryDrainJob(job_id.value());
ASSERT_TRUE(query.has_value());
EXPECT_EQ(query->status, JobStatus::SUCCEEDED);
EXPECT_EQ(query->active_units, 0u);
EXPECT_GE(query->succeeded_units, 1u);
auto segment_status = service_->QuerySegmentStatus("segment_0");
ASSERT_TRUE(segment_status.has_value());
EXPECT_EQ(segment_status.value(), SegmentStatus::DRAINED);
auto replicas = service_->GetReplicaList(key);
ASSERT_TRUE(replicas.has_value());
std::unordered_set<std::string> segment_names;
for (const auto& replica : replicas->replicas) {
segment_names.insert(replica.get_memory_descriptor()
.buffer_descriptor.transport_endpoint_);
}
EXPECT_TRUE(segment_names.contains("segment_1"));
EXPECT_FALSE(segment_names.contains("segment_0"));
}
TEST_F(MasterServiceTest, CancelDrainJobRestoresSegmentStatus) {
auto service_ = std::make_unique<MasterService>();
[[maybe_unused]] const auto ctx0 = PrepareSimpleSegment(
*service_, "segment_0", 0x300000000, kDefaultSegmentSize);
[[maybe_unused]] const auto ctx1 = PrepareSimpleSegment(
*service_, "segment_1", 0x400000000, kDefaultSegmentSize);
CreateDrainJobRequest request;
request.segments = {"segment_0"};
request.target_segments = {"segment_1"};
request.max_concurrency = 1;
auto job_id = service_->CreateDrainJob(request);
ASSERT_TRUE(job_id.has_value());
auto draining_status = service_->QuerySegmentStatus("segment_0");
ASSERT_TRUE(draining_status.has_value());
EXPECT_EQ(draining_status.value(), SegmentStatus::DRAINING);
auto cancel_result = service_->CancelDrainJob(job_id.value());
ASSERT_TRUE(cancel_result.has_value());
auto job = service_->QueryDrainJob(job_id.value());
ASSERT_TRUE(job.has_value());
EXPECT_EQ(job->status, JobStatus::CANCELED);
auto restored_status = service_->QuerySegmentStatus("segment_0");
ASSERT_TRUE(restored_status.has_value());
EXPECT_EQ(restored_status.value(), SegmentStatus::OK);
}
TEST_F(MasterServiceTest, CancelDrainJobRejectsActiveMoveTasks) {
auto service_config =
MasterServiceConfig::builder().set_default_kv_lease_ttl(0).build();
auto service_ = std::make_unique<MasterService>(service_config);
const auto ctx0 = PrepareSimpleSegment(*service_, "segment_0", 0x300000000,
kDefaultSegmentSize);
[[maybe_unused]] const auto ctx1 = PrepareSimpleSegment(
*service_, "segment_1", 0x400000000, kDefaultSegmentSize);
const UUID put_client_id = generate_uuid();
PutObjectOnSegment(*service_, put_client_id, "segment_0");
CreateDrainJobRequest request;
request.segments = {"segment_0"};
request.target_segments = {"segment_1"};
request.max_concurrency = 1;
auto job_id = service_->CreateDrainJob(request);
ASSERT_TRUE(job_id.has_value());
WaitUntil([&] {
auto fetched = service_->FetchTasks(ctx0.client_id, /*batch_size=*/16);
return fetched.has_value() && !fetched->empty();
});
auto cancel_result = service_->CancelDrainJob(job_id.value());
ASSERT_FALSE(cancel_result.has_value());
EXPECT_EQ(cancel_result.error(), ErrorCode::UNAVAILABLE_IN_CURRENT_STATUS);
}
TEST_F(MasterServiceTest, DrainJobFailsAfterRetryBudgetExhausted) {
auto service_config =
MasterServiceConfig::builder().set_default_kv_lease_ttl(0).build();
auto service_ = std::make_unique<MasterService>(service_config);
const auto ctx0 = PrepareSimpleSegment(*service_, "segment_0", 0x300000000,
kDefaultSegmentSize);
[[maybe_unused]] const auto ctx1 = PrepareSimpleSegment(
*service_, "segment_1", 0x400000000, kDefaultSegmentSize);
const UUID put_client_id = generate_uuid();
PutObjectOnSegment(*service_, put_client_id, "segment_0");
CreateDrainJobRequest request;
request.segments = {"segment_0"};
request.target_segments = {"segment_1"};
request.max_concurrency = 1;
auto job_id = service_->CreateDrainJob(request);
ASSERT_TRUE(job_id.has_value());
for (int attempt = 0; attempt < 3; ++attempt) {
WaitUntil(
[&] { return FailPendingMoveTasks(*service_, ctx0.client_id); });
}
WaitUntil([&] {
auto query = service_->QueryDrainJob(job_id.value());
return query.has_value() && query->status == JobStatus::FAILED;
});
auto query = service_->QueryDrainJob(job_id.value());
ASSERT_TRUE(query.has_value());
EXPECT_EQ(query->status, JobStatus::FAILED);
EXPECT_EQ(query->active_units, 0u);
EXPECT_GE(query->failed_units, 3u);
auto segment_status = service_->QuerySegmentStatus("segment_0");
ASSERT_TRUE(segment_status.has_value());
EXPECT_EQ(segment_status.value(), SegmentStatus::OK);
}
TEST_F(MasterServiceTest, ForceRemoveLeasedObject) {
// Set a long lease TTL so objects will have active leases
const uint64_t kv_lease_ttl = 10000; // 10 seconds
auto service_config = MasterServiceConfig::builder()
.set_default_kv_lease_ttl(kv_lease_ttl)
.build();
std::unique_ptr<MasterService> service_(new MasterService(service_config));
[[maybe_unused]] const auto context = PrepareSimpleSegment(*service_);
const UUID client_id = generate_uuid();
// Put an object
std::string key = "leased_key";
uint64_t slice_length = 1024;
ReplicateConfig config;
config.replica_num = 1;
auto put_start_result =
service_->PutStart(client_id, key, slice_length, config);
ASSERT_TRUE(put_start_result.has_value());
auto put_end_result = service_->PutEnd(client_id, key, ReplicaType::MEMORY);
ASSERT_TRUE(put_end_result.has_value());
// Verify object exists
auto exist_result = service_->ExistKey(key);
ASSERT_TRUE(exist_result.has_value());
ASSERT_TRUE(exist_result.value());
// Normal remove should fail because object has active lease
auto remove_result_no_force = service_->Remove(key, false);
EXPECT_FALSE(remove_result_no_force.has_value());
EXPECT_EQ(ErrorCode::OBJECT_HAS_LEASE, remove_result_no_force.error());
// Force remove should succeed even with active lease
auto remove_result_force = service_->Remove(key, true);
EXPECT_TRUE(remove_result_force.has_value());
// Verify object is removed
auto get_result = service_->GetReplicaList(key);
EXPECT_FALSE(get_result.has_value());
EXPECT_EQ(ErrorCode::OBJECT_NOT_FOUND, get_result.error());
}
// Test force RemoveByRegex - should bypass lease check
TEST_F(MasterServiceTest, ForceRemoveByRegexLeasedObjects) {
// Set a long lease TTL so objects will have active leases
const uint64_t kv_lease_ttl = 10000; // 10 seconds
auto service_config = MasterServiceConfig::builder()
.set_default_kv_lease_ttl(kv_lease_ttl)
.build();
std::unique_ptr<MasterService> service_(new MasterService(service_config));
[[maybe_unused]] const auto context = PrepareSimpleSegment(*service_);
const UUID client_id = generate_uuid();
// Put 5 objects and grant them active leases by reading
for (int i = 0; i < 5; ++i) {
std::string key = "force_regex_key_" + std::to_string(i);
uint64_t slice_length = 1024;
ReplicateConfig config;
config.replica_num = 1;
auto put_start_result =
service_->PutStart(client_id, key, slice_length, config);
ASSERT_TRUE(put_start_result.has_value());
auto put_end_result =
service_->PutEnd(client_id, key, ReplicaType::MEMORY);
ASSERT_TRUE(put_end_result.has_value());
// Grant lease by reading the object
auto exist_result = service_->ExistKey(key);
ASSERT_TRUE(exist_result.has_value());
ASSERT_TRUE(exist_result.value());
}
// Normal RemoveByRegex should remove 0 because all objects have active
// leases
auto remove_result_no_force =
service_->RemoveByRegex("^force_regex_key_", false);
ASSERT_TRUE(remove_result_no_force.has_value());
EXPECT_EQ(0, remove_result_no_force.value());
// All objects should still exist
for (int i = 0; i < 5; ++i) {
std::string key = "force_regex_key_" + std::to_string(i);
auto exist_result = service_->ExistKey(key);
ASSERT_TRUE(exist_result.has_value());
ASSERT_TRUE(exist_result.value());
}
// Force RemoveByRegex should remove all 5 objects
auto remove_result_force =
service_->RemoveByRegex("^force_regex_key_", true);
ASSERT_TRUE(remove_result_force.has_value());
EXPECT_EQ(5, remove_result_force.value());
// All objects should be removed
for (int i = 0; i < 5; ++i) {
std::string key = "force_regex_key_" + std::to_string(i);
auto exist_result = service_->ExistKey(key);
ASSERT_TRUE(exist_result.has_value());
ASSERT_FALSE(exist_result.value());
}
}
// Test force RemoveAll - should bypass lease check
TEST_F(MasterServiceTest, ForceRemoveAllLeasedObjects) {
// Set a long lease TTL so objects will have active leases
const uint64_t kv_lease_ttl = 10000; // 10 seconds
auto service_config = MasterServiceConfig::builder()
.set_default_kv_lease_ttl(kv_lease_ttl)
.build();
std::unique_ptr<MasterService> service_(new MasterService(service_config));
[[maybe_unused]] const auto context = PrepareSimpleSegment(*service_);
const UUID client_id = generate_uuid();
// Put 10 objects and grant them active leases by reading
for (int i = 0; i < 10; ++i) {
std::string key = "force_all_key_" + std::to_string(i);
uint64_t slice_length = 1024;
ReplicateConfig config;
config.replica_num = 1;
auto put_start_result =
service_->PutStart(client_id, key, slice_length, config);
ASSERT_TRUE(put_start_result.has_value());
auto put_end_result =
service_->PutEnd(client_id, key, ReplicaType::MEMORY);
ASSERT_TRUE(put_end_result.has_value());
// Grant lease by reading the object
auto exist_result = service_->ExistKey(key);
ASSERT_TRUE(exist_result.has_value());
ASSERT_TRUE(exist_result.value());
}
// Normal RemoveAll should remove 0 because all objects have active leases
EXPECT_EQ(0, service_->RemoveAll(false));
// All objects should still exist
for (int i = 0; i < 10; ++i) {
std::string key = "force_all_key_" + std::to_string(i);
auto exist_result = service_->ExistKey(key);
ASSERT_TRUE(exist_result.has_value());
ASSERT_TRUE(exist_result.value());
}
// Force RemoveAll should remove all 10 objects
EXPECT_EQ(10, service_->RemoveAll(true));
// All objects should be removed
for (int i = 0; i < 10; ++i) {
std::string key = "force_all_key_" + std::to_string(i);
auto exist_result = service_->ExistKey(key);
ASSERT_TRUE(exist_result.has_value());
ASSERT_FALSE(exist_result.value());
}
}
// ===================== Upsert Tests =====================
TEST_F(MasterServiceTest, UpsertNewKey) {
// Case A: key does not exist — behaves like PutStart
std::unique_ptr<MasterService> service_(new MasterService());
[[maybe_unused]] const auto context = PrepareSimpleSegment(*service_);
const UUID client_id = generate_uuid();
std::string key = "upsert_new_key";
uint64_t slice_length = 1024;
ReplicateConfig config;
config.replica_num = 1;
auto upsert_result =
service_->UpsertStart(client_id, key, slice_length, config);
ASSERT_TRUE(upsert_result.has_value());
auto replicas = upsert_result.value();
EXPECT_EQ(1, replicas.size());
EXPECT_EQ(ReplicaStatus::PROCESSING, replicas[0].status);
// During upsert, GetReplicaList should return not ready
auto get_result = service_->GetReplicaList(key);
EXPECT_FALSE(get_result.has_value());
EXPECT_EQ(ErrorCode::REPLICA_IS_NOT_READY, get_result.error());
// UpsertEnd completes the operation
auto end_result = service_->UpsertEnd(client_id, key, ReplicaType::MEMORY);
ASSERT_TRUE(end_result.has_value());
// Verify replica is COMPLETE
auto final_result = service_->GetReplicaList(key);
ASSERT_TRUE(final_result.has_value());
EXPECT_EQ(1, final_result.value().replicas.size());
EXPECT_EQ(ReplicaStatus::COMPLETE, final_result.value().replicas[0].status);
}
TEST_F(MasterServiceTest, UpsertSameSize) {
// Case B: key exists with same size — in-place update
std::unique_ptr<MasterService> service_(new MasterService());
[[maybe_unused]] const auto context = PrepareSimpleSegment(*service_);
const UUID client_id = generate_uuid();
std::string key = "upsert_same_size";
uint64_t slice_length = 1024;
ReplicateConfig config;
config.replica_num = 1;
// First: PutStart + PutEnd to create the object
auto put_result = service_->PutStart(client_id, key, slice_length, config);
ASSERT_TRUE(put_result.has_value());
auto original_replicas = put_result.value();
auto put_end = service_->PutEnd(client_id, key, ReplicaType::MEMORY);
ASSERT_TRUE(put_end.has_value());
// UpsertStart with same size — should reuse buffers
const UUID new_client_id = generate_uuid();
auto upsert_result =
service_->UpsertStart(new_client_id, key, slice_length, config);
ASSERT_TRUE(upsert_result.has_value());
auto upsert_replicas = upsert_result.value();
EXPECT_EQ(1, upsert_replicas.size());
EXPECT_EQ(ReplicaStatus::PROCESSING, upsert_replicas[0].status);
// Verify same buffer address (in-place reuse)
EXPECT_EQ(original_replicas[0]
.get_memory_descriptor()
.buffer_descriptor.buffer_address_,
upsert_replicas[0]
.get_memory_descriptor()
.buffer_descriptor.buffer_address_);
// UpsertEnd with the new client_id
auto end_result =
service_->UpsertEnd(new_client_id, key, ReplicaType::MEMORY);
ASSERT_TRUE(end_result.has_value());
// Verify replica is COMPLETE again
auto final_result = service_->GetReplicaList(key);
ASSERT_TRUE(final_result.has_value());
EXPECT_EQ(ReplicaStatus::COMPLETE, final_result.value().replicas[0].status);
}
TEST_F(MasterServiceTest, UpsertSameSizeRefreshesMetadata) {
// Case B: verify client_id and put_start_time are refreshed
std::unique_ptr<MasterService> service_(new MasterService());
[[maybe_unused]] const auto context = PrepareSimpleSegment(*service_);
const UUID client_id_a = generate_uuid();
const UUID client_id_b = generate_uuid();
std::string key = "upsert_refresh_metadata";
uint64_t slice_length = 1024;
ReplicateConfig config;
config.replica_num = 1;
// Create object with client_a
auto put_result =
service_->PutStart(client_id_a, key, slice_length, config);
ASSERT_TRUE(put_result.has_value());
auto put_end = service_->PutEnd(client_id_a, key, ReplicaType::MEMORY);
ASSERT_TRUE(put_end.has_value());
// UpsertStart with client_b
auto upsert_result =
service_->UpsertStart(client_id_b, key, slice_length, config);
ASSERT_TRUE(upsert_result.has_value());
// UpsertEnd with client_a should fail (client_id was refreshed to client_b)
auto end_fail = service_->UpsertEnd(client_id_a, key, ReplicaType::MEMORY);
EXPECT_FALSE(end_fail.has_value());
EXPECT_EQ(ErrorCode::ILLEGAL_CLIENT, end_fail.error());
// UpsertEnd with client_b should succeed
auto end_ok = service_->UpsertEnd(client_id_b, key, ReplicaType::MEMORY);
ASSERT_TRUE(end_ok.has_value());
}
TEST_F(MasterServiceTest, UpsertDifferentSize) {
// Case C: key exists with different size — delete and reallocate
std::unique_ptr<MasterService> service_(new MasterService());
[[maybe_unused]] const auto context = PrepareSimpleSegment(*service_);
const UUID client_id = generate_uuid();
std::string key = "upsert_diff_size";
uint64_t original_size = 1024;
uint64_t new_size = 2048;
ReplicateConfig config;
config.replica_num = 1;
// Create object with original_size
auto put_result = service_->PutStart(client_id, key, original_size, config);
ASSERT_TRUE(put_result.has_value());
auto original_replicas = put_result.value();
auto put_end = service_->PutEnd(client_id, key, ReplicaType::MEMORY);
ASSERT_TRUE(put_end.has_value());
// UpsertStart with different size
auto upsert_result =
service_->UpsertStart(client_id, key, new_size, config);
ASSERT_TRUE(upsert_result.has_value());
auto new_replicas = upsert_result.value();
EXPECT_EQ(1, new_replicas.size());
EXPECT_EQ(ReplicaStatus::PROCESSING, new_replicas[0].status);
// Buffer address should be different (reallocated)
EXPECT_NE(original_replicas[0]
.get_memory_descriptor()
.buffer_descriptor.buffer_address_,
new_replicas[0]
.get_memory_descriptor()
.buffer_descriptor.buffer_address_);
// UpsertEnd
auto end_result = service_->UpsertEnd(client_id, key, ReplicaType::MEMORY);
ASSERT_TRUE(end_result.has_value());
// Verify the object is complete
auto final_result = service_->GetReplicaList(key);
ASSERT_TRUE(final_result.has_value());
EXPECT_EQ(ReplicaStatus::COMPLETE, final_result.value().replicas[0].status);
}
TEST_F(MasterServiceTest, UpsertConflictReplicationTask) {
// Upsert should fail if Copy is in progress
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));
[[maybe_unused]] const auto ctx1 =
PrepareSimpleSegment(*service_, "segment_1");
[[maybe_unused]] const auto ctx2 =
PrepareSimpleSegment(*service_, "segment_2");
UUID client_id = generate_uuid();
std::string key = "upsert_conflict_copy";
uint64_t slice_length = 1024;
ReplicateConfig config;
config.replica_num = 1;
config.preferred_segment = "segment_1";
// Create object
auto put_result = service_->PutStart(client_id, key, slice_length, config);
ASSERT_TRUE(put_result.has_value());
auto put_end = service_->PutEnd(client_id, key, ReplicaType::MEMORY);
ASSERT_TRUE(put_end.has_value());
// Start a Copy
auto copy_result =
service_->CopyStart(client_id, key, "segment_1", {"segment_2"});
ASSERT_TRUE(copy_result.has_value());
// UpsertStart should fail with OBJECT_HAS_REPLICATION_TASK
auto upsert_result =
service_->UpsertStart(client_id, key, slice_length, config);
EXPECT_FALSE(upsert_result.has_value());
EXPECT_EQ(ErrorCode::OBJECT_HAS_REPLICATION_TASK, upsert_result.error());
}
TEST_F(MasterServiceTest, UpsertPreemptsInProgressPut) {
// Upsert should preempt an in-progress Put (no discard timeout needed
// for preemption via Upsert — Upsert always preempts immediately)
std::unique_ptr<MasterService> service_(new MasterService());
[[maybe_unused]] const auto context = PrepareSimpleSegment(*service_);
const UUID client_a = generate_uuid();
const UUID client_b = generate_uuid();
std::string key = "upsert_preempt";
uint64_t slice_length = 1024;
ReplicateConfig config;
config.replica_num = 1;
// Client A starts a Put but doesn't finish
auto put_result = service_->PutStart(client_a, key, slice_length, config);
ASSERT_TRUE(put_result.has_value());
// Client B upserts the same key — should preempt client A
auto upsert_result =
service_->UpsertStart(client_b, key, slice_length, config);
ASSERT_TRUE(upsert_result.has_value());
auto upsert_replicas = upsert_result.value();
EXPECT_EQ(1, upsert_replicas.size());
EXPECT_EQ(ReplicaStatus::PROCESSING, upsert_replicas[0].status);
// Client A's PutEnd should fail
auto put_end_a = service_->PutEnd(client_a, key, ReplicaType::MEMORY);
EXPECT_FALSE(put_end_a.has_value());
// Client B's UpsertEnd should succeed
auto upsert_end = service_->UpsertEnd(client_b, key, ReplicaType::MEMORY);
ASSERT_TRUE(upsert_end.has_value());
// Verify final state
auto final_result = service_->GetReplicaList(key);
ASSERT_TRUE(final_result.has_value());
EXPECT_EQ(ReplicaStatus::COMPLETE, final_result.value().replicas[0].status);
}
TEST_F(MasterServiceTest, UpsertRevoke) {
// UpsertRevoke should clean up like PutRevoke
std::unique_ptr<MasterService> service_(new MasterService());
[[maybe_unused]] const auto context = PrepareSimpleSegment(*service_);
const UUID client_id = generate_uuid();
std::string key = "upsert_revoke";
uint64_t slice_length = 1024;
ReplicateConfig config;
config.replica_num = 1;
// UpsertStart (Case A — new key)
auto upsert_result =
service_->UpsertStart(client_id, key, slice_length, config);
ASSERT_TRUE(upsert_result.has_value());
// UpsertRevoke
auto revoke_result =
service_->UpsertRevoke(client_id, key, ReplicaType::MEMORY);
ASSERT_TRUE(revoke_result.has_value());
// Key should be gone
auto exist_result = service_->ExistKey(key);
ASSERT_TRUE(exist_result.has_value());
EXPECT_FALSE(exist_result.value());
}
TEST_F(MasterServiceTest, UpsertInPlaceThenRevoke) {
// UpsertRevoke after in-place UpsertStart should clean up
std::unique_ptr<MasterService> service_(new MasterService());
[[maybe_unused]] const auto context = PrepareSimpleSegment(*service_);
const UUID client_id = generate_uuid();
std::string key = "upsert_inplace_revoke";
uint64_t slice_length = 1024;
ReplicateConfig config;
config.replica_num = 1;
// Create object first
auto put_result = service_->PutStart(client_id, key, slice_length, config);
ASSERT_TRUE(put_result.has_value());
auto put_end = service_->PutEnd(client_id, key, ReplicaType::MEMORY);
ASSERT_TRUE(put_end.has_value());
// UpsertStart in-place (same size)
const UUID new_client = generate_uuid();
auto upsert_result =
service_->UpsertStart(new_client, key, slice_length, config);
ASSERT_TRUE(upsert_result.has_value());
// UpsertRevoke — replicas are PROCESSING, should be erased
auto revoke_result =
service_->UpsertRevoke(new_client, key, ReplicaType::MEMORY);
ASSERT_TRUE(revoke_result.has_value());
// Key should be gone (no valid replicas left)
auto exist_result = service_->ExistKey(key);
ASSERT_TRUE(exist_result.has_value());
EXPECT_FALSE(exist_result.value());
}
TEST_F(MasterServiceTest, BatchUpsertStart) {
// Test batch upsert with a mix of new and existing keys
std::unique_ptr<MasterService> service_(new MasterService());
[[maybe_unused]] const auto context = PrepareSimpleSegment(*service_);
const UUID client_id = generate_uuid();
ReplicateConfig config;
config.replica_num = 1;
// Create key_1 with size 1024
auto put_result = service_->PutStart(client_id, "key_1", 1024, config);
ASSERT_TRUE(put_result.has_value());
auto put_end = service_->PutEnd(client_id, "key_1", ReplicaType::MEMORY);
ASSERT_TRUE(put_end.has_value());
// BatchUpsertStart: key_1 (same size), key_2 (new)
std::vector<std::string> keys = {"key_1", "key_2"};
std::vector<uint64_t> slice_lengths = {1024, 2048};
auto results =
service_->BatchUpsertStart(client_id, keys, slice_lengths, config);
ASSERT_EQ(2, results.size());
EXPECT_TRUE(results[0].has_value()); // key_1: Case B (in-place)
EXPECT_TRUE(results[1].has_value()); // key_2: Case A (new)
// Complete both
auto end_results = service_->BatchUpsertEnd(client_id, keys);
ASSERT_EQ(2, end_results.size());
EXPECT_TRUE(end_results[0].has_value());
EXPECT_TRUE(end_results[1].has_value());
}
TEST_F(MasterServiceTest, UpsertPreemptsInProgressUpsert) {
// Upsert should preempt an in-progress Upsert (Case B in-place).
// After preemption, all replicas were PROCESSING (no COMPLETE survives),
// so metadata is erased and the new upsert falls through to Case A.
std::unique_ptr<MasterService> service_(new MasterService());
[[maybe_unused]] const auto context = PrepareSimpleSegment(*service_);
const UUID client_a = generate_uuid();
const UUID client_b = generate_uuid();
const UUID client_c = generate_uuid();
std::string key = "upsert_preempt_upsert";
uint64_t slice_length = 1024;
ReplicateConfig config;
config.replica_num = 1;
// Step 1: Create the object via Put
auto put_result = service_->PutStart(client_a, key, slice_length, config);
ASSERT_TRUE(put_result.has_value());
auto put_end = service_->PutEnd(client_a, key, ReplicaType::MEMORY);
ASSERT_TRUE(put_end.has_value());
// Step 2: Client B starts in-place upsert (Case B) — marks COMPLETE →
// PROCESSING
auto upsert_b = service_->UpsertStart(client_b, key, slice_length, config);
ASSERT_TRUE(upsert_b.has_value());
// Key should be unreadable now (all replicas are PROCESSING)
auto get_mid = service_->GetReplicaList(key);
EXPECT_FALSE(get_mid.has_value());
EXPECT_EQ(ErrorCode::REPLICA_IS_NOT_READY, get_mid.error());
// Step 3: Client C upserts the same key — preempts Client B
auto upsert_c = service_->UpsertStart(client_c, key, slice_length, config);
ASSERT_TRUE(upsert_c.has_value());
EXPECT_EQ(1, upsert_c.value().size());
// Step 4: Client B's UpsertEnd should fail (preempted)
auto end_b = service_->UpsertEnd(client_b, key, ReplicaType::MEMORY);
EXPECT_FALSE(end_b.has_value());
// Step 5: Client C's UpsertEnd should succeed
auto end_c = service_->UpsertEnd(client_c, key, ReplicaType::MEMORY);
ASSERT_TRUE(end_c.has_value());
// Final verification
auto final_result = service_->GetReplicaList(key);
ASSERT_TRUE(final_result.has_value());
EXPECT_EQ(1, final_result.value().replicas.size());
EXPECT_EQ(ReplicaStatus::COMPLETE, final_result.value().replicas[0].status);
}
TEST_F(MasterServiceTest, UpsertDifferentSizeThenRevoke) {
// Case C (different size) followed by UpsertRevoke.
// Old replicas go to discarded_replicas_, new replicas are erased by
// revoke. The key should disappear entirely.
std::unique_ptr<MasterService> service_(new MasterService());
[[maybe_unused]] const auto context = PrepareSimpleSegment(*service_);
const UUID client_id = generate_uuid();
std::string key = "upsert_diff_revoke";
uint64_t original_size = 1024;
uint64_t new_size = 2048;
ReplicateConfig config;
config.replica_num = 1;
// Create object with original size
auto put_result = service_->PutStart(client_id, key, original_size, config);
ASSERT_TRUE(put_result.has_value());
auto put_end = service_->PutEnd(client_id, key, ReplicaType::MEMORY);
ASSERT_TRUE(put_end.has_value());
// Verify the key exists
auto exist_before = service_->ExistKey(key);
ASSERT_TRUE(exist_before.has_value());
EXPECT_TRUE(exist_before.value());
// UpsertStart with different size (Case C) — old replicas discarded,
// new replicas allocated
auto upsert_result =
service_->UpsertStart(client_id, key, new_size, config);
ASSERT_TRUE(upsert_result.has_value());
// Revoke — erase the newly allocated PROCESSING replicas
auto revoke_result =
service_->UpsertRevoke(client_id, key, ReplicaType::MEMORY);
ASSERT_TRUE(revoke_result.has_value());
// Key should be gone (old replicas in discarded, new replicas erased)
auto exist_after = service_->ExistKey(key);
ASSERT_TRUE(exist_after.has_value());
EXPECT_FALSE(exist_after.value());
}
// ===================== Hard Pin Tests =====================
TEST_F(MasterServiceTest, HardPinObjectNotEvicted) {
// Hard-pinned objects must survive eviction under memory pressure,
// even after lease expires and all non-pinned objects are gone.
const uint64_t kv_lease_ttl = 200;
auto service_config = MasterServiceConfig::builder()
.set_default_kv_lease_ttl(kv_lease_ttl)
.build();
std::unique_ptr<MasterService> service_(new MasterService(service_config));
const UUID client_id = generate_uuid();
constexpr size_t buffer = 0x300000000;
constexpr size_t segment_size = 1024 * 1024 * 16;
constexpr size_t value_size = 1024 * 1024;
[[maybe_unused]] const auto context =
PrepareSimpleSegment(*service_, "test_segment", buffer, segment_size);
// Put a hard-pinned object
{
ReplicateConfig config;
config.replica_num = 1;
config.with_hard_pin = true;
auto result =
service_->PutStart(client_id, "pinned_model", value_size, config);
ASSERT_TRUE(result.has_value());
ASSERT_TRUE(
service_->PutEnd(client_id, "pinned_model", ReplicaType::MEMORY)
.has_value());
}
// Fill remaining space with normal objects to trigger eviction
for (int i = 0; i < 20; i++) {
std::string key = "filler_" + std::to_string(i);
ReplicateConfig config;
config.replica_num = 1;
auto result = service_->PutStart(client_id, key, value_size, config);
if (result.has_value()) {
service_->PutEnd(client_id, key, ReplicaType::MEMORY);
}
}
// Wait for leases to expire and eviction to kick in
std::this_thread::sleep_for(std::chrono::milliseconds(kv_lease_ttl + 500));
// Hard-pinned object must still be there
auto get_result = service_->GetReplicaList("pinned_model");
ASSERT_TRUE(get_result.has_value())
<< "Hard-pinned object was evicted, but it should never be";
// Explicit Remove should still work on hard-pinned objects
auto remove_result = service_->Remove("pinned_model", /*force=*/true);
ASSERT_TRUE(remove_result.has_value());
auto exist_result = service_->ExistKey("pinned_model");
ASSERT_TRUE(exist_result.has_value());
ASSERT_FALSE(exist_result.value());
service_->RemoveAll();
}
TEST_F(MasterServiceTest, HardPinWithSoftPinEvictionOrder) {
// Verify eviction priority: non-pinned first, then soft-pinned,
// and hard-pinned objects are never evicted even under extreme pressure.
const uint64_t kv_lease_ttl = 200;
const uint64_t kv_soft_pin_ttl = 10000;
const bool allow_evict_soft_pinned_objects = true;
auto service_config = MasterServiceConfig::builder()
.set_default_kv_lease_ttl(kv_lease_ttl)
.set_default_kv_soft_pin_ttl(kv_soft_pin_ttl)
.set_allow_evict_soft_pinned_objects(
allow_evict_soft_pinned_objects)
.set_eviction_ratio(0.5)
.build();
std::unique_ptr<MasterService> service_(new MasterService(service_config));
const UUID client_id = generate_uuid();
constexpr size_t buffer = 0x300000000;
constexpr size_t segment_size = 1024 * 1024 * 16;
constexpr size_t value_size = 1024 * 1024;
[[maybe_unused]] const auto context =
PrepareSimpleSegment(*service_, "test_segment", buffer, segment_size);
// Put a hard-pinned object
{
ReplicateConfig config;
config.replica_num = 1;
config.with_hard_pin = true;
ASSERT_TRUE(
service_->PutStart(client_id, "hard_pinned", value_size, config)
.has_value());
ASSERT_TRUE(
service_->PutEnd(client_id, "hard_pinned", ReplicaType::MEMORY)
.has_value());
}
// Put a soft-pinned object
{
ReplicateConfig config;
config.replica_num = 1;
config.with_soft_pin = true;
ASSERT_TRUE(
service_->PutStart(client_id, "soft_pinned", value_size, config)
.has_value());
ASSERT_TRUE(
service_->PutEnd(client_id, "soft_pinned", ReplicaType::MEMORY)
.has_value());
}
// Fill the rest
for (int i = 0; i < 20; i++) {
std::string key = "normal_" + std::to_string(i);
ReplicateConfig config;
config.replica_num = 1;
auto result = service_->PutStart(client_id, key, value_size, config);
if (result.has_value()) {
service_->PutEnd(client_id, key, ReplicaType::MEMORY);
}
}
// Let leases expire, trigger eviction
std::this_thread::sleep_for(std::chrono::milliseconds(kv_lease_ttl + 500));
// Hard-pinned always survives
ASSERT_TRUE(service_->GetReplicaList("hard_pinned").has_value())
<< "Hard-pinned object was evicted";
std::this_thread::sleep_for(std::chrono::milliseconds(kv_lease_ttl));
service_->RemoveAll();
}
TEST_F(MasterServiceTest, HardPinDefaultIsFalse) {
// Objects created without with_hard_pin should not be hard-pinned
auto service_config =
MasterServiceConfig::builder().set_default_kv_lease_ttl(5000).build();
std::unique_ptr<MasterService> service_(new MasterService(service_config));
const UUID client_id = generate_uuid();
constexpr size_t buffer = 0x300000000;
constexpr size_t segment_size = 1024 * 1024 * 16;
[[maybe_unused]] const auto context =
PrepareSimpleSegment(*service_, "test_segment", buffer, segment_size);
// Put without hard_pin (default)
ReplicateConfig config;
config.replica_num = 1;
ASSERT_TRUE(
service_->PutStart(client_id, "normal_key", 1024, config).has_value());
ASSERT_TRUE(service_->PutEnd(client_id, "normal_key", ReplicaType::MEMORY)
.has_value());
// Put with hard_pin
ReplicateConfig hp_config;
hp_config.replica_num = 1;
hp_config.with_hard_pin = true;
ASSERT_TRUE(
service_->PutStart(client_id, "hp_key", 1024, hp_config).has_value());
ASSERT_TRUE(
service_->PutEnd(client_id, "hp_key", ReplicaType::MEMORY).has_value());
// Both should exist
ASSERT_TRUE(service_->GetReplicaList("normal_key").has_value());
ASSERT_TRUE(service_->GetReplicaList("hp_key").has_value());
service_->RemoveAll();
}
// ===================== Graceful Unmount Tests =====================
TEST_F(MasterServiceTest, GracefulUnmountSegment_SetsCorrectStatus) {
std::unique_ptr<MasterService> service_(new MasterService());
auto segment = MakeSegment("graceful_test_segment");
UUID client_id = generate_uuid();
// Mount segment
ASSERT_TRUE(service_->MountSegment(segment, client_id).has_value());
// Verify initial status
auto status_before = service_->QuerySegmentStatus(segment.name);
ASSERT_TRUE(status_before.has_value());
EXPECT_EQ(status_before.value(), SegmentStatus::OK);
// Graceful unmount with 1 second grace period
auto graceful_result = service_->GracefulUnmountSegment(
segment.id, client_id, /*grace_period_ms=*/1000);
ASSERT_TRUE(graceful_result.has_value())
<< "Graceful unmount should succeed: "
<< toString(graceful_result.error());
// Verify status is GRACEFULLY_UNMOUNTING
auto status_after = service_->QuerySegmentStatus(segment.name);
ASSERT_TRUE(status_after.has_value());
EXPECT_EQ(status_after.value(), SegmentStatus::GRACEFULLY_UNMOUNTING);
// Wait for timer to expire and clean up
std::this_thread::sleep_for(std::chrono::milliseconds(1500));
}
TEST_F(MasterServiceTest, GracefulUnmountSegment_RejectWrongClient) {
std::unique_ptr<MasterService> service_(new MasterService());
auto segment = MakeSegment("graceful_owner_segment");
UUID owner_client = generate_uuid();
UUID wrong_client = generate_uuid();
ASSERT_TRUE(service_->MountSegment(segment, owner_client).has_value());
// Wrong client trying to graceful unmount should fail
auto graceful_result = service_->GracefulUnmountSegment(
segment.id, wrong_client, /*grace_period_ms=*/1000);
ASSERT_FALSE(graceful_result.has_value());
EXPECT_EQ(graceful_result.error(), ErrorCode::SEGMENT_NOT_FOUND);
// Owner should still be able to unmount
auto owner_result = service_->GracefulUnmountSegment(
segment.id, owner_client, /*grace_period_ms=*/1000);
EXPECT_TRUE(owner_result.has_value());
std::this_thread::sleep_for(std::chrono::milliseconds(1500));
}
TEST_F(MasterServiceTest, GracefulUnmountSegment_Idempotent) {
std::unique_ptr<MasterService> service_(new MasterService());
auto segment = MakeSegment("graceful_idempotent_segment");
UUID client_id = generate_uuid();
ASSERT_TRUE(service_->MountSegment(segment, client_id).has_value());
// First graceful unmount should succeed
auto result1 = service_->GracefulUnmountSegment(segment.id, client_id,
/*grace_period_ms=*/1000);
ASSERT_TRUE(result1.has_value());
// Second graceful unmount on the same segment should also succeed
// (idempotent)
auto result2 = service_->GracefulUnmountSegment(segment.id, client_id,
/*grace_period_ms=*/1000);
EXPECT_TRUE(result2.has_value()) << "Graceful unmount should be idempotent";
std::this_thread::sleep_for(std::chrono::milliseconds(1500));
}
TEST_F(MasterServiceTest, GracefulUnmountSegment_TimerExpiresAndUnmounts) {
std::unique_ptr<MasterService> service_(new MasterService());
auto segment = MakeSegment("graceful_timer_segment");
UUID client_id = generate_uuid();
ASSERT_TRUE(service_->MountSegment(segment, client_id).has_value());
// Graceful unmount with a short grace period (50ms)
auto graceful_result = service_->GracefulUnmountSegment(
segment.id, client_id, /*grace_period_ms=*/50);
ASSERT_TRUE(graceful_result.has_value());
// Immediately after graceful unmount, segment should still exist
auto status_immediate = service_->QuerySegmentStatus(segment.name);
ASSERT_TRUE(status_immediate.has_value());
EXPECT_EQ(status_immediate.value(), SegmentStatus::GRACEFULLY_UNMOUNTING);
// Wait for timer to expire and unmount (give some margin)
std::this_thread::sleep_for(std::chrono::milliseconds(300));
// After timer expires, segment should be fully unmounted (UNDEFINED or
// error)
auto status_after = service_->QuerySegmentStatus(segment.name);
// Segment may be UNDEFINED (not found) or return an error
EXPECT_TRUE(!status_after.has_value() ||
status_after.value() == SegmentStatus::UNDEFINED)
<< "Segment should be unmounted after timer expires, got status="
<< (status_after.has_value() ? static_cast<int>(status_after.value())
: -1);
}
TEST_F(MasterServiceTest,
GracefulUnmountSegment_QueryStatusByIdWithReusedName) {
std::unique_ptr<MasterService> service_(new MasterService());
auto old_segment = MakeSegment("graceful_reused_name_segment");
auto new_segment = MakeSegment(old_segment.name, /*base=*/0x400000000);
UUID client_id = generate_uuid();
ASSERT_TRUE(service_->MountSegment(old_segment, client_id).has_value());
ASSERT_TRUE(service_
->GracefulUnmountSegment(old_segment.id, client_id,
/*grace_period_ms=*/50)
.has_value());
ASSERT_TRUE(service_->MountSegment(new_segment, client_id).has_value());
auto old_status = service_->QuerySegmentStatusById(old_segment.id);
ASSERT_TRUE(old_status.has_value());
EXPECT_EQ(old_status.value(), SegmentStatus::GRACEFULLY_UNMOUNTING);
auto new_status = service_->QuerySegmentStatusById(new_segment.id);
ASSERT_TRUE(new_status.has_value());
EXPECT_EQ(new_status.value(), SegmentStatus::OK);
std::this_thread::sleep_for(std::chrono::milliseconds(300));
EXPECT_FALSE(service_->QuerySegmentStatusById(old_segment.id).has_value());
ASSERT_TRUE(service_->QuerySegmentStatusById(new_segment.id).has_value());
auto status_by_name = service_->QuerySegmentStatus(new_segment.name);
ASSERT_TRUE(status_by_name.has_value());
EXPECT_EQ(status_by_name.value(), SegmentStatus::OK);
}
TEST_F(MasterServiceTest, GracefulUnmountSegment_EarlierTimerPreemptsWait) {
std::unique_ptr<MasterService> service_(new MasterService());
auto long_segment = MakeSegment("graceful_long_timer_segment");
auto short_segment =
MakeSegment("graceful_short_timer_segment", /*base=*/0x400000000);
UUID client_id = generate_uuid();
ASSERT_TRUE(service_->MountSegment(long_segment, client_id).has_value());
ASSERT_TRUE(service_->MountSegment(short_segment, client_id).has_value());
ASSERT_TRUE(service_
->GracefulUnmountSegment(long_segment.id, client_id,
/*grace_period_ms=*/1000)
.has_value());
std::this_thread::sleep_for(std::chrono::milliseconds(20));
ASSERT_TRUE(service_
->GracefulUnmountSegment(short_segment.id, client_id,
/*grace_period_ms=*/50)
.has_value());
std::this_thread::sleep_for(std::chrono::milliseconds(300));
auto short_status = service_->QuerySegmentStatus(short_segment.name);
EXPECT_TRUE(!short_status.has_value() ||
short_status.value() == SegmentStatus::UNDEFINED);
auto long_status = service_->QuerySegmentStatus(long_segment.name);
ASSERT_TRUE(long_status.has_value());
EXPECT_EQ(long_status.value(), SegmentStatus::GRACEFULLY_UNMOUNTING);
}
TEST_F(MasterServiceTest, GracefulUnmountSegment_PreventAllocation) {
std::unique_ptr<MasterService> service_(new MasterService());
auto segment1 = MakeSegment("graceful_seg1");
auto segment2 = MakeSegment("graceful_seg2", /*base=*/0x400000000);
UUID client_id = generate_uuid();
ASSERT_TRUE(service_->MountSegment(segment1, client_id).has_value());
ASSERT_TRUE(service_->MountSegment(segment2, client_id).has_value());
// Put an object on segment1
std::string key = "test_key_prevent_alloc";
ReplicateConfig config;
config.replica_num = 1;
config.preferred_segment = segment1.name;
auto put_start = service_->PutStart(client_id, key, 1024, config);
ASSERT_TRUE(put_start.has_value());
ASSERT_TRUE(
service_->PutEnd(client_id, key, ReplicaType::MEMORY).has_value());
// Graceful unmount segment1
ASSERT_TRUE(service_->GracefulUnmountSegment(segment1.id, client_id, 1000)
.has_value());
// Segment1 status should be GRACEFULLY_UNMOUNTING
auto status1 = service_->QuerySegmentStatus(segment1.name);
ASSERT_TRUE(status1.has_value());
EXPECT_EQ(status1.value(), SegmentStatus::GRACEFULLY_UNMOUNTING);
// Existing replicas on the graceful segment should remain readable during
// the grace window.
auto existing_replicas = service_->GetReplicaList(key);
ASSERT_TRUE(existing_replicas.has_value());
ASSERT_EQ(existing_replicas->replicas.size(), 1u);
EXPECT_EQ(existing_replicas->replicas[0]
.get_memory_descriptor()
.buffer_descriptor.transport_endpoint_,
segment1.name);
// Segment2 status should still be OK
auto status2 = service_->QuerySegmentStatus(segment2.name);
ASSERT_TRUE(status2.has_value());
EXPECT_EQ(status2.value(), SegmentStatus::OK);
// New put without preferred_segment should succeed on segment2
std::string key2 = "test_key_after_graceful";
ReplicateConfig config2;
config2.replica_num = 1;
auto put_start2 = service_->PutStart(client_id, key2, 1024, config2);
ASSERT_TRUE(put_start2.has_value());
auto replicas = put_start2.value();
ASSERT_EQ(replicas.size(), 1u);
// Should be placed on segment2, not segment1
EXPECT_EQ(replicas[0]
.get_memory_descriptor()
.buffer_descriptor.transport_endpoint_,
segment2.name);
std::this_thread::sleep_for(std::chrono::milliseconds(1500));
}
} // namespace mooncake::test
int main(int argc, char** argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}