forked from mooncake-track/Mooncake
1055 lines
41 KiB
C++
1055 lines
41 KiB
C++
#include "master_service.h"
|
|
|
|
#include <cassert>
|
|
#include <cstdint>
|
|
#include <queue>
|
|
#include <shared_mutex>
|
|
#include <ylt/util/tl/expected.hpp>
|
|
|
|
#include "master_metric_manager.h"
|
|
#include "segment.h"
|
|
#include "types.h"
|
|
|
|
namespace mooncake {
|
|
|
|
MasterService::MasterService(
|
|
bool enable_gc, uint64_t default_kv_lease_ttl,
|
|
uint64_t default_kv_soft_pin_ttl, bool allow_evict_soft_pinned_objects,
|
|
double eviction_ratio, double eviction_high_watermark_ratio,
|
|
ViewVersionId view_version, int64_t client_live_ttl_sec, bool enable_ha,
|
|
const std::string& cluster_id, const std::string& root_fs_dir,
|
|
BufferAllocatorType memory_allocator)
|
|
: enable_gc_(enable_gc),
|
|
default_kv_lease_ttl_(default_kv_lease_ttl),
|
|
default_kv_soft_pin_ttl_(default_kv_soft_pin_ttl),
|
|
allow_evict_soft_pinned_objects_(allow_evict_soft_pinned_objects),
|
|
eviction_ratio_(eviction_ratio),
|
|
eviction_high_watermark_ratio_(eviction_high_watermark_ratio),
|
|
client_live_ttl_sec_(client_live_ttl_sec),
|
|
enable_ha_(enable_ha),
|
|
cluster_id_(cluster_id),
|
|
root_fs_dir_(root_fs_dir),
|
|
segment_manager_(memory_allocator),
|
|
allocation_strategy_(std::make_shared<RandomAllocationStrategy>()) {
|
|
if (eviction_ratio_ < 0.0 || eviction_ratio_ > 1.0) {
|
|
LOG(ERROR) << "Eviction ratio must be between 0.0 and 1.0, "
|
|
<< "current value: " << eviction_ratio_;
|
|
throw std::invalid_argument("Invalid eviction ratio");
|
|
}
|
|
if (eviction_high_watermark_ratio_ < 0.0 ||
|
|
eviction_high_watermark_ratio_ > 1.0) {
|
|
LOG(ERROR)
|
|
<< "Eviction high watermark ratio must be between 0.0 and 1.0, "
|
|
<< "current value: " << eviction_high_watermark_ratio_;
|
|
throw std::invalid_argument("Invalid eviction high watermark ratio");
|
|
}
|
|
gc_running_ = true;
|
|
gc_thread_ = std::thread(&MasterService::GCThreadFunc, this);
|
|
VLOG(1) << "action=start_gc_thread";
|
|
|
|
if (enable_ha) {
|
|
client_monitor_running_ = true;
|
|
client_monitor_thread_ =
|
|
std::thread(&MasterService::ClientMonitorFunc, this);
|
|
VLOG(1) << "action=start_client_monitor_thread";
|
|
}
|
|
|
|
if (!root_fs_dir_.empty()) {
|
|
use_disk_replica_ = true;
|
|
}
|
|
}
|
|
|
|
MasterService::~MasterService() {
|
|
// Stop and join the threads
|
|
gc_running_ = false;
|
|
client_monitor_running_ = false;
|
|
if (gc_thread_.joinable()) {
|
|
gc_thread_.join();
|
|
}
|
|
if (client_monitor_thread_.joinable()) {
|
|
client_monitor_thread_.join();
|
|
}
|
|
|
|
// Clean up any remaining GC tasks
|
|
GCTask* task = nullptr;
|
|
while (gc_queue_.pop(task)) {
|
|
if (task) {
|
|
delete task;
|
|
}
|
|
}
|
|
}
|
|
|
|
auto MasterService::MountSegment(const Segment& segment, const UUID& client_id)
|
|
-> tl::expected<void, ErrorCode> {
|
|
ScopedSegmentAccess segment_access = segment_manager_.getSegmentAccess();
|
|
|
|
if (enable_ha_) {
|
|
// Tell the client monitor thread to start timing for this client. To
|
|
// avoid the following undesired situations, this message must be sent
|
|
// after locking the segment mutex and before the mounting operation
|
|
// completes:
|
|
// 1. Sending the message before the lock: the client expires and
|
|
// unmouting invokes before this mounting are completed, which prevents
|
|
// this segment being able to be unmounted forever;
|
|
// 2. Sending the message after mounting the segment: After mounting
|
|
// this segment, when trying to push id to the queue, the queue is
|
|
// already full. However, at this point, the message must be sent,
|
|
// otherwise this client cannot be monitored and expired.
|
|
PodUUID pod_client_id;
|
|
pod_client_id.first = client_id.first;
|
|
pod_client_id.second = client_id.second;
|
|
if (!client_ping_queue_.push(pod_client_id)) {
|
|
LOG(ERROR) << "segment_name=" << segment.name
|
|
<< ", error=client_ping_queue_full";
|
|
return tl::make_unexpected(ErrorCode::INTERNAL_ERROR);
|
|
}
|
|
}
|
|
|
|
auto err = segment_access.MountSegment(segment, client_id);
|
|
if (err == ErrorCode::SEGMENT_ALREADY_EXISTS) {
|
|
// Return OK because this is an idempotent operation
|
|
return {};
|
|
} else if (err != ErrorCode::OK) {
|
|
return tl::make_unexpected(err);
|
|
}
|
|
return {};
|
|
}
|
|
|
|
auto MasterService::ReMountSegment(const std::vector<Segment>& segments,
|
|
const UUID& client_id)
|
|
-> tl::expected<void, ErrorCode> {
|
|
if (!enable_ha_) {
|
|
LOG(ERROR) << "ReMountSegment is only available in HA mode";
|
|
return tl::make_unexpected(ErrorCode::UNAVAILABLE_IN_CURRENT_MODE);
|
|
}
|
|
|
|
std::unique_lock<std::shared_mutex> lock(client_mutex_);
|
|
if (ok_client_.contains(client_id)) {
|
|
LOG(WARNING) << "client_id=" << client_id
|
|
<< ", warn=client_already_remounted";
|
|
// Return OK because this is an idempotent operation
|
|
return {};
|
|
}
|
|
|
|
ScopedSegmentAccess segment_access = segment_manager_.getSegmentAccess();
|
|
|
|
// Tell the client monitor thread to start timing for this client. To
|
|
// avoid the following undesired situations, this message must be sent
|
|
// after locking the segment mutex or client mutex and before the remounting
|
|
// operation completes:
|
|
// 1. Sending the message before the lock: the client expires and
|
|
// unmouting invokes before this remounting are completed, which prevents
|
|
// this segment being able to be unmounted forever;
|
|
// 2. Sending the message after remounting the segments: After remounting
|
|
// these segments, when trying to push id to the queue, the queue is
|
|
// already full. However, at this point, the message must be sent,
|
|
// otherwise this client cannot be monitored and expired.
|
|
PodUUID pod_client_id;
|
|
pod_client_id.first = client_id.first;
|
|
pod_client_id.second = client_id.second;
|
|
if (!client_ping_queue_.push(pod_client_id)) {
|
|
LOG(ERROR) << "client_id=" << client_id
|
|
<< ", error=client_ping_queue_full";
|
|
return tl::make_unexpected(ErrorCode::INTERNAL_ERROR);
|
|
}
|
|
|
|
ErrorCode err = segment_access.ReMountSegment(segments, client_id);
|
|
if (err != ErrorCode::OK) {
|
|
return tl::make_unexpected(err);
|
|
}
|
|
|
|
// Change the client status to OK
|
|
ok_client_.insert(client_id);
|
|
MasterMetricManager::instance().inc_active_clients();
|
|
|
|
return {};
|
|
}
|
|
|
|
void MasterService::ClearInvalidHandles() {
|
|
for (auto& shard : metadata_shards_) {
|
|
MutexLocker lock(&shard.mutex);
|
|
auto it = shard.metadata.begin();
|
|
while (it != shard.metadata.end()) {
|
|
if (CleanupStaleHandles(it->second)) {
|
|
// If the object is empty, we need to erase the iterator
|
|
it = shard.metadata.erase(it);
|
|
} else {
|
|
++it;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
auto MasterService::UnmountSegment(const UUID& segment_id,
|
|
const UUID& client_id)
|
|
-> tl::expected<void, ErrorCode> {
|
|
size_t metrics_dec_capacity = 0; // to update the metrics
|
|
|
|
// 1. Prepare to unmount the segment by deleting its allocator
|
|
{
|
|
ScopedSegmentAccess segment_access =
|
|
segment_manager_.getSegmentAccess();
|
|
ErrorCode err = segment_access.PrepareUnmountSegment(
|
|
segment_id, metrics_dec_capacity);
|
|
if (err == ErrorCode::SEGMENT_NOT_FOUND) {
|
|
// Return OK because this is an idempotent operation
|
|
return {};
|
|
}
|
|
if (err != ErrorCode::OK) {
|
|
return tl::make_unexpected(err);
|
|
}
|
|
} // Release the segment mutex before long-running step 2 and avoid
|
|
// deadlocks
|
|
|
|
// 2. Remove the metadata of the related objects
|
|
ClearInvalidHandles();
|
|
|
|
// 3. Commit the unmount operation
|
|
ScopedSegmentAccess segment_access = segment_manager_.getSegmentAccess();
|
|
auto err = segment_access.CommitUnmountSegment(segment_id, client_id,
|
|
metrics_dec_capacity);
|
|
if (err != ErrorCode::OK) {
|
|
return tl::make_unexpected(err);
|
|
}
|
|
return {};
|
|
}
|
|
|
|
auto MasterService::ExistKey(const std::string& key)
|
|
-> tl::expected<bool, ErrorCode> {
|
|
MetadataAccessor accessor(this, key);
|
|
if (!accessor.Exists()) {
|
|
VLOG(1) << "key=" << key << ", info=object_not_found";
|
|
return false;
|
|
}
|
|
|
|
auto& metadata = accessor.Get();
|
|
for (const auto& replica : metadata.replicas) {
|
|
if (replica.status() == ReplicaStatus::COMPLETE) {
|
|
// Grant a lease to the object as it may be further used by the
|
|
// client.
|
|
metadata.GrantLease(default_kv_lease_ttl_,
|
|
default_kv_soft_pin_ttl_);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false; // If no complete replica is found, return false
|
|
}
|
|
|
|
std::vector<tl::expected<bool, ErrorCode>> MasterService::BatchExistKey(
|
|
const std::vector<std::string>& keys) {
|
|
std::vector<tl::expected<bool, ErrorCode>> results;
|
|
results.reserve(keys.size());
|
|
for (const auto& key : keys) {
|
|
results.emplace_back(ExistKey(key));
|
|
}
|
|
return results;
|
|
}
|
|
|
|
auto MasterService::GetAllKeys()
|
|
-> tl::expected<std::vector<std::string>, ErrorCode> {
|
|
std::vector<std::string> all_keys;
|
|
for (size_t i = 0; i < kNumShards; i++) {
|
|
MutexLocker lock(&metadata_shards_[i].mutex);
|
|
for (const auto& item : metadata_shards_[i].metadata) {
|
|
all_keys.push_back(item.first);
|
|
}
|
|
}
|
|
return all_keys;
|
|
}
|
|
|
|
auto MasterService::GetAllSegments()
|
|
-> tl::expected<std::vector<std::string>, ErrorCode> {
|
|
ScopedSegmentAccess segment_access = segment_manager_.getSegmentAccess();
|
|
std::vector<std::string> all_segments;
|
|
auto err = segment_access.GetAllSegments(all_segments);
|
|
if (err != ErrorCode::OK) {
|
|
return tl::make_unexpected(err);
|
|
}
|
|
return all_segments;
|
|
}
|
|
|
|
auto MasterService::QuerySegments(const std::string& segment)
|
|
-> tl::expected<std::pair<size_t, size_t>, ErrorCode> {
|
|
ScopedSegmentAccess segment_access = segment_manager_.getSegmentAccess();
|
|
size_t used, capacity;
|
|
auto err = segment_access.QuerySegments(segment, used, capacity);
|
|
if (err != ErrorCode::OK) {
|
|
return tl::make_unexpected(err);
|
|
}
|
|
return std::make_pair(used, capacity);
|
|
}
|
|
|
|
auto MasterService::GetReplicaList(std::string_view key)
|
|
-> tl::expected<std::vector<Replica::Descriptor>, ErrorCode> {
|
|
MetadataAccessor accessor(this, std::string(key));
|
|
if (!accessor.Exists()) {
|
|
VLOG(1) << "key=" << key << ", info=object_not_found";
|
|
return tl::make_unexpected(ErrorCode::OBJECT_NOT_FOUND);
|
|
}
|
|
auto& metadata = accessor.Get();
|
|
|
|
std::vector<Replica::Descriptor> replica_list;
|
|
replica_list.reserve(metadata.replicas.size());
|
|
for (const auto& replica : metadata.replicas) {
|
|
if (replica.status() == ReplicaStatus::COMPLETE) {
|
|
replica_list.emplace_back(replica.get_descriptor());
|
|
}
|
|
}
|
|
|
|
if (replica_list.empty()) {
|
|
LOG(WARNING) << "key=" << key << ", error=replica_not_ready";
|
|
return tl::make_unexpected(ErrorCode::REPLICA_IS_NOT_READY);
|
|
}
|
|
|
|
// Only mark for GC if enabled
|
|
if (enable_gc_) {
|
|
MarkForGC(std::string(key),
|
|
1000); // After 1 second, the object will be removed
|
|
} else {
|
|
// Grant a lease to the object so it will not be removed
|
|
// when the client is reading it.
|
|
metadata.GrantLease(default_kv_lease_ttl_, default_kv_soft_pin_ttl_);
|
|
}
|
|
|
|
return replica_list;
|
|
}
|
|
|
|
std::vector<tl::expected<std::vector<Replica::Descriptor>, ErrorCode>>
|
|
MasterService::BatchGetReplicaList(const std::vector<std::string>& keys) {
|
|
std::vector<tl::expected<std::vector<Replica::Descriptor>, ErrorCode>>
|
|
results;
|
|
results.reserve(keys.size());
|
|
for (const auto& key : keys) {
|
|
results.emplace_back(GetReplicaList(key));
|
|
}
|
|
return results;
|
|
}
|
|
|
|
auto MasterService::PutStart(const std::string& key,
|
|
const std::vector<uint64_t>& slice_lengths,
|
|
const ReplicateConfig& config)
|
|
-> tl::expected<std::vector<Replica::Descriptor>, ErrorCode> {
|
|
if (config.replica_num == 0 || key.empty() || slice_lengths.empty()) {
|
|
LOG(ERROR) << "key=" << key << ", replica_num=" << config.replica_num
|
|
<< ", slice_count=" << slice_lengths.size()
|
|
<< ", key_size=" << key.size() << ", error=invalid_params";
|
|
return tl::make_unexpected(ErrorCode::INVALID_PARAMS);
|
|
}
|
|
|
|
// Validate slice lengths
|
|
uint64_t total_length = 0;
|
|
for (size_t i = 0; i < slice_lengths.size(); ++i) {
|
|
if (slice_lengths[i] > kMaxSliceSize) {
|
|
LOG(ERROR) << "key=" << key << ", slice_index=" << i
|
|
<< ", slice_size=" << slice_lengths[i]
|
|
<< ", max_size=" << kMaxSliceSize
|
|
<< ", error=invalid_slice_size";
|
|
return tl::make_unexpected(ErrorCode::INVALID_PARAMS);
|
|
}
|
|
total_length += slice_lengths[i];
|
|
}
|
|
|
|
VLOG(1) << "key=" << key << ", value_length=" << total_length
|
|
<< ", slice_count=" << slice_lengths.size() << ", config=" << config
|
|
<< ", action=put_start_begin";
|
|
|
|
// Lock the shard and check if object already exists
|
|
size_t shard_idx = getShardIndex(key);
|
|
MutexLocker lock(&metadata_shards_[shard_idx].mutex);
|
|
|
|
auto it = metadata_shards_[shard_idx].metadata.find(key);
|
|
if (it != metadata_shards_[shard_idx].metadata.end() &&
|
|
!CleanupStaleHandles(it->second)) {
|
|
LOG(INFO) << "key=" << key << ", info=object_already_exists";
|
|
return tl::make_unexpected(ErrorCode::OBJECT_ALREADY_EXISTS);
|
|
}
|
|
|
|
// Allocate replicas
|
|
std::vector<Replica> replicas;
|
|
replicas.reserve(config.replica_num + use_disk_replica_);
|
|
{
|
|
ScopedAllocatorAccess allocator_access =
|
|
segment_manager_.getAllocatorAccess();
|
|
auto& allocators = allocator_access.getAllocators();
|
|
auto& allocators_by_name = allocator_access.getAllocatorsByName();
|
|
for (size_t i = 0; i < config.replica_num; ++i) {
|
|
std::vector<std::unique_ptr<AllocatedBuffer>> handles;
|
|
handles.reserve(slice_lengths.size());
|
|
|
|
// Allocate space for each slice
|
|
for (size_t j = 0; j < slice_lengths.size(); ++j) {
|
|
auto chunk_size = slice_lengths[j];
|
|
|
|
// Use the unified allocation strategy with replica config
|
|
auto handle = allocation_strategy_->Allocate(
|
|
allocators, allocators_by_name, chunk_size, config);
|
|
|
|
if (!handle) {
|
|
// If the allocation failed, we need to evict some objects
|
|
// to free up space for future allocations.
|
|
need_eviction_ = true;
|
|
return tl::make_unexpected(ErrorCode::NO_AVAILABLE_HANDLE);
|
|
}
|
|
|
|
VLOG(1) << "key=" << key << ", replica_id=" << i
|
|
<< ", slice_index=" << j << ", handle=" << *handle
|
|
<< ", action=slice_allocated";
|
|
handles.emplace_back(std::move(handle));
|
|
}
|
|
|
|
replicas.emplace_back(std::move(handles),
|
|
ReplicaStatus::PROCESSING);
|
|
}
|
|
}
|
|
|
|
// If disk replica is enabled, allocate a disk replica
|
|
if (use_disk_replica_) {
|
|
// Allocate a file path for the disk replica
|
|
std::string file_path = ResolvePath(key);
|
|
replicas.emplace_back(file_path, total_length,
|
|
ReplicaStatus::PROCESSING);
|
|
}
|
|
|
|
std::vector<Replica::Descriptor> replica_list;
|
|
replica_list.reserve(replicas.size());
|
|
for (const auto& replica : replicas) {
|
|
replica_list.emplace_back(replica.get_descriptor());
|
|
}
|
|
|
|
// No need to set lease here. The object will not be evicted until
|
|
// PutEnd is called.
|
|
metadata_shards_[shard_idx].metadata.emplace(
|
|
std::piecewise_construct, std::forward_as_tuple(key),
|
|
std::forward_as_tuple(total_length, std::move(replicas),
|
|
config.with_soft_pin));
|
|
return replica_list;
|
|
}
|
|
|
|
auto MasterService::PutEnd(const std::string& key, ReplicaType replica_type)
|
|
-> tl::expected<void, ErrorCode> {
|
|
MetadataAccessor accessor(this, key);
|
|
if (!accessor.Exists()) {
|
|
LOG(ERROR) << "key=" << key << ", error=object_not_found";
|
|
return tl::make_unexpected(ErrorCode::OBJECT_NOT_FOUND);
|
|
}
|
|
|
|
auto& metadata = accessor.Get();
|
|
for (auto& replica : metadata.replicas) {
|
|
if (replica.type() == replica_type) {
|
|
replica.mark_complete();
|
|
}
|
|
}
|
|
// 1. Set lease timeout to now, indicating that the object has no lease
|
|
// at beginning. 2. If this object has soft pin enabled, set it to be soft
|
|
// pinned.
|
|
metadata.GrantLease(0, default_kv_soft_pin_ttl_);
|
|
return {};
|
|
}
|
|
|
|
auto MasterService::PutRevoke(const std::string& key, ReplicaType replica_type)
|
|
-> tl::expected<void, ErrorCode> {
|
|
MetadataAccessor accessor(this, key);
|
|
if (!accessor.Exists()) {
|
|
LOG(INFO) << "key=" << key << ", info=object_not_found";
|
|
return tl::make_unexpected(ErrorCode::OBJECT_NOT_FOUND);
|
|
}
|
|
|
|
auto& metadata = accessor.Get();
|
|
if (auto status = metadata.HasDiffRepStatus(ReplicaStatus::PROCESSING,
|
|
replica_type)) {
|
|
LOG(ERROR) << "key=" << key << ", status=" << *status
|
|
<< ", error=invalid_replica_status";
|
|
return tl::make_unexpected(ErrorCode::INVALID_WRITE);
|
|
}
|
|
metadata.EraseReplica(replica_type);
|
|
if (metadata.IsValid() == false) {
|
|
accessor.Erase();
|
|
}
|
|
return {};
|
|
}
|
|
|
|
std::vector<tl::expected<void, ErrorCode>> MasterService::BatchPutEnd(
|
|
const std::vector<std::string>& keys) {
|
|
std::vector<tl::expected<void, ErrorCode>> results;
|
|
results.reserve(keys.size());
|
|
for (const auto& key : keys) {
|
|
results.emplace_back(PutEnd(key, ReplicaType::MEMORY));
|
|
}
|
|
return results;
|
|
}
|
|
|
|
std::vector<tl::expected<void, ErrorCode>> MasterService::BatchPutRevoke(
|
|
const std::vector<std::string>& keys) {
|
|
std::vector<tl::expected<void, ErrorCode>> results;
|
|
results.reserve(keys.size());
|
|
for (const auto& key : keys) {
|
|
results.emplace_back(PutRevoke(key, ReplicaType::MEMORY));
|
|
}
|
|
return results;
|
|
}
|
|
|
|
auto MasterService::Remove(const std::string& key)
|
|
-> tl::expected<void, ErrorCode> {
|
|
MetadataAccessor accessor(this, key);
|
|
if (!accessor.Exists()) {
|
|
VLOG(1) << "key=" << key << ", error=object_not_found";
|
|
return tl::make_unexpected(ErrorCode::OBJECT_NOT_FOUND);
|
|
}
|
|
|
|
auto& metadata = accessor.Get();
|
|
|
|
if (!metadata.IsLeaseExpired()) {
|
|
VLOG(1) << "key=" << key << ", error=object_has_lease";
|
|
return tl::make_unexpected(ErrorCode::OBJECT_HAS_LEASE);
|
|
}
|
|
|
|
if (!metadata.IsAllReplicasComplete()) {
|
|
LOG(ERROR) << "key=" << key << ", error=replica_not_ready";
|
|
return tl::make_unexpected(ErrorCode::REPLICA_IS_NOT_READY);
|
|
}
|
|
|
|
// Remove object metadata
|
|
accessor.Erase();
|
|
return {};
|
|
}
|
|
|
|
long MasterService::RemoveAll() {
|
|
long removed_count = 0;
|
|
uint64_t total_freed_size = 0;
|
|
// Store the current time to avoid repeatedly
|
|
// calling std::chrono::steady_clock::now()
|
|
auto now = std::chrono::steady_clock::now();
|
|
|
|
for (auto& shard : metadata_shards_) {
|
|
MutexLocker lock(&shard.mutex);
|
|
if (shard.metadata.empty()) {
|
|
continue;
|
|
}
|
|
|
|
// Only remove objects with expired leases
|
|
auto it = shard.metadata.begin();
|
|
while (it != shard.metadata.end()) {
|
|
if (it->second.IsLeaseExpired(now)) {
|
|
total_freed_size +=
|
|
it->second.size * it->second.GetMemReplicaCount();
|
|
it = shard.metadata.erase(it);
|
|
removed_count++;
|
|
} else {
|
|
++it;
|
|
}
|
|
}
|
|
}
|
|
|
|
VLOG(1) << "action=remove_all_objects"
|
|
<< ", removed_count=" << removed_count
|
|
<< ", total_freed_size=" << total_freed_size;
|
|
return removed_count;
|
|
}
|
|
|
|
auto MasterService::MarkForGC(const std::string& key, uint64_t delay_ms)
|
|
-> tl::expected<void, ErrorCode> {
|
|
// Create a new GC task and add it to the queue
|
|
GCTask* task = new GCTask(key, std::chrono::milliseconds(delay_ms));
|
|
if (!gc_queue_.push(task)) {
|
|
// Queue is full, delete the task to avoid memory leak
|
|
delete task;
|
|
LOG(ERROR) << "key=" << key << ", error=gc_queue_full";
|
|
return tl::make_unexpected(ErrorCode::INTERNAL_ERROR);
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
bool MasterService::CleanupStaleHandles(ObjectMetadata& metadata) {
|
|
// Iterate through replicas and remove those with invalid allocators
|
|
auto replica_it = metadata.replicas.begin();
|
|
while (replica_it != metadata.replicas.end()) {
|
|
// Use any_of algorithm to check if any handle has an invalid allocator
|
|
bool has_invalid_mem_handle = replica_it->has_invalid_mem_handle();
|
|
|
|
// Remove replicas with invalid handles using erase-remove idiom
|
|
if (has_invalid_mem_handle) {
|
|
replica_it = metadata.replicas.erase(replica_it);
|
|
} else {
|
|
++replica_it;
|
|
}
|
|
}
|
|
|
|
// Return true if no valid replicas remain after cleanup
|
|
return metadata.replicas.empty();
|
|
}
|
|
|
|
size_t MasterService::GetKeyCount() const {
|
|
size_t total = 0;
|
|
for (const auto& shard : metadata_shards_) {
|
|
MutexLocker lock(&shard.mutex);
|
|
total += shard.metadata.size();
|
|
}
|
|
return total;
|
|
}
|
|
|
|
auto MasterService::Ping(const UUID& client_id)
|
|
-> tl::expected<PingResponse, ErrorCode> {
|
|
if (!enable_ha_) {
|
|
LOG(ERROR) << "Ping is only available in HA mode";
|
|
return tl::make_unexpected(ErrorCode::UNAVAILABLE_IN_CURRENT_MODE);
|
|
}
|
|
|
|
std::shared_lock<std::shared_mutex> lock(client_mutex_);
|
|
ClientStatus client_status;
|
|
auto it = ok_client_.find(client_id);
|
|
if (it != ok_client_.end()) {
|
|
client_status = ClientStatus::OK;
|
|
} else {
|
|
client_status = ClientStatus::NEED_REMOUNT;
|
|
}
|
|
PodUUID pod_client_id = {client_id.first, client_id.second};
|
|
if (!client_ping_queue_.push(pod_client_id)) {
|
|
// Queue is full
|
|
LOG(ERROR) << "client_id=" << client_id
|
|
<< ", error=client_ping_queue_full";
|
|
return tl::make_unexpected(ErrorCode::INTERNAL_ERROR);
|
|
}
|
|
return PingResponse(view_version_, client_status);
|
|
}
|
|
|
|
tl::expected<std::string, ErrorCode> MasterService::GetFsdir() const {
|
|
if (root_fs_dir_.empty() || cluster_id_.empty()) {
|
|
LOG(ERROR) << "root_fs_dir or cluster_id is not set";
|
|
return tl::make_unexpected(ErrorCode::INVALID_PARAMS);
|
|
}
|
|
return root_fs_dir_ + "/" + cluster_id_;
|
|
}
|
|
|
|
void MasterService::GCThreadFunc() {
|
|
VLOG(1) << "action=gc_thread_started";
|
|
|
|
std::priority_queue<GCTask*, std::vector<GCTask*>, GCTaskComparator>
|
|
local_pq;
|
|
|
|
while (gc_running_) {
|
|
GCTask* task = nullptr;
|
|
while (gc_queue_.pop(task)) {
|
|
if (task) {
|
|
local_pq.push(task);
|
|
}
|
|
}
|
|
|
|
while (!local_pq.empty()) {
|
|
task = local_pq.top();
|
|
if (!task->is_ready()) {
|
|
break;
|
|
}
|
|
|
|
local_pq.pop();
|
|
VLOG(1) << "key=" << task->key << ", action=gc_removing_key";
|
|
auto result = Remove(task->key);
|
|
if (!result && result.error() != ErrorCode::OBJECT_NOT_FOUND &&
|
|
result.error() != ErrorCode::OBJECT_HAS_LEASE) {
|
|
LOG(WARNING) << "key=" << task->key
|
|
<< ", error=gc_remove_failed, error_code="
|
|
<< result.error();
|
|
}
|
|
delete task;
|
|
}
|
|
double used_ratio =
|
|
MasterMetricManager::instance().get_global_used_ratio();
|
|
if (used_ratio > eviction_high_watermark_ratio_ ||
|
|
(need_eviction_ && eviction_ratio_ > 0.0)) {
|
|
double evict_ratio_target = std::max(
|
|
eviction_ratio_,
|
|
used_ratio - eviction_high_watermark_ratio_ + eviction_ratio_);
|
|
double evict_ratio_lowerbound =
|
|
std::max(evict_ratio_target * 0.5,
|
|
used_ratio - eviction_high_watermark_ratio_);
|
|
BatchEvict(evict_ratio_target, evict_ratio_lowerbound);
|
|
}
|
|
|
|
std::this_thread::sleep_for(
|
|
std::chrono::milliseconds(kGCThreadSleepMs));
|
|
}
|
|
|
|
while (!local_pq.empty()) {
|
|
delete local_pq.top();
|
|
local_pq.pop();
|
|
}
|
|
|
|
VLOG(1) << "action=gc_thread_stopped";
|
|
}
|
|
|
|
void MasterService::BatchEvict(double evict_ratio_target,
|
|
double evict_ratio_lowerbound) {
|
|
if (evict_ratio_target < evict_ratio_lowerbound) {
|
|
LOG(ERROR) << "evict_ratio_target=" << evict_ratio_target
|
|
<< ", evict_ratio_lowerbound=" << evict_ratio_lowerbound
|
|
<< ", error=invalid_params";
|
|
evict_ratio_lowerbound = evict_ratio_target;
|
|
}
|
|
|
|
auto now = std::chrono::steady_clock::now();
|
|
long evicted_count = 0;
|
|
long object_count = 0;
|
|
uint64_t total_freed_size = 0;
|
|
|
|
// Candidates for second pass eviction
|
|
std::vector<std::chrono::steady_clock::time_point> no_pin_objects;
|
|
std::vector<std::chrono::steady_clock::time_point> soft_pin_objects;
|
|
|
|
// Randomly select a starting shard to avoid imbalance eviction between
|
|
// shards. No need to use expensive random_device here.
|
|
size_t start_idx = rand() % metadata_shards_.size();
|
|
|
|
// First pass: evict objects without soft pin and lease expired
|
|
for (size_t i = 0; i < metadata_shards_.size(); i++) {
|
|
auto& shard =
|
|
metadata_shards_[(start_idx + i) % metadata_shards_.size()];
|
|
MutexLocker lock(&shard.mutex);
|
|
|
|
// object_count must be updated at beginning as it will be used later
|
|
// to compute ideal_evict_num
|
|
object_count += shard.metadata.size();
|
|
|
|
// To achieve evicted_count / object_count = evict_ratio_target,
|
|
// ideally how many object should be evicted in this shard
|
|
const long ideal_evict_num =
|
|
std::ceil(object_count * evict_ratio_target) - evicted_count;
|
|
|
|
std::vector<std::chrono::steady_clock::time_point>
|
|
candidates; // can be removed
|
|
for (auto it = shard.metadata.begin(); it != shard.metadata.end();
|
|
it++) {
|
|
// Skip objects that are not expired or have incomplete replicas
|
|
if (!it->second.IsLeaseExpired(now) ||
|
|
it->second.HasDiffRepStatus(ReplicaStatus::COMPLETE,
|
|
ReplicaType::MEMORY)) {
|
|
continue;
|
|
}
|
|
if (!it->second.IsSoftPinned(now)) {
|
|
if (ideal_evict_num > 0) {
|
|
// first pass candidates
|
|
candidates.push_back(it->second.lease_timeout);
|
|
} else {
|
|
// No need to evict any object in this shard, put to
|
|
// second pass candidates
|
|
no_pin_objects.push_back(it->second.lease_timeout);
|
|
}
|
|
} else if (allow_evict_soft_pinned_objects_) {
|
|
// second pass candidates, only if
|
|
// allow_evict_soft_pinned_objects_ is true
|
|
soft_pin_objects.push_back(it->second.lease_timeout);
|
|
}
|
|
}
|
|
|
|
if (ideal_evict_num > 0 && !candidates.empty()) {
|
|
long evict_num = std::min(ideal_evict_num, (long)candidates.size());
|
|
long shard_evicted_count =
|
|
0; // number of objects evicted from this shard
|
|
std::nth_element(candidates.begin(),
|
|
candidates.begin() + (evict_num - 1),
|
|
candidates.end());
|
|
auto target_timeout = candidates[evict_num - 1];
|
|
// Evict objects with lease timeout less than or equal to target.
|
|
auto it = shard.metadata.begin();
|
|
while (it != shard.metadata.end()) {
|
|
// Skip objects that are not allowed to be evicted in the first
|
|
// pass
|
|
if (!it->second.IsLeaseExpired(now) ||
|
|
it->second.IsSoftPinned(now) ||
|
|
it->second.HasDiffRepStatus(ReplicaStatus::COMPLETE,
|
|
ReplicaType::MEMORY) ||
|
|
!it->second.HasMemReplica()) {
|
|
++it;
|
|
continue;
|
|
}
|
|
if (it->second.lease_timeout <= target_timeout) {
|
|
// Evict this object
|
|
total_freed_size +=
|
|
it->second.size * it->second.GetMemReplicaCount();
|
|
it->second.EraseReplica(
|
|
ReplicaType::MEMORY); // Erase memory replicas
|
|
if (it->second.IsValid() == false) {
|
|
it = shard.metadata.erase(it);
|
|
} else {
|
|
++it;
|
|
}
|
|
shard_evicted_count++;
|
|
} else {
|
|
// second pass candidates
|
|
no_pin_objects.push_back(it->second.lease_timeout);
|
|
++it;
|
|
}
|
|
}
|
|
evicted_count += shard_evicted_count;
|
|
}
|
|
}
|
|
|
|
// The ideal number of objects to evict in the second pass
|
|
long target_evict_num =
|
|
std::ceil(object_count * evict_ratio_lowerbound) - evicted_count;
|
|
// The actual number of objects we can evict in the second pass
|
|
target_evict_num =
|
|
std::min(target_evict_num,
|
|
(long)no_pin_objects.size() + (long)soft_pin_objects.size());
|
|
|
|
// Do second pass eviction only if 1). there are candidates that can be
|
|
// evicted AND 2). The evicted number in the first pass is less than
|
|
// evict_ratio_lowerbound.
|
|
if (target_evict_num > 0) {
|
|
// If 1). there are enough candidates without soft pin OR 2). soft pin
|
|
// candidates are empty, then do second pass A. Otherwise, do second
|
|
// pass B. Note that the second condition is ensured implicitly by the
|
|
// calculation of target_evict_num.
|
|
if (target_evict_num <= static_cast<long>(no_pin_objects.size())) {
|
|
// Second pass A: only evict objects without soft pin. The following
|
|
// code is error-prone if target_evict_num > no_pin_objects.size().
|
|
|
|
std::nth_element(no_pin_objects.begin(),
|
|
no_pin_objects.begin() + (target_evict_num - 1),
|
|
no_pin_objects.end());
|
|
auto target_timeout = no_pin_objects[target_evict_num - 1];
|
|
|
|
// Evict objects with lease timeout less than or equal to target.
|
|
// Stop when the target is reached.
|
|
for (size_t i = 0;
|
|
i < metadata_shards_.size() && target_evict_num > 0; i++) {
|
|
auto& shard =
|
|
metadata_shards_[(start_idx + i) % metadata_shards_.size()];
|
|
MutexLocker lock(&shard.mutex);
|
|
auto it = shard.metadata.begin();
|
|
while (it != shard.metadata.end() && target_evict_num > 0) {
|
|
if (it->second.lease_timeout <= target_timeout &&
|
|
!it->second.IsSoftPinned(now) &&
|
|
!it->second.HasDiffRepStatus(ReplicaStatus::COMPLETE,
|
|
ReplicaType::MEMORY) &&
|
|
it->second.HasMemReplica()) {
|
|
// Evict this object
|
|
total_freed_size +=
|
|
it->second.size * it->second.GetMemReplicaCount();
|
|
it->second.EraseReplica(
|
|
ReplicaType::MEMORY); // Erase memory replicas
|
|
if (it->second.IsValid() == false) {
|
|
it = shard.metadata.erase(it);
|
|
} else {
|
|
++it;
|
|
}
|
|
evicted_count++;
|
|
target_evict_num--;
|
|
} else {
|
|
++it;
|
|
}
|
|
}
|
|
}
|
|
} else if (!soft_pin_objects.empty()) {
|
|
// Second pass B: Prioritize evicting objects without soft pin, but
|
|
// also allow to evict soft pinned objects. The following code is
|
|
// error-prone if the soft pin objects are empty.
|
|
|
|
const long soft_pin_evict_num =
|
|
target_evict_num - static_cast<long>(no_pin_objects.size());
|
|
// For soft pin objects, prioritize to evict the ones with smaller
|
|
// lease timeout.
|
|
std::nth_element(
|
|
soft_pin_objects.begin(),
|
|
soft_pin_objects.begin() + (soft_pin_evict_num - 1),
|
|
soft_pin_objects.end());
|
|
auto soft_target_timeout = soft_pin_objects[soft_pin_evict_num - 1];
|
|
|
|
// Stop when the target is reached.
|
|
for (size_t i = 0;
|
|
i < metadata_shards_.size() && target_evict_num > 0; i++) {
|
|
auto& shard =
|
|
metadata_shards_[(start_idx + i) % metadata_shards_.size()];
|
|
MutexLocker lock(&shard.mutex);
|
|
|
|
auto it = shard.metadata.begin();
|
|
while (it != shard.metadata.end() && target_evict_num > 0) {
|
|
// Skip objects that are not expired or have incomplete
|
|
// replicas
|
|
if (!it->second.IsLeaseExpired(now) ||
|
|
it->second.HasDiffRepStatus(ReplicaStatus::COMPLETE,
|
|
ReplicaType::MEMORY) ||
|
|
!it->second.HasMemReplica()) {
|
|
++it;
|
|
continue;
|
|
}
|
|
// Evict objects with 1). no soft pin OR 2). with soft pin
|
|
// and lease timeout less than or equal to target.
|
|
if (!it->second.IsSoftPinned(now) ||
|
|
it->second.lease_timeout <= soft_target_timeout) {
|
|
total_freed_size +=
|
|
it->second.size * it->second.GetMemReplicaCount();
|
|
it->second.EraseReplica(
|
|
ReplicaType::MEMORY); // Erase memory replicas
|
|
if (it->second.IsValid() == false) {
|
|
it = shard.metadata.erase(it);
|
|
} else {
|
|
++it;
|
|
}
|
|
evicted_count++;
|
|
target_evict_num--;
|
|
} else {
|
|
++it;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// This should not happen.
|
|
LOG(ERROR) << "Error in second pass eviction: target_evict_num="
|
|
<< target_evict_num
|
|
<< ", no_pin_objects.size()=" << no_pin_objects.size()
|
|
<< ", soft_pin_objects.size()="
|
|
<< soft_pin_objects.size()
|
|
<< ", evicted_count=" << evicted_count
|
|
<< ", object_count=" << object_count
|
|
<< ", evict_ratio_target=" << evict_ratio_target
|
|
<< ", evict_ratio_lowerbound=" << evict_ratio_lowerbound;
|
|
}
|
|
}
|
|
|
|
if (evicted_count > 0) {
|
|
need_eviction_ = false;
|
|
MasterMetricManager::instance().inc_eviction_success(evicted_count,
|
|
total_freed_size);
|
|
} else {
|
|
if (object_count == 0) {
|
|
// No objects to evict, no need to check again
|
|
need_eviction_ = false;
|
|
}
|
|
MasterMetricManager::instance().inc_eviction_fail();
|
|
}
|
|
VLOG(1) << "action=evict_objects"
|
|
<< ", evicted_count=" << evicted_count
|
|
<< ", total_freed_size=" << total_freed_size;
|
|
}
|
|
|
|
void MasterService::ClientMonitorFunc() {
|
|
std::unordered_map<UUID, std::chrono::steady_clock::time_point,
|
|
boost::hash<UUID>>
|
|
client_ttl;
|
|
while (client_monitor_running_) {
|
|
auto now = std::chrono::steady_clock::now();
|
|
|
|
// Update the client ttl
|
|
PodUUID pod_client_id;
|
|
while (client_ping_queue_.pop(pod_client_id)) {
|
|
UUID client_id = {pod_client_id.first, pod_client_id.second};
|
|
client_ttl[client_id] =
|
|
now + std::chrono::seconds(client_live_ttl_sec_);
|
|
}
|
|
|
|
// Find out expired clients
|
|
std::vector<UUID> expired_clients;
|
|
for (auto it = client_ttl.begin(); it != client_ttl.end();) {
|
|
if (it->second < now) {
|
|
LOG(INFO) << "client_id=" << it->first
|
|
<< ", action=client_expired";
|
|
expired_clients.push_back(it->first);
|
|
it = client_ttl.erase(it);
|
|
} else {
|
|
++it;
|
|
}
|
|
}
|
|
|
|
// Update the client status to NEED_REMOUNT
|
|
if (!expired_clients.empty()) {
|
|
// Record which segments are unmounted, will be used in the commit
|
|
// phase.
|
|
std::vector<UUID> unmount_segments;
|
|
std::vector<size_t> dec_capacities;
|
|
std::vector<UUID> client_ids;
|
|
std::vector<std::string> segment_names;
|
|
{
|
|
// Lock client_mutex and segment_mutex
|
|
std::unique_lock<std::shared_mutex> lock(client_mutex_);
|
|
for (auto& client_id : expired_clients) {
|
|
auto it = ok_client_.find(client_id);
|
|
if (it != ok_client_.end()) {
|
|
ok_client_.erase(it);
|
|
MasterMetricManager::instance().dec_active_clients();
|
|
}
|
|
}
|
|
|
|
ScopedSegmentAccess segment_access =
|
|
segment_manager_.getSegmentAccess();
|
|
for (auto& client_id : expired_clients) {
|
|
std::vector<Segment> segments;
|
|
segment_access.GetClientSegments(client_id, segments);
|
|
for (auto& seg : segments) {
|
|
size_t metrics_dec_capacity = 0;
|
|
if (segment_access.PrepareUnmountSegment(
|
|
seg.id, metrics_dec_capacity) ==
|
|
ErrorCode::OK) {
|
|
unmount_segments.push_back(seg.id);
|
|
dec_capacities.push_back(metrics_dec_capacity);
|
|
client_ids.push_back(client_id);
|
|
segment_names.push_back(seg.name);
|
|
} else {
|
|
LOG(ERROR) << "client_id=" << client_id
|
|
<< ", segment_name=" << seg.name
|
|
<< ", "
|
|
"error=prepare_unmount_expired_"
|
|
"segment_failed";
|
|
}
|
|
}
|
|
}
|
|
} // Release the mutex before long-running ClearInvalidHandles and
|
|
// avoid deadlocks
|
|
|
|
if (!unmount_segments.empty()) {
|
|
ClearInvalidHandles();
|
|
|
|
ScopedSegmentAccess segment_access =
|
|
segment_manager_.getSegmentAccess();
|
|
for (size_t i = 0; i < unmount_segments.size(); i++) {
|
|
segment_access.CommitUnmountSegment(
|
|
unmount_segments[i], client_ids[i], dec_capacities[i]);
|
|
LOG(INFO) << "client_id=" << client_ids[i]
|
|
<< ", segment_name=" << segment_names[i]
|
|
<< ", action=unmount_expired_segment";
|
|
}
|
|
}
|
|
}
|
|
|
|
std::this_thread::sleep_for(
|
|
std::chrono::milliseconds(kClientMonitorSleepMs));
|
|
}
|
|
}
|
|
|
|
std::string MasterService::SanitizeKey(const std::string& key) const {
|
|
// Set of invalid filesystem characters to be replaced
|
|
constexpr std::string_view kInvalidChars = "/\\:*?\"<>|";
|
|
std::string sanitized_key;
|
|
sanitized_key.reserve(key.size());
|
|
|
|
for (char c : key) {
|
|
// Replace invalid characters with underscore
|
|
sanitized_key.push_back(
|
|
kInvalidChars.find(c) != std::string_view::npos ? '_' : c);
|
|
}
|
|
return sanitized_key;
|
|
}
|
|
|
|
std::string MasterService::ResolvePath(const std::string& key) const {
|
|
// Compute hash of the key
|
|
size_t hash = std::hash<std::string>{}(key);
|
|
|
|
// Use low 8 bits to create 2-level directory structure (e.g. "a1/b2")
|
|
char dir1 =
|
|
static_cast<char>('a' + (hash & 0x0F)); // Lower 4 bits -> 16 dirs
|
|
char dir2 = static_cast<char>(
|
|
'a' + ((hash >> 4) & 0x0F)); // Next 4 bits -> 16 subdirs
|
|
|
|
// Safely construct path using std::filesystem
|
|
namespace fs = std::filesystem;
|
|
fs::path dir_path = fs::path(std::string(1, dir1)) / std::string(1, dir2);
|
|
|
|
// Combine directory path with sanitized filename
|
|
fs::path full_path =
|
|
fs::path(root_fs_dir_) / cluster_id_ / dir_path / SanitizeKey(key);
|
|
|
|
return full_path.lexically_normal().string();
|
|
}
|
|
|
|
} // namespace mooncake
|