[Store] feat: Add BatchQueryIp API for querying multiple client IPs (#1162)

* [Store] feat: Add BatchQueryIp API for querying multiple client IPs

Add BatchQueryIp API with client test and master server test

Signed-off-by: Vincent Gao <vincentbo@linux.alibaba.com>

* [Doc] Add API documents for BatchQueryIp

Signed-off-by: Vincent Gao <vincentbo@linux.alibaba.com>

---------

Signed-off-by: Vincent Gao <vincentbo@linux.alibaba.com>
This commit is contained in:
Vincent-Bo-ali 2025-12-08 17:29:04 +08:00 committed by GitHub
parent d116df6c4e
commit 72bfa28c04
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 720 additions and 18 deletions

View File

@ -112,6 +112,15 @@ tl::expected<void, ErrorCode> Remove(const ObjectKey& key);
Used to delete the object corresponding to the specified key. This interface marks all data replicas associated with the key in the storage engine as deleted, without needing to communicate with the corresponding storage node (Client).
### BatchQueryIp
```C++
tl::expected<std::unordered_map<UUID, std::vector<std::string>, boost::hash<UUID>>, ErrorCode>
BatchQueryIp(const std::vector<UUID>& client_ids);
```
Used to batch query the IP addresses for multiple client IDs. For each client ID in the input list, this interface retrieves the unique IP addresses from all segments mounted by that client. The operation is performed on the Master Service and returns a map from client ID to their IP address lists. Only client IDs that have successfully mounted segments are included in the result map. This is useful for discovering the network locations of storage nodes in the cluster.
### QueryByRegex
```C++
@ -175,6 +184,9 @@ service MasterService {
// Get replica lists for objects matching a regex
rpc GetReplicaListByRegex(GetReplicaListByRegexRequest) returns (GetReplicaListByRegexResponse);
// Batch query IP addresses for multiple client IDs
rpc BatchQueryIp(BatchQueryIpRequest) returns (BatchQueryIpResponse);
// Start Put operation, allocate storage space
rpc PutStart(PutStartRequest) returns (PutStartResponse);
@ -233,7 +245,28 @@ message GetReplicaListByRegexResponse {
- **Response**: GetReplicaListByRegexResponse, which contains a status_code and an object_map. The keys of this map are the successfully matched object keys, and the values are the lists of replica information for each key.
- **Description**: Used to query for all keys and their replica information that match the specified regular expression. This interface facilitates bulk queries and management.
3. PutStart
3. BatchQueryIp
```protobuf
message BatchQueryIpRequest {
repeated UUID client_ids = 1; // List of client IDs to query
};
message BatchQueryIpResponse {
required int32 status_code = 1;
map<UUID, IPAddressList> client_ip_map = 2; // Map from client ID to their IP address lists
};
message IPAddressList {
repeated string ip_addresses = 1; // List of unique IP addresses
};
```
- **Request**: `BatchQueryIpRequest` containing a list of client IDs to query.
- **Response**: `BatchQueryIpResponse` containing the status code `status_code` and a `client_ip_map`. The keys of this map are the client IDs that have successfully mounted segments, and the values are lists of unique IP addresses extracted from all segments mounted by each client. Client IDs that have no mounted segments or are not found are silently skipped and not included in the result map.
- **Description**: Used to batch query the IP addresses for multiple client IDs. For each client ID in the input list, this interface retrieves the unique IP addresses from all segments mounted by that client.
4. PutStart
```protobuf
message PutStartRequest {
@ -253,7 +286,7 @@ message PutStartResponse {
- **Response**: `PutStartResponse` containing the status code status_code and the allocated replica information replica_list.
- **Description**: Before writing an object, the Client must call PutStart to request storage space from the Master Service. The Master Service allocates space based on the config and returns the allocation results (`replica_list`) to the Client. The allocation strategy ensures that each slice of the object is placed in different segments, while operating on a best-effort basis - if insufficient space is available for all requested replicas, as many replicas as possible will be allocated. The Client then writes data to the storage nodes where the allocated replicas are located. The need for both start and end steps ensures that other Clients do not read partially written values, preventing dirty reads.
4. PutEnd
5. PutEnd
```protobuf
message PutEndRequest {
@ -269,7 +302,7 @@ message PutEndResponse {
- **Response**: `PutEndResponse` containing the status code status_code.
- **Description**: After the Client completes data writing, it calls `PutEnd` to notify the Master Service. The Master Service updates the object's metadata, marking the replica status as `COMPLETE`, indicating that the object is readable.
5. Remove
6. Remove
```protobuf
message RemoveRequest {
@ -285,7 +318,7 @@ message RemoveResponse {
- **Response**: `RemoveResponse` containing the status code `status_code`.
- **Description**: Used to delete the object and all its replicas corresponding to the specified key. The Master Service marks all replicas of the corresponding object as deleted.
6. RemoveByRegex
7. RemoveByRegex
```protobuf
message RemoveByRegexRequest {
@ -302,7 +335,7 @@ message RemoveByRegexResponse {
- **Response**: RemoveByRegexResponse, which contains a status_code and the number of objects that were removed, removed_count.
- **Description**: Used to delete all objects and their corresponding replicas for keys that match the specified regular expression. Similar to the Remove interface, this is a metadata operation where the Master Service marks the status of all matched object replicas as removed.
7. MountSegment
8. MountSegment
```protobuf
message MountSegmentRequest {
@ -318,7 +351,7 @@ message MountSegmentResponse {
The storage node (Client) allocates a segment of memory and, after calling `TransferEngine::registerLocalMemory` to complete local mounting, calls this interface to mount the allocated continuous address space to the Master Service for allocation.
8. UnmountSegment
9. UnmountSegment
```protobuf
message UnmountSegmentRequest {

View File

@ -124,6 +124,15 @@ tl::expected<long, ErrorCode> RemoveByRegex(const ObjectKey& str);
用于删除与正则表达式匹配的所有 key 对应的对象。其余能力类似 Remove。
### BatchQueryIp
```C++
tl::expected<std::unordered_map<UUID, std::vector<std::string>, boost::hash<UUID>>, ErrorCode>
BatchQueryIp(const std::vector<UUID>& client_ids);
```
用于批量查询多个客户端 ID 的 IP 地址。对于输入列表中的每个客户端 ID此接口会检索该客户端挂载的所有网段中的唯一 IP 地址。此操作在主服务器上执行,并返回一个从客户端 ID 到其 IP 地址列表的映射。
### QueryByRegex
```C++
@ -177,6 +186,9 @@ service MasterService {
// 获取与正则表达式匹配的对性的副本列表
rpc GetReplicaListByRegex(GetReplicaListByRegexRequest) returns (GetReplicaListByRegexResponse);
// 批量查询多个客户端 ID 的 IP 地址
rpc BatchQueryIp(BatchQueryIpRequest) returns (BatchQueryIpResponse);
// 开始 Put 操作,分配存储空间
rpc PutStart(PutStartRequest) returns (PutStartResponse);
@ -237,7 +249,29 @@ message GetReplicaListByRegexResponse {
说明: 用于查询与指定正则表达式匹配的所有 key 及其副本信息。该接口方便进行批量查询和管理。
3. PutStart
3. BatchQueryIp
```protobuf
message BatchQueryIpRequest {
repeated UUID client_ids = 1; // 客户端ID列表
};
message BatchQueryIpResponse {
required int32 status_code = 1;
map<UUID, IPAddressList> client_ip_map = 2; // 从客户端 ID 到其 IP 地址列表的映射
};
message IPAddressList {
repeated string ip_addresses = 1; // 唯一 IP 地址列表
};
```
* 请求: BatchQueryIpRequest 包含要查询的客户端 ID 列表。
* 响应: BatchQueryIpResponse 包含状态码 status_code 和 client_ip_map。该映射的键是已成功挂载网段的客户端 ID值是从每个客户端挂载的所有网段中提取的唯一 IP 地址列表。未挂载网段或未找到的客户端 ID 将被静默跳过,不包含在结果映射中。
说明: 用于批量查询多个客户端 ID 的 IP 地址。对于输入列表中的每个客户端 ID此接口会从该客户端挂载的所有网段中检索唯一的 IP 地址。
4. PutStart
```protobuf
message PutStartRequest {
@ -258,7 +292,7 @@ message PutStartResponse {
说明: Client 在写入对象前,需要先调用 PutStart 向 `Master Service` 申请存储空间。`Master Service` 会根据 config 分配空间并将分配结果replica_list返回给 Client。分配策略确保对象的每个slice被放置在不同的segment中同时采用尽力而为的方式运行——如果没有足够的空间来分配所有请求的副本将分配尽可能多的副本。Client 随后将数据写入到分配副本所在的存储节点。 之所以需要 start 和 end 两步是为确保其他Client不会读到正在写的值进而造成脏读。
4. PutEnd
5. PutEnd
```protobuf
message PutEndRequest {
@ -275,7 +309,7 @@ message PutEndResponse {
Client 完成数据写入后,调用 PutEnd 通知 `Master Service`。`Master Service` 将更新对象的元数据信息,将副本状态标记为 COMPLETE表示该对象可以被读取。
5. Remove
6. Remove
```protobuf
message RemoveRequest {
@ -292,7 +326,7 @@ message RemoveResponse {
用于删除指定 key 对应的对象及其所有副本。Master Service 将对应对象的所有副本状态标记为删除。
6. RemoveByRegex
7. RemoveByRegex
```protobuf
message RemoveByRegexRequest {
@ -310,7 +344,7 @@ message RemoveByRegexResponse {
说明: 用于删除与指定正则表达式匹配的所有对象及其全部副本。与 Remove 接口类似这是一个元数据操作Master Service 将所有匹配对象的副本状态标记为删除。
7. MountSegment
8. MountSegment
```protobuf
message MountSegmentRequest {
@ -326,7 +360,7 @@ message MountSegmentResponse {
存储节点(Client)自己分配一段内存,然后在调用`TransferEngine::registerLoalMemory` 完成本地挂载后,调用该接口,将分配好的一段连续的地址空间挂载到`Master Service`用于分配。
8. UnmountSegment
9. UnmountSegment
```protobuf
message UnmountSegmentRequest {

View File

@ -112,6 +112,15 @@ tl::expected<void, ErrorCode> Remove(const ObjectKey& key);
Used to delete the object corresponding to the specified key. This interface marks all data replicas associated with the key in the storage engine as deleted, without needing to communicate with the corresponding storage node (Client).
### BatchQueryIp
```C++
tl::expected<std::unordered_map<UUID, std::vector<std::string>, boost::hash<UUID>>, ErrorCode>
BatchQueryIp(const std::vector<UUID>& client_ids);
```
Used to batch query the IP addresses for multiple client IDs. For each client ID in the input list, this interface retrieves the unique IP addresses from all segments mounted by that client. The operation is performed on the Master Service and returns a map from client ID to their IP address lists. Only client IDs that have successfully mounted segments are included in the result map. This is useful for discovering the network locations of storage nodes in the cluster.
### QueryByRegex
```C++
@ -175,6 +184,9 @@ service MasterService {
// Get replica lists for objects matching a regex
rpc GetReplicaListByRegex(GetReplicaListByRegexRequest) returns (GetReplicaListByRegexResponse);
// Batch query IP addresses for multiple client IDs
rpc BatchQueryIp(BatchQueryIpRequest) returns (BatchQueryIpResponse);
// Start Put operation, allocate storage space
rpc PutStart(PutStartRequest) returns (PutStartResponse);
@ -233,7 +245,28 @@ message GetReplicaListByRegexResponse {
- **Response**: GetReplicaListByRegexResponse, which contains a status_code and an object_map. The keys of this map are the successfully matched object keys, and the values are the lists of replica information for each key.
- **Description**: Used to query for all keys and their replica information that match the specified regular expression. This interface facilitates bulk queries and management.
3. PutStart
3. BatchQueryIp
```protobuf
message BatchQueryIpRequest {
repeated UUID client_ids = 1; // List of client IDs to query
};
message BatchQueryIpResponse {
required int32 status_code = 1;
map<UUID, IPAddressList> client_ip_map = 2; // Map from client ID to their IP address lists
};
message IPAddressList {
repeated string ip_addresses = 1; // List of unique IP addresses
};
```
- **Request**: `BatchQueryIpRequest` containing a list of client IDs to query.
- **Response**: `BatchQueryIpResponse` containing the status code `status_code` and a `client_ip_map`. The keys of this map are the client IDs that have successfully mounted segments, and the values are lists of unique IP addresses extracted from all segments mounted by each client. Client IDs that have no mounted segments or are not found are silently skipped and not included in the result map.
- **Description**: Used to batch query the IP addresses for multiple client IDs. For each client ID in the input list, this interface retrieves the unique IP addresses from all segments mounted by that client.
4. PutStart
```protobuf
message PutStartRequest {
@ -253,7 +286,7 @@ message PutStartResponse {
- **Response**: `PutStartResponse` containing the status code status_code and the allocated replica information replica_list.
- **Description**: Before writing an object, the Client must call PutStart to request storage space from the Master Service. The Master Service allocates space based on the config and returns the allocation results (`replica_list`) to the Client. The allocation strategy ensures that each slice of the object is placed in different segments, while operating on a best-effort basis - if insufficient space is available for all requested replicas, as many replicas as possible will be allocated. The Client then writes data to the storage nodes where the allocated replicas are located. The need for both start and end steps ensures that other Clients do not read partially written values, preventing dirty reads.
4. PutEnd
5. PutEnd
```protobuf
message PutEndRequest {
@ -269,7 +302,7 @@ message PutEndResponse {
- **Response**: `PutEndResponse` containing the status code status_code.
- **Description**: After the Client completes data writing, it calls `PutEnd` to notify the Master Service. The Master Service updates the object's metadata, marking the replica status as `COMPLETE`, indicating that the object is readable.
5. Remove
6. Remove
```protobuf
message RemoveRequest {
@ -285,7 +318,7 @@ message RemoveResponse {
- **Response**: `RemoveResponse` containing the status code `status_code`.
- **Description**: Used to delete the object and all its replicas corresponding to the specified key. The Master Service marks all replicas of the corresponding object as deleted.
6. RemoveByRegex
7. RemoveByRegex
```protobuf
message RemoveByRegexRequest {
@ -302,7 +335,7 @@ message RemoveByRegexResponse {
- **Response**: RemoveByRegexResponse, which contains a status_code and the number of objects that were removed, removed_count.
- **Description**: Used to delete all objects and their corresponding replicas for keys that match the specified regular expression. Similar to the Remove interface, this is a metadata operation where the Master Service marks the status of all matched object replicas as removed.
7. MountSegment
8. MountSegment
```protobuf
message MountSegmentRequest {
@ -318,7 +351,7 @@ message MountSegmentResponse {
The storage node (Client) allocates a segment of memory and, after calling `TransferEngine::registerLocalMemory` to complete local mounting, calls this interface to mount the allocated continuous address space to the Master Service for allocation.
8. UnmountSegment
9. UnmountSegment
```protobuf
message UnmountSegmentRequest {

View File

@ -97,6 +97,17 @@ class Client {
const std::vector<std::string>& object_keys,
std::unordered_map<std::string, std::vector<Slice>>& slices);
/**
* @brief Batch query IP addresses for multiple client IDs.
* @param client_ids Vector of client UUIDs to query.
* @return An expected object containing a map from client_id to their IP
* address lists on success, or an ErrorCode on failure.
*/
tl::expected<
std::unordered_map<UUID, std::vector<std::string>, boost::hash<UUID>>,
ErrorCode>
BatchQueryIp(const std::vector<UUID>& client_ids);
/**
* @brief Gets object metadata without transferring data
* @param object_key Key to query

View File

@ -4,6 +4,7 @@
#include <string>
#include <vector>
#include <cstdlib>
#include <boost/functional/hash.hpp>
#include <ylt/coro_rpc/coro_rpc_client.hpp>
#include <ylt/coro_io/client_pool.hpp>
@ -72,6 +73,17 @@ class MasterClient {
[[nodiscard]] tl::expected<MasterMetricManager::CacheHitStatDict, ErrorCode>
CalcCacheStats();
/**
* @brief Batch query IP addresses for multiple client IDs.
* @param client_ids Vector of client UUIDs to query.
* @return An expected object containing a map from client_id to their IP
* address lists on success, or an ErrorCode on failure.
*/
[[nodiscard]] tl::expected<
std::unordered_map<UUID, std::vector<std::string>, boost::hash<UUID>>,
ErrorCode>
BatchQueryIp(const std::vector<UUID>& client_ids);
/**
* @brief Gets object metadata without transferring data
* @param object_key Key to query

View File

@ -118,6 +118,9 @@ class MasterMetricManager {
void inc_batch_exist_key_requests(int64_t items);
void inc_batch_exist_key_failures(int64_t failed_items);
void inc_batch_exist_key_partial_success(int64_t failed_items);
void inc_batch_query_ip_requests(int64_t items);
void inc_batch_query_ip_failures(int64_t failed_items);
void inc_batch_query_ip_partial_success(int64_t failed_items);
void inc_batch_get_replica_list_requests(int64_t items);
void inc_batch_get_replica_list_failures(int64_t failed_items);
void inc_batch_get_replica_list_partial_success(int64_t failed_items);
@ -165,6 +168,11 @@ class MasterMetricManager {
int64_t get_batch_exist_key_partial_successes();
int64_t get_batch_exist_key_items();
int64_t get_batch_exist_key_failed_items();
int64_t get_batch_query_ip_requests();
int64_t get_batch_query_ip_failures();
int64_t get_batch_query_ip_partial_successes();
int64_t get_batch_query_ip_items();
int64_t get_batch_query_ip_failed_items();
int64_t get_batch_get_replica_list_requests();
int64_t get_batch_get_replica_list_failures();
int64_t get_batch_get_replica_list_partial_successes();
@ -286,6 +294,11 @@ class MasterMetricManager {
ylt::metric::counter_t batch_exist_key_partial_successes_;
ylt::metric::counter_t batch_exist_key_items_;
ylt::metric::counter_t batch_exist_key_failed_items_;
ylt::metric::counter_t batch_query_ip_requests_;
ylt::metric::counter_t batch_query_ip_failures_;
ylt::metric::counter_t batch_query_ip_partial_successes_;
ylt::metric::counter_t batch_query_ip_items_;
ylt::metric::counter_t batch_query_ip_failed_items_;
ylt::metric::counter_t batch_get_replica_list_requests_;
ylt::metric::counter_t batch_get_replica_list_failures_;
ylt::metric::counter_t batch_get_replica_list_partial_successes_;

View File

@ -110,6 +110,28 @@ class MasterService {
auto QuerySegments(const std::string& segment)
-> tl::expected<std::pair<size_t, size_t>, ErrorCode>;
/**
* @brief Query IP addresses for a given client ID.
* @param client_id The UUID of the client to query.
* @return An expected object containing a vector of IP addresses on success
* (empty vector if client has no IPs), or ErrorCode::CLIENT_NOT_FOUND if
* the client doesn't exist, or another ErrorCode on other failures.
*/
auto QueryIp(const UUID& client_id)
-> tl::expected<std::vector<std::string>, ErrorCode>;
/**
* @brief Batch query IP addresses for multiple client IDs.
* @param client_ids Vector of client UUIDs to query.
* @return An expected object containing a map from client_id to their IP
* address lists on success, or an ErrorCode on failure. Non-existent
* clients are omitted from the result map. Clients that exist but have no
* IPs are included with empty vectors.
*/
auto BatchQueryIp(const std::vector<UUID>& client_ids) -> tl::expected<
std::unordered_map<UUID, std::vector<std::string>, boost::hash<UUID>>,
ErrorCode>;
/**
* @brief Retrieves replica lists for object keys that match a regex
* pattern.

View File

@ -1,6 +1,7 @@
#pragma once
#include <atomic>
#include <boost/functional/hash.hpp>
#include <cstdint>
#include <thread>
#include <ylt/coro_http/coro_http_server.hpp>
@ -32,6 +33,11 @@ class WrappedMasterService {
std::vector<tl::expected<bool, ErrorCode>> BatchExistKey(
const std::vector<std::string>& keys);
tl::expected<
std::unordered_map<UUID, std::vector<std::string>, boost::hash<UUID>>,
ErrorCode>
BatchQueryIp(const std::vector<UUID>& client_ids);
tl::expected<
std::unordered_map<std::string, std::vector<Replica::Descriptor>>,
ErrorCode>

View File

@ -100,6 +100,7 @@ enum class ErrorCode : int32_t {
SHARD_INDEX_OUT_OF_RANGE = -100, ///< Shard index is out of bounds.
SEGMENT_NOT_FOUND = -101, ///< No available segments found.
SEGMENT_ALREADY_EXISTS = -102, ///< Segment already exists.
CLIENT_NOT_FOUND = -103, ///< Client not found.
// Handle selection errors (Range: -200 to -299)
NO_AVAILABLE_HANDLE =

View File

@ -74,6 +74,11 @@ void to_stream(std::ostream& os, const std::vector<T>& vec) {
template <typename K, typename V>
void to_stream(std::ostream& os, const std::unordered_map<K, V>& map) {
to_stream<K, V, std::hash<K>>(os, map);
}
template <typename K, typename V, typename H>
void to_stream(std::ostream& os, const std::unordered_map<K, V, H>& map) {
os << "{";
auto it = map.begin();
while (it != map.end()) {

View File

@ -501,6 +501,14 @@ std::vector<tl::expected<void, ErrorCode>> Client::BatchGet(
return results;
}
tl::expected<
std::unordered_map<UUID, std::vector<std::string>, boost::hash<UUID>>,
ErrorCode>
Client::BatchQueryIp(const std::vector<UUID>& client_ids) {
auto result = master_client_.BatchQueryIp(client_ids);
return result;
}
tl::expected<std::unordered_map<std::string, std::vector<Replica::Descriptor>>,
ErrorCode>
Client::QueryByRegex(const std::string& str) {

View File

@ -41,6 +41,11 @@ struct RpcNameTraits<&WrappedMasterService::CalcCacheStats> {
static constexpr const char* value = "CalcCacheStats";
};
template <>
struct RpcNameTraits<&WrappedMasterService::BatchQueryIp> {
static constexpr const char* value = "BatchQueryIp";
};
template <>
struct RpcNameTraits<&WrappedMasterService::GetReplicaListByRegex> {
static constexpr const char* value = "GetReplicaListByRegex";
@ -281,6 +286,22 @@ MasterClient::CalcCacheStats() {
MasterMetricManager::CacheHitStatDict>();
}
tl::expected<
std::unordered_map<UUID, std::vector<std::string>, boost::hash<UUID>>,
ErrorCode>
MasterClient::BatchQueryIp(const std::vector<UUID>& client_ids) {
ScopedVLogTimer timer(1, "MasterClient::BatchQueryIp");
timer.LogRequest("client_ids_count=", client_ids.size());
auto result = invoke_rpc<
&WrappedMasterService::BatchQueryIp,
std::unordered_map<UUID, std::vector<std::string>, boost::hash<UUID>>>(
client_ids);
timer.LogResponseExpected(result);
return result;
}
tl::expected<std::unordered_map<std::string, std::vector<Replica::Descriptor>>,
ErrorCode>
MasterClient::GetReplicaListByRegex(const std::string& str) {

View File

@ -130,6 +130,20 @@ MasterMetricManager::MasterMetricManager()
batch_exist_key_failed_items_(
"master_batch_exist_key_failed_items_total",
"Total number of failed items in BatchExistKey requests"),
batch_query_ip_requests_(
"master_batch_query_ip_requests_total",
"Total number of BatchQueryIp requests received"),
batch_query_ip_failures_("master_batch_query_ip_failures_total",
"Total number of failed BatchQueryIp requests"),
batch_query_ip_partial_successes_(
"master_batch_query_ip_partial_successes_total",
"Total number of partially successful BatchQueryIp requests"),
batch_query_ip_items_(
"master_batch_query_ip_items_total",
"Total number of items processed in BatchQueryIp requests"),
batch_query_ip_failed_items_(
"master_batch_query_ip_failed_items_total",
"Total number of failed items in BatchQueryIp requests"),
batch_get_replica_list_requests_(
"master_batch_get_replica_list_requests_total",
"Total number of BatchGetReplicaList requests received"),
@ -274,6 +288,11 @@ void MasterMetricManager::update_metrics_for_zero_output() {
batch_exist_key_partial_successes_.inc(0);
batch_exist_key_items_.inc(0);
batch_exist_key_failed_items_.inc(0);
batch_query_ip_requests_.inc(0);
batch_query_ip_failures_.inc(0);
batch_query_ip_partial_successes_.inc(0);
batch_query_ip_items_.inc(0);
batch_query_ip_failed_items_.inc(0);
batch_get_replica_list_requests_.inc(0);
batch_get_replica_list_failures_.inc(0);
batch_get_replica_list_partial_successes_.inc(0);
@ -576,6 +595,19 @@ void MasterMetricManager::inc_batch_exist_key_partial_success(
batch_exist_key_partial_successes_.inc(1);
batch_exist_key_failed_items_.inc(failed_items);
}
void MasterMetricManager::inc_batch_query_ip_requests(int64_t items) {
batch_query_ip_requests_.inc(1);
batch_query_ip_items_.inc(items);
}
void MasterMetricManager::inc_batch_query_ip_failures(int64_t failed_items) {
batch_query_ip_failures_.inc(1);
batch_query_ip_failed_items_.inc(failed_items);
}
void MasterMetricManager::inc_batch_query_ip_partial_success(
int64_t failed_items) {
batch_query_ip_partial_successes_.inc(1);
batch_query_ip_failed_items_.inc(failed_items);
}
void MasterMetricManager::inc_batch_get_replica_list_requests(int64_t items) {
batch_get_replica_list_requests_.inc(1);
batch_get_replica_list_items_.inc(items);
@ -767,6 +799,26 @@ int64_t MasterMetricManager::get_batch_exist_key_failed_items() {
return batch_exist_key_failed_items_.value();
}
int64_t MasterMetricManager::get_batch_query_ip_requests() {
return batch_query_ip_requests_.value();
}
int64_t MasterMetricManager::get_batch_query_ip_failures() {
return batch_query_ip_failures_.value();
}
int64_t MasterMetricManager::get_batch_query_ip_partial_successes() {
return batch_query_ip_partial_successes_.value();
}
int64_t MasterMetricManager::get_batch_query_ip_items() {
return batch_query_ip_items_.value();
}
int64_t MasterMetricManager::get_batch_query_ip_failed_items() {
return batch_query_ip_failed_items_.value();
}
int64_t MasterMetricManager::get_batch_get_replica_list_requests() {
return batch_get_replica_list_requests_.value();
}
@ -946,6 +998,8 @@ std::string MasterMetricManager::serialize_metrics() {
// Serialize Batch Request Counters
serialize_metric(batch_exist_key_requests_);
serialize_metric(batch_exist_key_failures_);
serialize_metric(batch_query_ip_requests_);
serialize_metric(batch_query_ip_failures_);
serialize_metric(batch_get_replica_list_requests_);
serialize_metric(batch_get_replica_list_failures_);
serialize_metric(batch_put_start_requests_);
@ -1099,6 +1153,12 @@ std::string MasterMetricManager::get_summary_string() {
int64_t batch_exist_key_items = batch_exist_key_items_.value();
int64_t batch_exist_key_failed_items =
batch_exist_key_failed_items_.value();
int64_t batch_query_ip_requests = batch_query_ip_requests_.value();
int64_t batch_query_ip_fails = batch_query_ip_failures_.value();
int64_t batch_query_ip_partial_successes =
batch_query_ip_partial_successes_.value();
int64_t batch_query_ip_items = batch_query_ip_items_.value();
int64_t batch_query_ip_failed_items = batch_query_ip_failed_items_.value();
// Eviction counters
int64_t eviction_success = eviction_success_.value();
@ -1181,6 +1241,13 @@ std::string MasterMetricManager::get_summary_string() {
<< batch_exist_key_requests
<< ", Item=" << batch_exist_key_items - batch_exist_key_failed_items
<< "/" << batch_exist_key_items << "), ";
ss << "QueryIp:(Req="
<< batch_query_ip_requests - batch_query_ip_fails -
batch_query_ip_partial_successes
<< "/" << batch_query_ip_partial_successes << "/"
<< batch_query_ip_requests
<< ", Item=" << batch_query_ip_items - batch_query_ip_failed_items << "/"
<< batch_query_ip_items << "), ";
// Eviction summary
ss << " | Eviction: " << "Success/Attempts=" << eviction_success << "/"

View File

@ -4,6 +4,7 @@
#include <cstdint>
#include <shared_mutex>
#include <regex>
#include <unordered_set>
#include <ylt/util/tl/expected.hpp>
#include "master_metric_manager.h"
@ -280,6 +281,63 @@ auto MasterService::QuerySegments(const std::string& segment)
return std::make_pair(used, capacity);
}
auto MasterService::QueryIp(const UUID& client_id)
-> tl::expected<std::vector<std::string>, ErrorCode> {
ScopedSegmentAccess segment_access = segment_manager_.getSegmentAccess();
std::vector<Segment> segments;
ErrorCode err = segment_access.GetClientSegments(client_id, segments);
if (err != ErrorCode::OK) {
if (err == ErrorCode::SEGMENT_NOT_FOUND) {
VLOG(1) << "QueryIp: client_id=" << client_id
<< " not found or has no segments";
return tl::make_unexpected(ErrorCode::CLIENT_NOT_FOUND);
}
LOG(ERROR) << "QueryIp: failed to get segments for client_id="
<< client_id << ", error=" << toString(err);
return tl::make_unexpected(err);
}
std::unordered_set<std::string> unique_ips;
unique_ips.reserve(segments.size());
for (const auto& segment : segments) {
if (!segment.te_endpoint.empty()) {
size_t colon_pos = segment.te_endpoint.find(':');
if (colon_pos != std::string::npos) {
std::string ip = segment.te_endpoint.substr(0, colon_pos);
unique_ips.emplace(ip);
} else {
unique_ips.emplace(segment.te_endpoint);
}
}
}
if (unique_ips.empty()) {
LOG(WARNING) << "QueryIp: client_id=" << client_id
<< " has no valid IP addresses";
return {};
}
std::vector<std::string> result(unique_ips.begin(), unique_ips.end());
return result;
}
auto MasterService::BatchQueryIp(const std::vector<UUID>& client_ids)
-> tl::expected<
std::unordered_map<UUID, std::vector<std::string>, boost::hash<UUID>>,
ErrorCode> {
std::unordered_map<UUID, std::vector<std::string>, boost::hash<UUID>>
results;
results.reserve(client_ids.size());
for (const auto& client_id : client_ids) {
auto ip_result = QueryIp(client_id);
if (ip_result.has_value()) {
results.emplace(client_id, std::move(ip_result.value()));
}
}
return results;
}
auto MasterService::GetReplicaListByRegex(const std::string& regex_pattern)
-> tl::expected<
std::unordered_map<std::string, std::vector<Replica::Descriptor>>,

View File

@ -218,6 +218,46 @@ std::vector<tl::expected<bool, ErrorCode>> WrappedMasterService::BatchExistKey(
return result;
}
tl::expected<
std::unordered_map<UUID, std::vector<std::string>, boost::hash<UUID>>,
ErrorCode>
WrappedMasterService::BatchQueryIp(const std::vector<UUID>& client_ids) {
ScopedVLogTimer timer(1, "BatchQueryIp");
const size_t total_client_ids = client_ids.size();
timer.LogRequest("client_ids_count=", total_client_ids);
MasterMetricManager::instance().inc_batch_query_ip_requests(
total_client_ids);
auto result = master_service_.BatchQueryIp(client_ids);
size_t failure_count = 0;
if (!result.has_value()) {
failure_count = total_client_ids;
} else {
for (size_t i = 0; i < client_ids.size(); ++i) {
const auto& client_id = client_ids[i];
if (result.value().find(client_id) == result.value().end()) {
failure_count++;
VLOG(1) << "BatchQueryIp failed for client_id[" << i << "] '"
<< client_id << "': not found in results";
}
}
}
if (failure_count == total_client_ids) {
MasterMetricManager::instance().inc_batch_query_ip_failures(
failure_count);
} else if (failure_count != 0) {
MasterMetricManager::instance().inc_batch_query_ip_partial_success(
failure_count);
}
timer.LogResponse("total=", total_client_ids,
", success=", total_client_ids - failure_count,
", failures=", failure_count);
return result;
}
tl::expected<std::unordered_map<std::string, std::vector<Replica::Descriptor>>,
ErrorCode>
WrappedMasterService::GetReplicaListByRegex(const std::string& str) {
@ -604,6 +644,8 @@ void RegisterRpcService(
mooncake::WrappedMasterService& wrapped_master_service) {
server.register_handler<&mooncake::WrappedMasterService::ExistKey>(
&wrapped_master_service);
server.register_handler<&mooncake::WrappedMasterService::BatchQueryIp>(
&wrapped_master_service);
server.register_handler<
&mooncake::WrappedMasterService::GetReplicaListByRegex>(
&wrapped_master_service);

View File

@ -14,6 +14,7 @@ const std::string& toString(ErrorCode errorCode) noexcept {
{ErrorCode::SHARD_INDEX_OUT_OF_RANGE, "SHARD_INDEX_OUT_OF_RANGE"},
{ErrorCode::SEGMENT_NOT_FOUND, "SEGMENT_NOT_FOUND"},
{ErrorCode::SEGMENT_ALREADY_EXISTS, "SEGMENT_ALREADY_EXISTS"},
{ErrorCode::CLIENT_NOT_FOUND, "CLIENT_NOT_FOUND"},
{ErrorCode::NO_AVAILABLE_HANDLE, "NO_AVAILABLE_HANDLE"},
{ErrorCode::INVALID_VERSION, "INVALID_VERSION"},
{ErrorCode::INVALID_KEY, "INVALID_KEY"},

View File

@ -6,6 +6,11 @@
#include <memory>
#include <string>
#include <vector>
#include <regex>
#include <unordered_set>
#include <unordered_map>
#include <thread>
#include <chrono>
#include "allocator.h"
#include "client_service.h"
@ -22,6 +27,58 @@ DEFINE_uint64(default_kv_lease_ttl, mooncake::DEFAULT_DEFAULT_KV_LEASE_TTL,
namespace mooncake {
namespace testing {
// Helper functions for client_id parsing
std::string FormatClientId(const UUID& client_id) {
return std::to_string(client_id.first) + "-" +
std::to_string(client_id.second);
}
UUID ParseClientId(const std::string& client_id_str) {
UUID client_id{0, 0};
size_t dash_pos = client_id_str.find('-');
if (dash_pos != std::string::npos) {
try {
client_id.first = std::stoull(client_id_str.substr(0, dash_pos));
client_id.second = std::stoull(client_id_str.substr(dash_pos + 1));
} catch (const std::exception& e) {
LOG(ERROR) << "Failed to parse client_id: " << e.what();
}
} else {
LOG(ERROR) << "Invalid client_id format. Expected format: first-second";
}
return client_id;
}
class ClientIdCaptureSink : public google::LogSink {
public:
std::string captured_client_id;
void send(google::LogSeverity severity, const char* full_filename,
const char* base_filename, int line, const struct ::tm* tm_time,
const char* message, size_t message_len) override {
(void)severity;
(void)full_filename;
(void)base_filename;
(void)line;
(void)tm_time;
std::string msg(message, message_len);
size_t pos = msg.find("client_id=");
if (pos != std::string::npos) {
std::string client_id_str = msg.substr(pos + 10);
client_id_str.erase(0, client_id_str.find_first_not_of(" \t\n\r"));
client_id_str.erase(client_id_str.find_last_not_of(" \t\n\r") + 1);
std::regex uuid_pattern(R"((\d+)-(\d+))");
std::smatch match;
if (std::regex_search(client_id_str, match, uuid_pattern)) {
captured_client_id = match[0].str();
}
}
}
};
class ClientIntegrationTest : public ::testing::Test {
protected:
static std::shared_ptr<Client> CreateClient(const std::string& host_name) {
@ -94,13 +151,51 @@ class ClientIntegrationTest : public ::testing::Test {
static void InitializeClients() {
// This client is used for testing purposes.
// Capture test_client_ client_id from logs
ClientIdCaptureSink* test_client_sink = new ClientIdCaptureSink();
google::AddLogSink(test_client_sink);
test_client_ = CreateClient("localhost:17813");
ASSERT_TRUE(test_client_ != nullptr);
// Wait for logs to flush
std::this_thread::sleep_for(std::chrono::milliseconds(200));
google::RemoveLogSink(test_client_sink);
if (!test_client_sink->captured_client_id.empty()) {
UUID extracted_id =
ParseClientId(test_client_sink->captured_client_id);
if (extracted_id.first != 0 || extracted_id.second != 0) {
test_client_id_ = extracted_id;
LOG(INFO) << "Captured test_client_id: "
<< FormatClientId(test_client_id_);
}
}
delete test_client_sink;
// This client is used to provide segments.
// Capture segment_provider_client_ client_id from logs
ClientIdCaptureSink* provider_client_sink = new ClientIdCaptureSink();
google::AddLogSink(provider_client_sink);
segment_provider_client_ = CreateClient("localhost:17812");
ASSERT_TRUE(segment_provider_client_ != nullptr);
// Wait for logs to flush
std::this_thread::sleep_for(std::chrono::milliseconds(200));
google::RemoveLogSink(provider_client_sink);
if (!provider_client_sink->captured_client_id.empty()) {
UUID extracted_id =
ParseClientId(provider_client_sink->captured_client_id);
if (extracted_id.first != 0 || extracted_id.second != 0) {
segment_provider_client_id_ = extracted_id;
LOG(INFO) << "Captured segment_provider_client_id: "
<< FormatClientId(segment_provider_client_id_);
}
}
delete provider_client_sink;
client_buffer_allocator_ =
std::make_unique<SimpleAllocator>(128 * 1024 * 1024);
auto register_result = test_client_->RegisterLocalMemory(
@ -174,6 +269,8 @@ class ClientIntegrationTest : public ::testing::Test {
static InProcMaster master_;
static std::string master_address_;
static std::string metadata_url_;
static UUID test_client_id_;
static UUID segment_provider_client_id_;
};
// Static members initialization
@ -190,6 +287,8 @@ uint64_t ClientIntegrationTest::default_kv_lease_ttl_ = 0;
InProcMaster ClientIntegrationTest::master_;
std::string ClientIntegrationTest::master_address_;
std::string ClientIntegrationTest::metadata_url_;
UUID ClientIntegrationTest::test_client_id_{0, 0};
UUID ClientIntegrationTest::segment_provider_client_id_{0, 0};
// Test basic Put/Get operations through the client
TEST_F(ClientIntegrationTest, BasicPutGetOperations) {
@ -629,6 +728,100 @@ TEST_F(ClientIntegrationTest, BatchIsExistOperations) {
}
}
// Test batch QueryIp operations through the client
TEST_F(ClientIntegrationTest, BatchQueryIpOperations) {
// Skip test if we couldn't capture client_ids
if ((test_client_id_.first == 0 && test_client_id_.second == 0) ||
(segment_provider_client_id_.first == 0 &&
segment_provider_client_id_.second == 0)) {
GTEST_SKIP()
<< "Could not capture client_ids, skipping BatchQueryIp test";
}
// Test 1: Query IP for test_client_
std::vector<UUID> client_ids = {test_client_id_};
auto result = test_client_->BatchQueryIp(client_ids);
ASSERT_TRUE(result.has_value())
<< "BatchQueryIp failed: " << toString(result.error());
const auto& results = result.value();
ASSERT_FALSE(results.empty()) << "BatchQueryIp returned empty results";
auto it = results.find(test_client_id_);
ASSERT_NE(it, results.end()) << "test_client_id not found in results";
const auto& ip_addresses = it->second;
ASSERT_FALSE(ip_addresses.empty())
<< "test_client_ should have at least one IP address";
LOG(INFO) << "test_client_ IP addresses (" << ip_addresses.size() << "):";
for (size_t i = 0; i < ip_addresses.size(); ++i) {
LOG(INFO) << " [" << (i + 1) << "] " << ip_addresses[i];
}
// Verify IP addresses are valid (should contain "127.0.0.1" or "localhost")
bool has_valid_ip = false;
for (const auto& ip : ip_addresses) {
if (ip == "127.0.0.1" || ip.find("127.0.0.1") != std::string::npos ||
ip == "localhost" || ip.find("localhost") != std::string::npos) {
has_valid_ip = true;
break;
}
}
EXPECT_TRUE(has_valid_ip) << "Expected at least one valid IP address";
// Test 2: Query IP for multiple client_ids
std::vector<UUID> multiple_ids = {test_client_id_,
segment_provider_client_id_};
auto multi_result = test_client_->BatchQueryIp(multiple_ids);
ASSERT_TRUE(multi_result.has_value())
<< "BatchQueryIp failed for multiple client_ids: "
<< toString(multi_result.error());
const auto& multi_results = multi_result.value();
// Verify test_client_id_ is in results
auto test_it = multi_results.find(test_client_id_);
if (test_it != multi_results.end()) {
EXPECT_FALSE(test_it->second.empty())
<< "test_client_ should have IP addresses";
}
// Verify segment_provider_client_id_ is in results
auto provider_it = multi_results.find(segment_provider_client_id_);
if (provider_it != multi_results.end()) {
EXPECT_FALSE(provider_it->second.empty())
<< "segment_provider_client_ should have IP addresses";
LOG(INFO) << "segment_provider_client_ IP addresses ("
<< provider_it->second.size() << "):";
for (size_t i = 0; i < provider_it->second.size(); ++i) {
LOG(INFO) << " [" << (i + 1) << "] " << provider_it->second[i];
}
}
// Test 3: Query with empty client_ids list
std::vector<UUID> empty_client_ids;
auto empty_result = test_client_->BatchQueryIp(empty_client_ids);
ASSERT_TRUE(empty_result.has_value());
EXPECT_TRUE(empty_result.value().empty())
<< "Empty client_ids should return empty results";
// Test 4: Query with non-existent client_id (should be silently skipped)
UUID non_existent_client_id = generate_uuid();
std::vector<UUID> non_existent_ids = {non_existent_client_id};
auto non_existent_result = test_client_->BatchQueryIp(non_existent_ids);
ASSERT_TRUE(non_existent_result.has_value());
// Non-existent client_id should not be in results (silently skipped)
EXPECT_TRUE(non_existent_result.value().empty() ||
non_existent_result.value().find(non_existent_client_id) ==
non_existent_result.value().end())
<< "Non-existent client_id should not be in results";
}
// Test batch put with duplicate keys
TEST_F(ClientIntegrationTest, BatchPutDuplicateKeys) {
const std::string test_data = "test_data_duplicate";

View File

@ -2087,6 +2087,148 @@ TEST_F(MasterServiceTest, BatchExistKeyTest) {
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, PutStartExpiringTest) {
// Reset storage space metrics.
MasterMetricManager::instance().reset_allocated_mem_size();