From 358c53508c6f0640d24794a37aea9e26a3b2aa4a Mon Sep 17 00:00:00 2001 From: Quang-Minh Nguyen Date: Fri, 10 Oct 2025 08:52:15 +0700 Subject: [PATCH 1/4] proto: Add AddReplica RPC for operator-driven replica management Operators currently lack a high-level API to add replicas to Raft partitions. Existing internal methods require understanding Raft config changes, making cluster expansion complex and error-prone. This commit defines the AddReplica RPC in cluster.proto to enable safe, operator-friendly replica addition. The RPC accepts partition identification, target storage details, and desired replica type, returning the new replica's details and updated partition state. Part of: https://gitlab.com/gitlab-org/gitaly/-/issues/6908 --- proto/cluster.proto | 62 +++ proto/go/gitalypb/cluster.pb.go | 741 ++++++++++++++++++++------- proto/go/gitalypb/cluster_grpc.pb.go | 44 ++ 3 files changed, 655 insertions(+), 192 deletions(-) diff --git a/proto/cluster.proto b/proto/cluster.proto index 552eb70ccd..9a204eb200 100644 --- a/proto/cluster.proto +++ b/proto/cluster.proto @@ -138,6 +138,29 @@ message JoinClusterRequest { message JoinClusterResponse { } +// AddReplicaRequest is the request for the AddReplica RPC. +message AddReplicaRequest { + // partition_key identifies the partition to add a replica to. + RaftPartitionKey partition_key = 1; + // target_storage is the name of the storage where the new replica should be created. + string target_storage = 2; + // target_address is the network address of the target storage. + string target_address = 3; + // replica_type specifies whether to add the replica as a voter or learner. + ReplicaID.ReplicaType replica_type = 4; +} + +// AddReplicaResponse is the response for the AddReplica RPC. +message AddReplicaResponse { + // new_replica_id contains details of the newly added replica. + ReplicaID new_replica_id = 1; + // partition_info contains the current state of the partition after the replica was added. + RaftClusterInfoResponse partition_info = 2; + // replica_type confirms the type of replica that was added. + ReplicaID.ReplicaType replica_type = 3; +} + + // GetPartitionsRequest is the request for the GetPartitions RPC. message GetPartitionsRequest { // cluster_id is the identifier of the Raft cluster to query. @@ -165,6 +188,7 @@ message GetPartitionsRequest { bool include_relative_paths = 6; } + // ClusterStatistics contains aggregated statistics about the entire cluster. message ClusterStatistics { // StorageStats contains statistics for a specific storage/authority. @@ -252,10 +276,38 @@ message RaftClusterInfoRequest { // RaftClusterInfoResponse is the response for the GetClusterInfo RPC. message RaftClusterInfoResponse { + // ReplicaStatus contains status information about a single replica. + message ReplicaStatus { + // replica_id uniquely identifies the replica. + ReplicaID replica_id = 1; + // is_leader indicates whether this replica is currently the leader. + bool is_leader = 2; + // is_healthy indicates whether the replica is reachable and functioning. + bool is_healthy = 3; + // last_index is the index of the last log entry this replica has. + uint64 last_index = 4; + // match_index is the highest log index known to be replicated on this replica. + uint64 match_index = 5; + // state represents the Raft state of this replica (follower, candidate, leader). + string state = 6; + } + // cluster_id is the identifier of the Raft cluster. string cluster_id = 1; // statistics contains aggregated cluster statistics. ClusterStatistics statistics = 2; + // partition_key identifies the partition this response describes. + RaftPartitionKey partition_key = 3; + // replicas contains information about all replicas in this partition. + repeated ReplicaStatus replicas = 4; + // leader_id is the member ID of the current leader, 0 if no leader. + uint64 leader_id = 5; + // term is the current Raft term for this partition. + uint64 term = 6; + // index is the current Raft log index for this partition. + uint64 index = 7; + // relative_path is the repository path for backward compatibility. + string relative_path = 8; } // RaftService manages the sending of Raft messages to peers. @@ -303,4 +355,14 @@ service RaftService { scope_level: STORAGE }; } + + // AddReplica adds a new replica to an existing Raft partition. This is an operator-facing + // RPC that safely coordinates membership changes, handling config changes, remote replica + // initialization, and cleanup on failure. The operation can only be performed by the partition leader. + rpc AddReplica(AddReplicaRequest) returns (AddReplicaResponse) { + option (op_type) = { + op: MUTATOR + scope_level: STORAGE + }; + } } diff --git a/proto/go/gitalypb/cluster.pb.go b/proto/go/gitalypb/cluster.pb.go index a7c411dc66..5d5e2c82a4 100644 --- a/proto/go/gitalypb/cluster.pb.go +++ b/proto/go/gitalypb/cluster.pb.go @@ -640,6 +640,143 @@ func (*JoinClusterResponse) Descriptor() ([]byte, []int) { return file_cluster_proto_rawDescGZIP(), []int{8} } +// AddReplicaRequest is the request for the AddReplica RPC. +type AddReplicaRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // partition_key identifies the partition to add a replica to. + PartitionKey *RaftPartitionKey `protobuf:"bytes,1,opt,name=partition_key,json=partitionKey,proto3" json:"partition_key,omitempty"` + // target_storage is the name of the storage where the new replica should be created. + TargetStorage string `protobuf:"bytes,2,opt,name=target_storage,json=targetStorage,proto3" json:"target_storage,omitempty"` + // target_address is the network address of the target storage. + TargetAddress string `protobuf:"bytes,3,opt,name=target_address,json=targetAddress,proto3" json:"target_address,omitempty"` + // replica_type specifies whether to add the replica as a voter or learner. + ReplicaType ReplicaID_ReplicaType `protobuf:"varint,4,opt,name=replica_type,json=replicaType,proto3,enum=gitaly.ReplicaID_ReplicaType" json:"replica_type,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AddReplicaRequest) Reset() { + *x = AddReplicaRequest{} + mi := &file_cluster_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AddReplicaRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AddReplicaRequest) ProtoMessage() {} + +func (x *AddReplicaRequest) ProtoReflect() protoreflect.Message { + mi := &file_cluster_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AddReplicaRequest.ProtoReflect.Descriptor instead. +func (*AddReplicaRequest) Descriptor() ([]byte, []int) { + return file_cluster_proto_rawDescGZIP(), []int{9} +} + +func (x *AddReplicaRequest) GetPartitionKey() *RaftPartitionKey { + if x != nil { + return x.PartitionKey + } + return nil +} + +func (x *AddReplicaRequest) GetTargetStorage() string { + if x != nil { + return x.TargetStorage + } + return "" +} + +func (x *AddReplicaRequest) GetTargetAddress() string { + if x != nil { + return x.TargetAddress + } + return "" +} + +func (x *AddReplicaRequest) GetReplicaType() ReplicaID_ReplicaType { + if x != nil { + return x.ReplicaType + } + return ReplicaID_REPLICA_TYPE_UNSPECIFIED +} + +// AddReplicaResponse is the response for the AddReplica RPC. +type AddReplicaResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // new_replica_id contains details of the newly added replica. + NewReplicaId *ReplicaID `protobuf:"bytes,1,opt,name=new_replica_id,json=newReplicaId,proto3" json:"new_replica_id,omitempty"` + // partition_info contains the current state of the partition after the replica was added. + PartitionInfo *RaftClusterInfoResponse `protobuf:"bytes,2,opt,name=partition_info,json=partitionInfo,proto3" json:"partition_info,omitempty"` + // replica_type confirms the type of replica that was added. + ReplicaType ReplicaID_ReplicaType `protobuf:"varint,3,opt,name=replica_type,json=replicaType,proto3,enum=gitaly.ReplicaID_ReplicaType" json:"replica_type,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AddReplicaResponse) Reset() { + *x = AddReplicaResponse{} + mi := &file_cluster_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AddReplicaResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AddReplicaResponse) ProtoMessage() {} + +func (x *AddReplicaResponse) ProtoReflect() protoreflect.Message { + mi := &file_cluster_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AddReplicaResponse.ProtoReflect.Descriptor instead. +func (*AddReplicaResponse) Descriptor() ([]byte, []int) { + return file_cluster_proto_rawDescGZIP(), []int{10} +} + +func (x *AddReplicaResponse) GetNewReplicaId() *ReplicaID { + if x != nil { + return x.NewReplicaId + } + return nil +} + +func (x *AddReplicaResponse) GetPartitionInfo() *RaftClusterInfoResponse { + if x != nil { + return x.PartitionInfo + } + return nil +} + +func (x *AddReplicaResponse) GetReplicaType() ReplicaID_ReplicaType { + if x != nil { + return x.ReplicaType + } + return ReplicaID_REPLICA_TYPE_UNSPECIFIED +} + // GetPartitionsRequest is the request for the GetPartitions RPC. type GetPartitionsRequest struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -667,7 +804,7 @@ type GetPartitionsRequest struct { func (x *GetPartitionsRequest) Reset() { *x = GetPartitionsRequest{} - mi := &file_cluster_proto_msgTypes[9] + mi := &file_cluster_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -679,7 +816,7 @@ func (x *GetPartitionsRequest) String() string { func (*GetPartitionsRequest) ProtoMessage() {} func (x *GetPartitionsRequest) ProtoReflect() protoreflect.Message { - mi := &file_cluster_proto_msgTypes[9] + mi := &file_cluster_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -692,7 +829,7 @@ func (x *GetPartitionsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetPartitionsRequest.ProtoReflect.Descriptor instead. func (*GetPartitionsRequest) Descriptor() ([]byte, []int) { - return file_cluster_proto_rawDescGZIP(), []int{9} + return file_cluster_proto_rawDescGZIP(), []int{11} } func (x *GetPartitionsRequest) GetClusterId() string { @@ -762,7 +899,7 @@ type ClusterStatistics struct { func (x *ClusterStatistics) Reset() { *x = ClusterStatistics{} - mi := &file_cluster_proto_msgTypes[10] + mi := &file_cluster_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -774,7 +911,7 @@ func (x *ClusterStatistics) String() string { func (*ClusterStatistics) ProtoMessage() {} func (x *ClusterStatistics) ProtoReflect() protoreflect.Message { - mi := &file_cluster_proto_msgTypes[10] + mi := &file_cluster_proto_msgTypes[12] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -787,7 +924,7 @@ func (x *ClusterStatistics) ProtoReflect() protoreflect.Message { // Deprecated: Use ClusterStatistics.ProtoReflect.Descriptor instead. func (*ClusterStatistics) Descriptor() ([]byte, []int) { - return file_cluster_proto_rawDescGZIP(), []int{10} + return file_cluster_proto_rawDescGZIP(), []int{12} } func (x *ClusterStatistics) GetTotalPartitions() uint32 { @@ -852,7 +989,7 @@ type GetPartitionsResponse struct { func (x *GetPartitionsResponse) Reset() { *x = GetPartitionsResponse{} - mi := &file_cluster_proto_msgTypes[11] + mi := &file_cluster_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -864,7 +1001,7 @@ func (x *GetPartitionsResponse) String() string { func (*GetPartitionsResponse) ProtoMessage() {} func (x *GetPartitionsResponse) ProtoReflect() protoreflect.Message { - mi := &file_cluster_proto_msgTypes[11] + mi := &file_cluster_proto_msgTypes[13] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -877,7 +1014,7 @@ func (x *GetPartitionsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetPartitionsResponse.ProtoReflect.Descriptor instead. func (*GetPartitionsResponse) Descriptor() ([]byte, []int) { - return file_cluster_proto_rawDescGZIP(), []int{11} + return file_cluster_proto_rawDescGZIP(), []int{13} } func (x *GetPartitionsResponse) GetClusterId() string { @@ -947,7 +1084,7 @@ type RaftClusterInfoRequest struct { func (x *RaftClusterInfoRequest) Reset() { *x = RaftClusterInfoRequest{} - mi := &file_cluster_proto_msgTypes[12] + mi := &file_cluster_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -959,7 +1096,7 @@ func (x *RaftClusterInfoRequest) String() string { func (*RaftClusterInfoRequest) ProtoMessage() {} func (x *RaftClusterInfoRequest) ProtoReflect() protoreflect.Message { - mi := &file_cluster_proto_msgTypes[12] + mi := &file_cluster_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -972,7 +1109,7 @@ func (x *RaftClusterInfoRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RaftClusterInfoRequest.ProtoReflect.Descriptor instead. func (*RaftClusterInfoRequest) Descriptor() ([]byte, []int) { - return file_cluster_proto_rawDescGZIP(), []int{12} + return file_cluster_proto_rawDescGZIP(), []int{14} } func (x *RaftClusterInfoRequest) GetClusterId() string { @@ -988,14 +1125,26 @@ type RaftClusterInfoResponse struct { // cluster_id is the identifier of the Raft cluster. ClusterId string `protobuf:"bytes,1,opt,name=cluster_id,json=clusterId,proto3" json:"cluster_id,omitempty"` // statistics contains aggregated cluster statistics. - Statistics *ClusterStatistics `protobuf:"bytes,2,opt,name=statistics,proto3" json:"statistics,omitempty"` + Statistics *ClusterStatistics `protobuf:"bytes,2,opt,name=statistics,proto3" json:"statistics,omitempty"` + // partition_key identifies the partition this response describes. + PartitionKey *RaftPartitionKey `protobuf:"bytes,3,opt,name=partition_key,json=partitionKey,proto3" json:"partition_key,omitempty"` + // replicas contains information about all replicas in this partition. + Replicas []*RaftClusterInfoResponse_ReplicaStatus `protobuf:"bytes,4,rep,name=replicas,proto3" json:"replicas,omitempty"` + // leader_id is the member ID of the current leader, 0 if no leader. + LeaderId uint64 `protobuf:"varint,5,opt,name=leader_id,json=leaderId,proto3" json:"leader_id,omitempty"` + // term is the current Raft term for this partition. + Term uint64 `protobuf:"varint,6,opt,name=term,proto3" json:"term,omitempty"` + // index is the current Raft log index for this partition. + Index uint64 `protobuf:"varint,7,opt,name=index,proto3" json:"index,omitempty"` + // relative_path is the repository path for backward compatibility. + RelativePath string `protobuf:"bytes,8,opt,name=relative_path,json=relativePath,proto3" json:"relative_path,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *RaftClusterInfoResponse) Reset() { *x = RaftClusterInfoResponse{} - mi := &file_cluster_proto_msgTypes[13] + mi := &file_cluster_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1007,7 +1156,7 @@ func (x *RaftClusterInfoResponse) String() string { func (*RaftClusterInfoResponse) ProtoMessage() {} func (x *RaftClusterInfoResponse) ProtoReflect() protoreflect.Message { - mi := &file_cluster_proto_msgTypes[13] + mi := &file_cluster_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1020,7 +1169,7 @@ func (x *RaftClusterInfoResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RaftClusterInfoResponse.ProtoReflect.Descriptor instead. func (*RaftClusterInfoResponse) Descriptor() ([]byte, []int) { - return file_cluster_proto_rawDescGZIP(), []int{13} + return file_cluster_proto_rawDescGZIP(), []int{15} } func (x *RaftClusterInfoResponse) GetClusterId() string { @@ -1037,6 +1186,48 @@ func (x *RaftClusterInfoResponse) GetStatistics() *ClusterStatistics { return nil } +func (x *RaftClusterInfoResponse) GetPartitionKey() *RaftPartitionKey { + if x != nil { + return x.PartitionKey + } + return nil +} + +func (x *RaftClusterInfoResponse) GetReplicas() []*RaftClusterInfoResponse_ReplicaStatus { + if x != nil { + return x.Replicas + } + return nil +} + +func (x *RaftClusterInfoResponse) GetLeaderId() uint64 { + if x != nil { + return x.LeaderId + } + return 0 +} + +func (x *RaftClusterInfoResponse) GetTerm() uint64 { + if x != nil { + return x.Term + } + return 0 +} + +func (x *RaftClusterInfoResponse) GetIndex() uint64 { + if x != nil { + return x.Index + } + return 0 +} + +func (x *RaftClusterInfoResponse) GetRelativePath() string { + if x != nil { + return x.RelativePath + } + return "" +} + // LogData contains serialized log data, including the log entry itself // and all attached files in the log entry's directory. These data are // exchanged at the Transport layer before sending and after receiving @@ -1058,7 +1249,7 @@ type RaftEntry_LogData struct { func (x *RaftEntry_LogData) Reset() { *x = RaftEntry_LogData{} - mi := &file_cluster_proto_msgTypes[14] + mi := &file_cluster_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1070,7 +1261,7 @@ func (x *RaftEntry_LogData) String() string { func (*RaftEntry_LogData) ProtoMessage() {} func (x *RaftEntry_LogData) ProtoReflect() protoreflect.Message { - mi := &file_cluster_proto_msgTypes[14] + mi := &file_cluster_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1111,7 +1302,7 @@ type ReplicaID_Metadata struct { func (x *ReplicaID_Metadata) Reset() { *x = ReplicaID_Metadata{} - mi := &file_cluster_proto_msgTypes[15] + mi := &file_cluster_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1123,7 +1314,7 @@ func (x *ReplicaID_Metadata) String() string { func (*ReplicaID_Metadata) ProtoMessage() {} func (x *ReplicaID_Metadata) ProtoReflect() protoreflect.Message { - mi := &file_cluster_proto_msgTypes[15] + mi := &file_cluster_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1159,7 +1350,7 @@ type ClusterStatistics_StorageStats struct { func (x *ClusterStatistics_StorageStats) Reset() { *x = ClusterStatistics_StorageStats{} - mi := &file_cluster_proto_msgTypes[16] + mi := &file_cluster_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1171,7 +1362,7 @@ func (x *ClusterStatistics_StorageStats) String() string { func (*ClusterStatistics_StorageStats) ProtoMessage() {} func (x *ClusterStatistics_StorageStats) ProtoReflect() protoreflect.Message { - mi := &file_cluster_proto_msgTypes[16] + mi := &file_cluster_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1184,7 +1375,7 @@ func (x *ClusterStatistics_StorageStats) ProtoReflect() protoreflect.Message { // Deprecated: Use ClusterStatistics_StorageStats.ProtoReflect.Descriptor instead. func (*ClusterStatistics_StorageStats) Descriptor() ([]byte, []int) { - return file_cluster_proto_rawDescGZIP(), []int{10, 0} + return file_cluster_proto_rawDescGZIP(), []int{12, 0} } func (x *ClusterStatistics_StorageStats) GetLeaderCount() uint32 { @@ -1226,7 +1417,7 @@ type GetPartitionsResponse_ReplicaStatus struct { func (x *GetPartitionsResponse_ReplicaStatus) Reset() { *x = GetPartitionsResponse_ReplicaStatus{} - mi := &file_cluster_proto_msgTypes[18] + mi := &file_cluster_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1238,7 +1429,7 @@ func (x *GetPartitionsResponse_ReplicaStatus) String() string { func (*GetPartitionsResponse_ReplicaStatus) ProtoMessage() {} func (x *GetPartitionsResponse_ReplicaStatus) ProtoReflect() protoreflect.Message { - mi := &file_cluster_proto_msgTypes[18] + mi := &file_cluster_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1251,7 +1442,7 @@ func (x *GetPartitionsResponse_ReplicaStatus) ProtoReflect() protoreflect.Messag // Deprecated: Use GetPartitionsResponse_ReplicaStatus.ProtoReflect.Descriptor instead. func (*GetPartitionsResponse_ReplicaStatus) Descriptor() ([]byte, []int) { - return file_cluster_proto_rawDescGZIP(), []int{11, 0} + return file_cluster_proto_rawDescGZIP(), []int{13, 0} } func (x *GetPartitionsResponse_ReplicaStatus) GetReplicaId() *ReplicaID { @@ -1296,6 +1487,97 @@ func (x *GetPartitionsResponse_ReplicaStatus) GetState() string { return "" } +// ReplicaStatus contains status information about a single replica. +type RaftClusterInfoResponse_ReplicaStatus struct { + state protoimpl.MessageState `protogen:"open.v1"` + // replica_id uniquely identifies the replica. + ReplicaId *ReplicaID `protobuf:"bytes,1,opt,name=replica_id,json=replicaId,proto3" json:"replica_id,omitempty"` + // is_leader indicates whether this replica is currently the leader. + IsLeader bool `protobuf:"varint,2,opt,name=is_leader,json=isLeader,proto3" json:"is_leader,omitempty"` + // is_healthy indicates whether the replica is reachable and functioning. + IsHealthy bool `protobuf:"varint,3,opt,name=is_healthy,json=isHealthy,proto3" json:"is_healthy,omitempty"` + // last_index is the index of the last log entry this replica has. + LastIndex uint64 `protobuf:"varint,4,opt,name=last_index,json=lastIndex,proto3" json:"last_index,omitempty"` + // match_index is the highest log index known to be replicated on this replica. + MatchIndex uint64 `protobuf:"varint,5,opt,name=match_index,json=matchIndex,proto3" json:"match_index,omitempty"` + // state represents the Raft state of this replica (follower, candidate, leader). + State string `protobuf:"bytes,6,opt,name=state,proto3" json:"state,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RaftClusterInfoResponse_ReplicaStatus) Reset() { + *x = RaftClusterInfoResponse_ReplicaStatus{} + mi := &file_cluster_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RaftClusterInfoResponse_ReplicaStatus) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RaftClusterInfoResponse_ReplicaStatus) ProtoMessage() {} + +func (x *RaftClusterInfoResponse_ReplicaStatus) ProtoReflect() protoreflect.Message { + mi := &file_cluster_proto_msgTypes[21] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RaftClusterInfoResponse_ReplicaStatus.ProtoReflect.Descriptor instead. +func (*RaftClusterInfoResponse_ReplicaStatus) Descriptor() ([]byte, []int) { + return file_cluster_proto_rawDescGZIP(), []int{15, 0} +} + +func (x *RaftClusterInfoResponse_ReplicaStatus) GetReplicaId() *ReplicaID { + if x != nil { + return x.ReplicaId + } + return nil +} + +func (x *RaftClusterInfoResponse_ReplicaStatus) GetIsLeader() bool { + if x != nil { + return x.IsLeader + } + return false +} + +func (x *RaftClusterInfoResponse_ReplicaStatus) GetIsHealthy() bool { + if x != nil { + return x.IsHealthy + } + return false +} + +func (x *RaftClusterInfoResponse_ReplicaStatus) GetLastIndex() uint64 { + if x != nil { + return x.LastIndex + } + return 0 +} + +func (x *RaftClusterInfoResponse_ReplicaStatus) GetMatchIndex() uint64 { + if x != nil { + return x.MatchIndex + } + return 0 +} + +func (x *RaftClusterInfoResponse_ReplicaStatus) GetState() string { + if x != nil { + return x.State + } + return "" +} + var File_cluster_proto protoreflect.FileDescriptor var file_cluster_proto_rawDesc = string([]byte{ @@ -1381,133 +1663,195 @@ var file_cluster_proto_rawDesc = string([]byte{ 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x49, 0x44, 0x52, 0x08, 0x72, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x73, 0x22, 0x15, 0x0a, 0x13, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, - 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xa1, 0x02, 0x0a, 0x14, 0x47, 0x65, - 0x74, 0x50, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x5f, 0x69, 0x64, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x49, - 0x64, 0x12, 0x3d, 0x0a, 0x0d, 0x70, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6b, - 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, - 0x79, 0x2e, 0x52, 0x61, 0x66, 0x74, 0x50, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x4b, - 0x65, 0x79, 0x52, 0x0c, 0x70, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x4b, 0x65, 0x79, - 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x70, 0x61, 0x74, - 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x76, - 0x65, 0x50, 0x61, 0x74, 0x68, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, - 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x12, - 0x36, 0x0a, 0x17, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x5f, 0x72, 0x65, 0x70, 0x6c, 0x69, - 0x63, 0x61, 0x5f, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x15, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, - 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x12, 0x34, 0x0a, 0x16, 0x69, 0x6e, 0x63, 0x6c, 0x75, - 0x64, 0x65, 0x5f, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x70, 0x61, 0x74, 0x68, - 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, - 0x52, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x76, 0x65, 0x50, 0x61, 0x74, 0x68, 0x73, 0x22, 0xd2, 0x03, - 0x0a, 0x11, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x69, 0x73, 0x74, - 0x69, 0x63, 0x73, 0x12, 0x29, 0x0a, 0x10, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x70, 0x61, 0x72, - 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0f, 0x74, - 0x6f, 0x74, 0x61, 0x6c, 0x50, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x2d, - 0x0a, 0x12, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x79, 0x5f, 0x70, 0x61, 0x72, 0x74, 0x69, 0x74, - 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x11, 0x68, 0x65, 0x61, 0x6c, - 0x74, 0x68, 0x79, 0x50, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x25, 0x0a, - 0x0e, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x72, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x73, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0d, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x52, 0x65, 0x70, 0x6c, - 0x69, 0x63, 0x61, 0x73, 0x12, 0x29, 0x0a, 0x10, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x79, 0x5f, - 0x72, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0f, - 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x79, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x73, 0x12, - 0x50, 0x0a, 0x0d, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x73, - 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, - 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x69, 0x73, 0x74, 0x69, 0x63, - 0x73, 0x2e, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x45, 0x6e, - 0x74, 0x72, 0x79, 0x52, 0x0c, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x53, 0x74, 0x61, 0x74, - 0x73, 0x1a, 0x56, 0x0a, 0x0c, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x53, 0x74, 0x61, 0x74, - 0x73, 0x12, 0x21, 0x0a, 0x0c, 0x6c, 0x65, 0x61, 0x64, 0x65, 0x72, 0x5f, 0x63, 0x6f, 0x75, 0x6e, - 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0b, 0x6c, 0x65, 0x61, 0x64, 0x65, 0x72, 0x43, - 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x5f, - 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0c, 0x72, 0x65, 0x70, - 0x6c, 0x69, 0x63, 0x61, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x1a, 0x67, 0x0a, 0x11, 0x53, 0x74, 0x6f, - 0x72, 0x61, 0x67, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, - 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, - 0x12, 0x3c, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x26, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, - 0x53, 0x74, 0x61, 0x74, 0x69, 0x73, 0x74, 0x69, 0x63, 0x73, 0x2e, 0x53, 0x74, 0x6f, 0x72, 0x61, - 0x67, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, - 0x38, 0x01, 0x22, 0xa7, 0x04, 0x0a, 0x15, 0x47, 0x65, 0x74, 0x50, 0x61, 0x72, 0x74, 0x69, 0x74, - 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1d, 0x0a, 0x0a, - 0x63, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x09, 0x63, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x49, 0x64, 0x12, 0x3d, 0x0a, 0x0d, 0x70, - 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x52, 0x61, 0x66, 0x74, - 0x50, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x4b, 0x65, 0x79, 0x52, 0x0c, 0x70, 0x61, - 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x4b, 0x65, 0x79, 0x12, 0x47, 0x0a, 0x08, 0x72, 0x65, - 0x70, 0x6c, 0x69, 0x63, 0x61, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x67, - 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x47, 0x65, 0x74, 0x50, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, - 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x52, 0x65, 0x70, 0x6c, - 0x69, 0x63, 0x61, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x08, 0x72, 0x65, 0x70, 0x6c, 0x69, - 0x63, 0x61, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x6c, 0x65, 0x61, 0x64, 0x65, 0x72, 0x5f, 0x69, 0x64, - 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x08, 0x6c, 0x65, 0x61, 0x64, 0x65, 0x72, 0x49, 0x64, - 0x12, 0x12, 0x0a, 0x04, 0x74, 0x65, 0x72, 0x6d, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, - 0x74, 0x65, 0x72, 0x6d, 0x12, 0x14, 0x0a, 0x05, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x06, 0x20, - 0x01, 0x28, 0x04, 0x52, 0x05, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, - 0x6c, 0x61, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x07, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x0c, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x76, 0x65, 0x50, 0x61, 0x74, 0x68, 0x12, - 0x25, 0x0a, 0x0e, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x70, 0x61, 0x74, 0x68, - 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0d, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x76, - 0x65, 0x50, 0x61, 0x74, 0x68, 0x73, 0x1a, 0xd3, 0x01, 0x0a, 0x0d, 0x52, 0x65, 0x70, 0x6c, 0x69, - 0x63, 0x61, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x30, 0x0a, 0x0a, 0x72, 0x65, 0x70, 0x6c, - 0x69, 0x63, 0x61, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x67, - 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x49, 0x44, 0x52, - 0x09, 0x72, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x69, 0x73, - 0x5f, 0x6c, 0x65, 0x61, 0x64, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x69, - 0x73, 0x4c, 0x65, 0x61, 0x64, 0x65, 0x72, 0x12, 0x1d, 0x0a, 0x0a, 0x69, 0x73, 0x5f, 0x68, 0x65, - 0x61, 0x6c, 0x74, 0x68, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x69, 0x73, 0x48, - 0x65, 0x61, 0x6c, 0x74, 0x68, 0x79, 0x12, 0x1d, 0x0a, 0x0a, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x69, - 0x6e, 0x64, 0x65, 0x78, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x6c, 0x61, 0x73, 0x74, - 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x1f, 0x0a, 0x0b, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x5f, 0x69, - 0x6e, 0x64, 0x65, 0x78, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0a, 0x6d, 0x61, 0x74, 0x63, - 0x68, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, - 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x22, 0x37, 0x0a, 0x16, + 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xe2, 0x01, 0x0a, 0x11, 0x41, 0x64, + 0x64, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x3d, 0x0a, 0x0d, 0x70, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6b, 0x65, 0x79, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, + 0x52, 0x61, 0x66, 0x74, 0x50, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x4b, 0x65, 0x79, + 0x52, 0x0c, 0x70, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x4b, 0x65, 0x79, 0x12, 0x25, + 0x0a, 0x0e, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x53, 0x74, + 0x6f, 0x72, 0x61, 0x67, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, + 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x74, + 0x61, 0x72, 0x67, 0x65, 0x74, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x40, 0x0a, 0x0c, + 0x72, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x0e, 0x32, 0x1d, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6c, + 0x69, 0x63, 0x61, 0x49, 0x44, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x54, 0x79, 0x70, + 0x65, 0x52, 0x0b, 0x72, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x54, 0x79, 0x70, 0x65, 0x22, 0xd7, + 0x01, 0x0a, 0x12, 0x41, 0x64, 0x64, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37, 0x0a, 0x0e, 0x6e, 0x65, 0x77, 0x5f, 0x72, 0x65, 0x70, + 0x6c, 0x69, 0x63, 0x61, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, + 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x49, 0x44, + 0x52, 0x0c, 0x6e, 0x65, 0x77, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x49, 0x64, 0x12, 0x46, + 0x0a, 0x0e, 0x70, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x6e, 0x66, 0x6f, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x52, 0x61, 0x66, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6c, 0x75, 0x73, 0x74, 0x65, - 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x6c, 0x75, 0x73, - 0x74, 0x65, 0x72, 0x49, 0x64, 0x22, 0x73, 0x0a, 0x17, 0x52, 0x61, 0x66, 0x74, 0x43, 0x6c, 0x75, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x0d, 0x70, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, + 0x6f, 0x6e, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x40, 0x0a, 0x0c, 0x72, 0x65, 0x70, 0x6c, 0x69, 0x63, + 0x61, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1d, 0x2e, 0x67, + 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x49, 0x44, 0x2e, + 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x54, 0x79, 0x70, 0x65, 0x52, 0x0b, 0x72, 0x65, 0x70, + 0x6c, 0x69, 0x63, 0x61, 0x54, 0x79, 0x70, 0x65, 0x22, 0xa1, 0x02, 0x0a, 0x14, 0x47, 0x65, 0x74, + 0x50, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x49, 0x64, + 0x12, 0x3d, 0x0a, 0x0d, 0x70, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6b, 0x65, + 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, + 0x2e, 0x52, 0x61, 0x66, 0x74, 0x50, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x4b, 0x65, + 0x79, 0x52, 0x0c, 0x70, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x4b, 0x65, 0x79, 0x12, + 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x70, 0x61, 0x74, 0x68, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x76, 0x65, + 0x50, 0x61, 0x74, 0x68, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x12, 0x36, + 0x0a, 0x17, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x5f, 0x72, 0x65, 0x70, 0x6c, 0x69, 0x63, + 0x61, 0x5f, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x15, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x44, + 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x12, 0x34, 0x0a, 0x16, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, + 0x65, 0x5f, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x73, + 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x52, + 0x65, 0x6c, 0x61, 0x74, 0x69, 0x76, 0x65, 0x50, 0x61, 0x74, 0x68, 0x73, 0x22, 0xd2, 0x03, 0x0a, + 0x11, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x69, 0x73, 0x74, 0x69, + 0x63, 0x73, 0x12, 0x29, 0x0a, 0x10, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x70, 0x61, 0x72, 0x74, + 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0f, 0x74, 0x6f, + 0x74, 0x61, 0x6c, 0x50, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x2d, 0x0a, + 0x12, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x79, 0x5f, 0x70, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x11, 0x68, 0x65, 0x61, 0x6c, 0x74, + 0x68, 0x79, 0x50, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x25, 0x0a, 0x0e, + 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x72, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x73, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0d, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x52, 0x65, 0x70, 0x6c, 0x69, + 0x63, 0x61, 0x73, 0x12, 0x29, 0x0a, 0x10, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x79, 0x5f, 0x72, + 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0f, 0x68, + 0x65, 0x61, 0x6c, 0x74, 0x68, 0x79, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x73, 0x12, 0x50, + 0x0a, 0x0d, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x73, 0x18, + 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x43, + 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x69, 0x73, 0x74, 0x69, 0x63, 0x73, + 0x2e, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x45, 0x6e, 0x74, + 0x72, 0x79, 0x52, 0x0c, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, + 0x1a, 0x56, 0x0a, 0x0c, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, + 0x12, 0x21, 0x0a, 0x0c, 0x6c, 0x65, 0x61, 0x64, 0x65, 0x72, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0b, 0x6c, 0x65, 0x61, 0x64, 0x65, 0x72, 0x43, 0x6f, + 0x75, 0x6e, 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x5f, 0x63, + 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0c, 0x72, 0x65, 0x70, 0x6c, + 0x69, 0x63, 0x61, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x1a, 0x67, 0x0a, 0x11, 0x53, 0x74, 0x6f, 0x72, + 0x61, 0x67, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, + 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, + 0x3c, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, + 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x53, + 0x74, 0x61, 0x74, 0x69, 0x73, 0x74, 0x69, 0x63, 0x73, 0x2e, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, + 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, + 0x01, 0x22, 0xa7, 0x04, 0x0a, 0x15, 0x47, 0x65, 0x74, 0x50, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x63, + 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x09, 0x63, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x49, 0x64, 0x12, 0x3d, 0x0a, 0x0d, 0x70, 0x61, + 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x18, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x52, 0x61, 0x66, 0x74, 0x50, + 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x4b, 0x65, 0x79, 0x52, 0x0c, 0x70, 0x61, 0x72, + 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x4b, 0x65, 0x79, 0x12, 0x47, 0x0a, 0x08, 0x72, 0x65, 0x70, + 0x6c, 0x69, 0x63, 0x61, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x67, 0x69, + 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x47, 0x65, 0x74, 0x50, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x69, + 0x63, 0x61, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x08, 0x72, 0x65, 0x70, 0x6c, 0x69, 0x63, + 0x61, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x6c, 0x65, 0x61, 0x64, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x08, 0x6c, 0x65, 0x61, 0x64, 0x65, 0x72, 0x49, 0x64, 0x12, + 0x12, 0x0a, 0x04, 0x74, 0x65, 0x72, 0x6d, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x74, + 0x65, 0x72, 0x6d, 0x12, 0x14, 0x0a, 0x05, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x06, 0x20, 0x01, + 0x28, 0x04, 0x52, 0x05, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x6c, + 0x61, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0c, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x76, 0x65, 0x50, 0x61, 0x74, 0x68, 0x12, 0x25, + 0x0a, 0x0e, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x73, + 0x18, 0x08, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0d, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x76, 0x65, + 0x50, 0x61, 0x74, 0x68, 0x73, 0x1a, 0xd3, 0x01, 0x0a, 0x0d, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, + 0x61, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x30, 0x0a, 0x0a, 0x72, 0x65, 0x70, 0x6c, 0x69, + 0x63, 0x61, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x67, 0x69, + 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x49, 0x44, 0x52, 0x09, + 0x72, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x69, 0x73, 0x5f, + 0x6c, 0x65, 0x61, 0x64, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x69, 0x73, + 0x4c, 0x65, 0x61, 0x64, 0x65, 0x72, 0x12, 0x1d, 0x0a, 0x0a, 0x69, 0x73, 0x5f, 0x68, 0x65, 0x61, + 0x6c, 0x74, 0x68, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x69, 0x73, 0x48, 0x65, + 0x61, 0x6c, 0x74, 0x68, 0x79, 0x12, 0x1d, 0x0a, 0x0a, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x69, 0x6e, + 0x64, 0x65, 0x78, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x6c, 0x61, 0x73, 0x74, 0x49, + 0x6e, 0x64, 0x65, 0x78, 0x12, 0x1f, 0x0a, 0x0b, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x5f, 0x69, 0x6e, + 0x64, 0x65, 0x78, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0a, 0x6d, 0x61, 0x74, 0x63, 0x68, + 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x06, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x22, 0x37, 0x0a, 0x16, 0x52, + 0x61, 0x66, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, + 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x6c, 0x75, 0x73, 0x74, + 0x65, 0x72, 0x49, 0x64, 0x22, 0xbf, 0x04, 0x0a, 0x17, 0x52, 0x61, 0x66, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x49, 0x64, 0x12, 0x39, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x74, 0x69, 0x73, 0x74, 0x69, 0x63, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x69, 0x73, 0x74, 0x69, 0x63, 0x73, 0x52, 0x0a, - 0x73, 0x74, 0x61, 0x74, 0x69, 0x73, 0x74, 0x69, 0x63, 0x73, 0x32, 0xcf, 0x03, 0x0a, 0x0b, 0x52, - 0x61, 0x66, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x52, 0x0a, 0x0b, 0x53, 0x65, - 0x6e, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x1a, 0x2e, 0x67, 0x69, 0x74, 0x61, - 0x6c, 0x79, 0x2e, 0x52, 0x61, 0x66, 0x74, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x52, - 0x61, 0x66, 0x74, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x22, 0x08, 0xfa, 0x97, 0x28, 0x04, 0x08, 0x01, 0x10, 0x02, 0x28, 0x01, 0x12, 0x63, - 0x0a, 0x0c, 0x53, 0x65, 0x6e, 0x64, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x12, 0x22, + 0x73, 0x74, 0x61, 0x74, 0x69, 0x73, 0x74, 0x69, 0x63, 0x73, 0x12, 0x3d, 0x0a, 0x0d, 0x70, 0x61, + 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x18, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x52, 0x61, 0x66, 0x74, 0x50, + 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x4b, 0x65, 0x79, 0x52, 0x0c, 0x70, 0x61, 0x72, + 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x4b, 0x65, 0x79, 0x12, 0x49, 0x0a, 0x08, 0x72, 0x65, 0x70, + 0x6c, 0x69, 0x63, 0x61, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x67, 0x69, + 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x52, 0x61, 0x66, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, + 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x52, 0x65, 0x70, + 0x6c, 0x69, 0x63, 0x61, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x08, 0x72, 0x65, 0x70, 0x6c, + 0x69, 0x63, 0x61, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x6c, 0x65, 0x61, 0x64, 0x65, 0x72, 0x5f, 0x69, + 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x08, 0x6c, 0x65, 0x61, 0x64, 0x65, 0x72, 0x49, + 0x64, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x65, 0x72, 0x6d, 0x18, 0x06, 0x20, 0x01, 0x28, 0x04, 0x52, + 0x04, 0x74, 0x65, 0x72, 0x6d, 0x12, 0x14, 0x0a, 0x05, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x07, + 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x23, 0x0a, 0x0d, 0x72, + 0x65, 0x6c, 0x61, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x08, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x76, 0x65, 0x50, 0x61, 0x74, 0x68, + 0x1a, 0xd3, 0x01, 0x0a, 0x0d, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x53, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x12, 0x30, 0x0a, 0x0a, 0x72, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x5f, 0x69, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, + 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x49, 0x44, 0x52, 0x09, 0x72, 0x65, 0x70, 0x6c, 0x69, + 0x63, 0x61, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x69, 0x73, 0x5f, 0x6c, 0x65, 0x61, 0x64, 0x65, + 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x69, 0x73, 0x4c, 0x65, 0x61, 0x64, 0x65, + 0x72, 0x12, 0x1d, 0x0a, 0x0a, 0x69, 0x73, 0x5f, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x79, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x69, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x79, + 0x12, 0x1d, 0x0a, 0x0a, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x6c, 0x61, 0x73, 0x74, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, + 0x1f, 0x0a, 0x0b, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x5f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x05, + 0x20, 0x01, 0x28, 0x04, 0x52, 0x0a, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x49, 0x6e, 0x64, 0x65, 0x78, + 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x32, 0x9e, 0x04, 0x0a, 0x0b, 0x52, 0x61, 0x66, 0x74, 0x53, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x52, 0x0a, 0x0b, 0x53, 0x65, 0x6e, 0x64, 0x4d, 0x65, + 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x1a, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x52, + 0x61, 0x66, 0x74, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x1b, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x52, 0x61, 0x66, 0x74, 0x4d, + 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x08, + 0xfa, 0x97, 0x28, 0x04, 0x08, 0x01, 0x10, 0x02, 0x28, 0x01, 0x12, 0x63, 0x0a, 0x0c, 0x53, 0x65, + 0x6e, 0x64, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x12, 0x22, 0x2e, 0x67, 0x69, 0x74, + 0x61, 0x6c, 0x79, 0x2e, 0x52, 0x61, 0x66, 0x74, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, + 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x52, 0x61, 0x66, 0x74, 0x53, 0x6e, 0x61, 0x70, - 0x73, 0x68, 0x6f, 0x74, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x52, 0x61, 0x66, 0x74, - 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x08, 0xfa, 0x97, 0x28, 0x04, 0x08, 0x01, 0x10, - 0x02, 0x28, 0x01, 0x12, 0x50, 0x0a, 0x0b, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75, 0x73, 0x74, - 0x65, 0x72, 0x12, 0x1a, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x4a, 0x6f, 0x69, 0x6e, - 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, + 0x73, 0x68, 0x6f, 0x74, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0x08, 0xfa, 0x97, 0x28, 0x04, 0x08, 0x01, 0x10, 0x02, 0x28, 0x01, 0x12, + 0x50, 0x0a, 0x0b, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x12, 0x1a, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75, 0x73, - 0x74, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x08, 0xfa, 0x97, 0x28, - 0x04, 0x08, 0x01, 0x10, 0x02, 0x12, 0x58, 0x0a, 0x0d, 0x47, 0x65, 0x74, 0x50, 0x61, 0x72, 0x74, - 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x1c, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, - 0x47, 0x65, 0x74, 0x50, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x47, 0x65, - 0x74, 0x50, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x22, 0x08, 0xfa, 0x97, 0x28, 0x04, 0x08, 0x02, 0x10, 0x02, 0x30, 0x01, 0x12, - 0x5b, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x49, 0x6e, 0x66, - 0x6f, 0x12, 0x1e, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x52, 0x61, 0x66, 0x74, 0x43, - 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x1f, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x52, 0x61, 0x66, 0x74, 0x43, - 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x22, 0x08, 0xfa, 0x97, 0x28, 0x04, 0x08, 0x02, 0x10, 0x02, 0x42, 0x34, 0x5a, 0x32, - 0x67, 0x69, 0x74, 0x6c, 0x61, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x69, 0x74, 0x6c, 0x61, - 0x62, 0x2d, 0x6f, 0x72, 0x67, 0x2f, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2f, 0x76, 0x31, 0x38, - 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x67, 0x6f, 0x2f, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, - 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x67, 0x69, 0x74, + 0x61, 0x6c, 0x79, 0x2e, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x08, 0xfa, 0x97, 0x28, 0x04, 0x08, 0x01, 0x10, + 0x02, 0x12, 0x58, 0x0a, 0x0d, 0x47, 0x65, 0x74, 0x50, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x12, 0x1c, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x47, 0x65, 0x74, 0x50, + 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x1d, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x47, 0x65, 0x74, 0x50, 0x61, 0x72, + 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, + 0x08, 0xfa, 0x97, 0x28, 0x04, 0x08, 0x02, 0x10, 0x02, 0x30, 0x01, 0x12, 0x5b, 0x0a, 0x0e, 0x47, + 0x65, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1e, 0x2e, + 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x52, 0x61, 0x66, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, + 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, + 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x52, 0x61, 0x66, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, + 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x08, + 0xfa, 0x97, 0x28, 0x04, 0x08, 0x02, 0x10, 0x02, 0x12, 0x4d, 0x0a, 0x0a, 0x41, 0x64, 0x64, 0x52, + 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x12, 0x19, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, + 0x41, 0x64, 0x64, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x1a, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x41, 0x64, 0x64, 0x52, 0x65, + 0x70, 0x6c, 0x69, 0x63, 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x08, 0xfa, + 0x97, 0x28, 0x04, 0x08, 0x01, 0x10, 0x02, 0x42, 0x34, 0x5a, 0x32, 0x67, 0x69, 0x74, 0x6c, 0x61, + 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x69, 0x74, 0x6c, 0x61, 0x62, 0x2d, 0x6f, 0x72, 0x67, + 0x2f, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2f, 0x76, 0x31, 0x38, 0x2f, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x2f, 0x67, 0x6f, 0x2f, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x70, 0x62, 0x62, 0x06, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x33, }) var ( @@ -1523,62 +1867,75 @@ func file_cluster_proto_rawDescGZIP() []byte { } var file_cluster_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_cluster_proto_msgTypes = make([]protoimpl.MessageInfo, 19) +var file_cluster_proto_msgTypes = make([]protoimpl.MessageInfo, 22) var file_cluster_proto_goTypes = []any{ - (ReplicaID_ReplicaType)(0), // 0: gitaly.ReplicaID.ReplicaType - (*RaftEntry)(nil), // 1: gitaly.RaftEntry - (*RaftPartitionKey)(nil), // 2: gitaly.RaftPartitionKey - (*ReplicaID)(nil), // 3: gitaly.ReplicaID - (*RaftMessageRequest)(nil), // 4: gitaly.RaftMessageRequest - (*RaftMessageResponse)(nil), // 5: gitaly.RaftMessageResponse - (*RaftSnapshotMessageRequest)(nil), // 6: gitaly.RaftSnapshotMessageRequest - (*RaftSnapshotMessageResponse)(nil), // 7: gitaly.RaftSnapshotMessageResponse - (*JoinClusterRequest)(nil), // 8: gitaly.JoinClusterRequest - (*JoinClusterResponse)(nil), // 9: gitaly.JoinClusterResponse - (*GetPartitionsRequest)(nil), // 10: gitaly.GetPartitionsRequest - (*ClusterStatistics)(nil), // 11: gitaly.ClusterStatistics - (*GetPartitionsResponse)(nil), // 12: gitaly.GetPartitionsResponse - (*RaftClusterInfoRequest)(nil), // 13: gitaly.RaftClusterInfoRequest - (*RaftClusterInfoResponse)(nil), // 14: gitaly.RaftClusterInfoResponse - (*RaftEntry_LogData)(nil), // 15: gitaly.RaftEntry.LogData - (*ReplicaID_Metadata)(nil), // 16: gitaly.ReplicaID.Metadata - (*ClusterStatistics_StorageStats)(nil), // 17: gitaly.ClusterStatistics.StorageStats - nil, // 18: gitaly.ClusterStatistics.StorageStatsEntry - (*GetPartitionsResponse_ReplicaStatus)(nil), // 19: gitaly.GetPartitionsResponse.ReplicaStatus - (*raftpb.Message)(nil), // 20: raftpb.Message + (ReplicaID_ReplicaType)(0), // 0: gitaly.ReplicaID.ReplicaType + (*RaftEntry)(nil), // 1: gitaly.RaftEntry + (*RaftPartitionKey)(nil), // 2: gitaly.RaftPartitionKey + (*ReplicaID)(nil), // 3: gitaly.ReplicaID + (*RaftMessageRequest)(nil), // 4: gitaly.RaftMessageRequest + (*RaftMessageResponse)(nil), // 5: gitaly.RaftMessageResponse + (*RaftSnapshotMessageRequest)(nil), // 6: gitaly.RaftSnapshotMessageRequest + (*RaftSnapshotMessageResponse)(nil), // 7: gitaly.RaftSnapshotMessageResponse + (*JoinClusterRequest)(nil), // 8: gitaly.JoinClusterRequest + (*JoinClusterResponse)(nil), // 9: gitaly.JoinClusterResponse + (*AddReplicaRequest)(nil), // 10: gitaly.AddReplicaRequest + (*AddReplicaResponse)(nil), // 11: gitaly.AddReplicaResponse + (*GetPartitionsRequest)(nil), // 12: gitaly.GetPartitionsRequest + (*ClusterStatistics)(nil), // 13: gitaly.ClusterStatistics + (*GetPartitionsResponse)(nil), // 14: gitaly.GetPartitionsResponse + (*RaftClusterInfoRequest)(nil), // 15: gitaly.RaftClusterInfoRequest + (*RaftClusterInfoResponse)(nil), // 16: gitaly.RaftClusterInfoResponse + (*RaftEntry_LogData)(nil), // 17: gitaly.RaftEntry.LogData + (*ReplicaID_Metadata)(nil), // 18: gitaly.ReplicaID.Metadata + (*ClusterStatistics_StorageStats)(nil), // 19: gitaly.ClusterStatistics.StorageStats + nil, // 20: gitaly.ClusterStatistics.StorageStatsEntry + (*GetPartitionsResponse_ReplicaStatus)(nil), // 21: gitaly.GetPartitionsResponse.ReplicaStatus + (*RaftClusterInfoResponse_ReplicaStatus)(nil), // 22: gitaly.RaftClusterInfoResponse.ReplicaStatus + (*raftpb.Message)(nil), // 23: raftpb.Message } var file_cluster_proto_depIdxs = []int32{ - 15, // 0: gitaly.RaftEntry.data:type_name -> gitaly.RaftEntry.LogData + 17, // 0: gitaly.RaftEntry.data:type_name -> gitaly.RaftEntry.LogData 2, // 1: gitaly.ReplicaID.partition_key:type_name -> gitaly.RaftPartitionKey - 16, // 2: gitaly.ReplicaID.metadata:type_name -> gitaly.ReplicaID.Metadata + 18, // 2: gitaly.ReplicaID.metadata:type_name -> gitaly.ReplicaID.Metadata 0, // 3: gitaly.ReplicaID.type:type_name -> gitaly.ReplicaID.ReplicaType 3, // 4: gitaly.RaftMessageRequest.replica_id:type_name -> gitaly.ReplicaID - 20, // 5: gitaly.RaftMessageRequest.message:type_name -> raftpb.Message + 23, // 5: gitaly.RaftMessageRequest.message:type_name -> raftpb.Message 4, // 6: gitaly.RaftSnapshotMessageRequest.raft_msg:type_name -> gitaly.RaftMessageRequest 2, // 7: gitaly.JoinClusterRequest.partition_key:type_name -> gitaly.RaftPartitionKey 3, // 8: gitaly.JoinClusterRequest.replicas:type_name -> gitaly.ReplicaID - 2, // 9: gitaly.GetPartitionsRequest.partition_key:type_name -> gitaly.RaftPartitionKey - 18, // 10: gitaly.ClusterStatistics.storage_stats:type_name -> gitaly.ClusterStatistics.StorageStatsEntry - 2, // 11: gitaly.GetPartitionsResponse.partition_key:type_name -> gitaly.RaftPartitionKey - 19, // 12: gitaly.GetPartitionsResponse.replicas:type_name -> gitaly.GetPartitionsResponse.ReplicaStatus - 11, // 13: gitaly.RaftClusterInfoResponse.statistics:type_name -> gitaly.ClusterStatistics - 17, // 14: gitaly.ClusterStatistics.StorageStatsEntry.value:type_name -> gitaly.ClusterStatistics.StorageStats - 3, // 15: gitaly.GetPartitionsResponse.ReplicaStatus.replica_id:type_name -> gitaly.ReplicaID - 4, // 16: gitaly.RaftService.SendMessage:input_type -> gitaly.RaftMessageRequest - 6, // 17: gitaly.RaftService.SendSnapshot:input_type -> gitaly.RaftSnapshotMessageRequest - 8, // 18: gitaly.RaftService.JoinCluster:input_type -> gitaly.JoinClusterRequest - 10, // 19: gitaly.RaftService.GetPartitions:input_type -> gitaly.GetPartitionsRequest - 13, // 20: gitaly.RaftService.GetClusterInfo:input_type -> gitaly.RaftClusterInfoRequest - 5, // 21: gitaly.RaftService.SendMessage:output_type -> gitaly.RaftMessageResponse - 7, // 22: gitaly.RaftService.SendSnapshot:output_type -> gitaly.RaftSnapshotMessageResponse - 9, // 23: gitaly.RaftService.JoinCluster:output_type -> gitaly.JoinClusterResponse - 12, // 24: gitaly.RaftService.GetPartitions:output_type -> gitaly.GetPartitionsResponse - 14, // 25: gitaly.RaftService.GetClusterInfo:output_type -> gitaly.RaftClusterInfoResponse - 21, // [21:26] is the sub-list for method output_type - 16, // [16:21] is the sub-list for method input_type - 16, // [16:16] is the sub-list for extension type_name - 16, // [16:16] is the sub-list for extension extendee - 0, // [0:16] is the sub-list for field type_name + 2, // 9: gitaly.AddReplicaRequest.partition_key:type_name -> gitaly.RaftPartitionKey + 0, // 10: gitaly.AddReplicaRequest.replica_type:type_name -> gitaly.ReplicaID.ReplicaType + 3, // 11: gitaly.AddReplicaResponse.new_replica_id:type_name -> gitaly.ReplicaID + 16, // 12: gitaly.AddReplicaResponse.partition_info:type_name -> gitaly.RaftClusterInfoResponse + 0, // 13: gitaly.AddReplicaResponse.replica_type:type_name -> gitaly.ReplicaID.ReplicaType + 2, // 14: gitaly.GetPartitionsRequest.partition_key:type_name -> gitaly.RaftPartitionKey + 20, // 15: gitaly.ClusterStatistics.storage_stats:type_name -> gitaly.ClusterStatistics.StorageStatsEntry + 2, // 16: gitaly.GetPartitionsResponse.partition_key:type_name -> gitaly.RaftPartitionKey + 21, // 17: gitaly.GetPartitionsResponse.replicas:type_name -> gitaly.GetPartitionsResponse.ReplicaStatus + 13, // 18: gitaly.RaftClusterInfoResponse.statistics:type_name -> gitaly.ClusterStatistics + 2, // 19: gitaly.RaftClusterInfoResponse.partition_key:type_name -> gitaly.RaftPartitionKey + 22, // 20: gitaly.RaftClusterInfoResponse.replicas:type_name -> gitaly.RaftClusterInfoResponse.ReplicaStatus + 19, // 21: gitaly.ClusterStatistics.StorageStatsEntry.value:type_name -> gitaly.ClusterStatistics.StorageStats + 3, // 22: gitaly.GetPartitionsResponse.ReplicaStatus.replica_id:type_name -> gitaly.ReplicaID + 3, // 23: gitaly.RaftClusterInfoResponse.ReplicaStatus.replica_id:type_name -> gitaly.ReplicaID + 4, // 24: gitaly.RaftService.SendMessage:input_type -> gitaly.RaftMessageRequest + 6, // 25: gitaly.RaftService.SendSnapshot:input_type -> gitaly.RaftSnapshotMessageRequest + 8, // 26: gitaly.RaftService.JoinCluster:input_type -> gitaly.JoinClusterRequest + 12, // 27: gitaly.RaftService.GetPartitions:input_type -> gitaly.GetPartitionsRequest + 15, // 28: gitaly.RaftService.GetClusterInfo:input_type -> gitaly.RaftClusterInfoRequest + 10, // 29: gitaly.RaftService.AddReplica:input_type -> gitaly.AddReplicaRequest + 5, // 30: gitaly.RaftService.SendMessage:output_type -> gitaly.RaftMessageResponse + 7, // 31: gitaly.RaftService.SendSnapshot:output_type -> gitaly.RaftSnapshotMessageResponse + 9, // 32: gitaly.RaftService.JoinCluster:output_type -> gitaly.JoinClusterResponse + 14, // 33: gitaly.RaftService.GetPartitions:output_type -> gitaly.GetPartitionsResponse + 16, // 34: gitaly.RaftService.GetClusterInfo:output_type -> gitaly.RaftClusterInfoResponse + 11, // 35: gitaly.RaftService.AddReplica:output_type -> gitaly.AddReplicaResponse + 30, // [30:36] is the sub-list for method output_type + 24, // [24:30] is the sub-list for method input_type + 24, // [24:24] is the sub-list for extension type_name + 24, // [24:24] is the sub-list for extension extendee + 0, // [0:24] is the sub-list for field type_name } func init() { file_cluster_proto_init() } @@ -1597,7 +1954,7 @@ func file_cluster_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_cluster_proto_rawDesc), len(file_cluster_proto_rawDesc)), NumEnums: 1, - NumMessages: 19, + NumMessages: 22, NumExtensions: 0, NumServices: 1, }, diff --git a/proto/go/gitalypb/cluster_grpc.pb.go b/proto/go/gitalypb/cluster_grpc.pb.go index fa31ee93c2..428ec3bb81 100644 --- a/proto/go/gitalypb/cluster_grpc.pb.go +++ b/proto/go/gitalypb/cluster_grpc.pb.go @@ -24,6 +24,7 @@ const ( RaftService_JoinCluster_FullMethodName = "/gitaly.RaftService/JoinCluster" RaftService_GetPartitions_FullMethodName = "/gitaly.RaftService/GetPartitions" RaftService_GetClusterInfo_FullMethodName = "/gitaly.RaftService/GetClusterInfo" + RaftService_AddReplica_FullMethodName = "/gitaly.RaftService/AddReplica" ) // RaftServiceClient is the client API for RaftService service. @@ -47,6 +48,10 @@ type RaftServiceClient interface { // GetClusterInfo retrieves cluster-wide statistics and overview information. // This is a unary RPC that returns only aggregated statistics, not partition details. GetClusterInfo(ctx context.Context, in *RaftClusterInfoRequest, opts ...grpc.CallOption) (*RaftClusterInfoResponse, error) + // AddReplica adds a new replica to an existing Raft partition. This is an operator-facing + // RPC that safely coordinates membership changes, handling config changes, remote replica + // initialization, and cleanup on failure. The operation can only be performed by the partition leader. + AddReplica(ctx context.Context, in *AddReplicaRequest, opts ...grpc.CallOption) (*AddReplicaResponse, error) } type raftServiceClient struct { @@ -122,6 +127,16 @@ func (c *raftServiceClient) GetClusterInfo(ctx context.Context, in *RaftClusterI return out, nil } +func (c *raftServiceClient) AddReplica(ctx context.Context, in *AddReplicaRequest, opts ...grpc.CallOption) (*AddReplicaResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(AddReplicaResponse) + err := c.cc.Invoke(ctx, RaftService_AddReplica_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + // RaftServiceServer is the server API for RaftService service. // All implementations must embed UnimplementedRaftServiceServer // for forward compatibility. @@ -143,6 +158,10 @@ type RaftServiceServer interface { // GetClusterInfo retrieves cluster-wide statistics and overview information. // This is a unary RPC that returns only aggregated statistics, not partition details. GetClusterInfo(context.Context, *RaftClusterInfoRequest) (*RaftClusterInfoResponse, error) + // AddReplica adds a new replica to an existing Raft partition. This is an operator-facing + // RPC that safely coordinates membership changes, handling config changes, remote replica + // initialization, and cleanup on failure. The operation can only be performed by the partition leader. + AddReplica(context.Context, *AddReplicaRequest) (*AddReplicaResponse, error) mustEmbedUnimplementedRaftServiceServer() } @@ -168,6 +187,9 @@ func (UnimplementedRaftServiceServer) GetPartitions(*GetPartitionsRequest, grpc. func (UnimplementedRaftServiceServer) GetClusterInfo(context.Context, *RaftClusterInfoRequest) (*RaftClusterInfoResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method GetClusterInfo not implemented") } +func (UnimplementedRaftServiceServer) AddReplica(context.Context, *AddReplicaRequest) (*AddReplicaResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method AddReplica not implemented") +} func (UnimplementedRaftServiceServer) mustEmbedUnimplementedRaftServiceServer() {} func (UnimplementedRaftServiceServer) testEmbeddedByValue() {} @@ -250,6 +272,24 @@ func _RaftService_GetClusterInfo_Handler(srv interface{}, ctx context.Context, d return interceptor(ctx, in, info, handler) } +func _RaftService_AddReplica_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(AddReplicaRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RaftServiceServer).AddReplica(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: RaftService_AddReplica_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RaftServiceServer).AddReplica(ctx, req.(*AddReplicaRequest)) + } + return interceptor(ctx, in, info, handler) +} + // RaftService_ServiceDesc is the grpc.ServiceDesc for RaftService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -265,6 +305,10 @@ var RaftService_ServiceDesc = grpc.ServiceDesc{ MethodName: "GetClusterInfo", Handler: _RaftService_GetClusterInfo_Handler, }, + { + MethodName: "AddReplica", + Handler: _RaftService_AddReplica_Handler, + }, }, Streams: []grpc.StreamDesc{ { -- GitLab From 385beb0944fbab9a84228831510ec12711a7d06c Mon Sep 17 00:00:00 2001 From: Quang-Minh Nguyen Date: Fri, 10 Oct 2025 16:22:45 +0700 Subject: [PATCH 2/4] raft: Add AddReplica RPC for operator-driven membership changes Operators currently lack a safe way to add replicas to existing Raft partitions. The only option is using internal AddNode() and AddLearner() methods that require understanding Raft internals, managing leadership states, and coordinating routing tables. This forces operators to rely on manual intervention during cluster scaling or failure recovery, increasing operational risk and complexity. This commit introduces a high-level RPC that abstracts the complexity of membership changes. The RPC enforces leader-only operations to maintain Raft safety guarantees, scans all configured storages to locate partitions without requiring operators to specify source storage, and automatically handles rollback if the remote JoinCluster call fails. The response includes full partition state to enable operators to verify topology changes without additional calls. All errors carry structured metadata (partition key, storage names, member IDs) for debugging. Part of: https://gitlab.com/gitlab-org/gitaly/-/issues/6908 --- internal/gitaly/service/raft/add_replica.go | 278 ++++++++++++++ .../gitaly/service/raft/add_replica_test.go | 352 ++++++++++++++++++ internal/gitaly/service/raft/testhelper.go | 19 + 3 files changed, 649 insertions(+) create mode 100644 internal/gitaly/service/raft/add_replica.go create mode 100644 internal/gitaly/service/raft/add_replica_test.go diff --git a/internal/gitaly/service/raft/add_replica.go b/internal/gitaly/service/raft/add_replica.go new file mode 100644 index 0000000000..b21a878f30 --- /dev/null +++ b/internal/gitaly/service/raft/add_replica.go @@ -0,0 +1,278 @@ +package raft + +import ( + "context" + "fmt" + + "gitlab.com/gitlab-org/gitaly/v18/internal/gitaly/storage/raftmgr" + "gitlab.com/gitlab-org/gitaly/v18/internal/structerr" + "gitlab.com/gitlab-org/gitaly/v18/proto/go/gitalypb" +) + +// AddReplica adds a new replica to an existing Raft partition. This is an operator-facing +// RPC that safely coordinates membership changes, handling config changes, remote replica +// initialization, and cleanup on failure. The operation can only be performed by the partition leader. +func (s *Server) AddReplica(ctx context.Context, req *gitalypb.AddReplicaRequest) (*gitalypb.AddReplicaResponse, error) { + if err := validateAddReplicaRequest(req); err != nil { + return nil, structerr.NewInvalidArgument("invalid request: %w", err) + } + + storage, replica, err := s.findPartitionLeader(req.GetPartitionKey()) + if err != nil { + return nil, err + } + + if err := s.validateLeaderStatus(replica); err != nil { + return nil, err + } + + if _, err := s.validateTargetStorage(req.GetTargetStorage()); err != nil { + return nil, err + } + + routingTable := storage.GetRoutingTable() + if err := checkMembershipConflict(req.GetPartitionKey(), req.GetTargetStorage(), routingTable); err != nil { + return nil, err + } + + if err := s.executeMembershipChange(ctx, replica, req); err != nil { + return nil, structerr.NewInternal("membership change failed: %w", err). + WithMetadata("partition_key", req.GetPartitionKey().GetValue()). + WithMetadata("target_storage", req.GetTargetStorage()) + } + + return s.buildAddReplicaResponse( + req.GetPartitionKey(), + req.GetTargetStorage(), + req.GetReplicaType(), + routingTable, + storage.GetReplicaRegistry(), + ) +} + +// validateAddReplicaRequest validates that all required fields are present and valid. +func validateAddReplicaRequest(req *gitalypb.AddReplicaRequest) error { + if req == nil { + return fmt.Errorf("request is nil") + } + + if req.GetPartitionKey() == nil { + return fmt.Errorf("partition_key is required") + } + + if req.GetPartitionKey().GetValue() == "" { + return fmt.Errorf("partition_key value is empty") + } + + if req.GetTargetStorage() == "" { + return fmt.Errorf("target_storage is required") + } + + if req.GetTargetAddress() == "" { + return fmt.Errorf("target_address is required") + } + + switch req.GetReplicaType() { + case gitalypb.ReplicaID_REPLICA_TYPE_VOTER, gitalypb.ReplicaID_REPLICA_TYPE_LEARNER: + return nil + case gitalypb.ReplicaID_REPLICA_TYPE_UNSPECIFIED: + return fmt.Errorf("replica_type must be specified (VOTER or LEARNER)") + default: + return fmt.Errorf("invalid replica_type: %v", req.GetReplicaType()) + } +} + +// findPartitionLeader finds the storage and replica for a partition by scanning all configured storages. +func (s *Server) findPartitionLeader(partitionKey *gitalypb.RaftPartitionKey) (*raftmgr.RaftEnabledStorage, raftmgr.RaftReplica, error) { + node, ok := s.node.(*raftmgr.Node) + if !ok { + return nil, nil, structerr.NewInternal("node is not Raft-enabled") + } + + for _, storageCfg := range s.cfg.Storages { + storageInterface, err := node.GetStorage(storageCfg.Name) + if err != nil { + continue + } + + raftStorage, ok := storageInterface.(*raftmgr.RaftEnabledStorage) + if !ok { + continue + } + + replicaRegistry := raftStorage.GetReplicaRegistry() + replica, err := replicaRegistry.GetReplica(partitionKey) + if err != nil || replica == nil { + continue + } + + return raftStorage, replica, nil + } + + return nil, nil, structerr.NewNotFound("partition %s not found on any storage", partitionKey.GetValue()). + WithMetadata("partition_key", partitionKey.GetValue()) +} + +// validateLeaderStatus checks if the replica is the current partition leader. +func (s *Server) validateLeaderStatus(replica raftmgr.RaftReplica) error { + if replica == nil { + return structerr.NewInternal("replica is nil") + } + + concreteReplica, ok := replica.(*raftmgr.Replica) + if !ok { + return structerr.NewInternal("replica is not a concrete Replica type") + } + + leaderID, err := concreteReplica.GetLeaderID() + if err != nil { + return structerr.NewInternal("failed to get leader ID: %w", err) + } + + memberID, err := concreteReplica.GetMemberID() + if err != nil { + return structerr.NewInternal("failed to get member ID: %w", err) + } + + if leaderID != memberID { + return structerr.NewFailedPrecondition( + "not the partition leader (leader is member %d, this is member %d)", + leaderID, + memberID, + ).WithMetadata("leader_id", fmt.Sprintf("%d", leaderID)). + WithMetadata("member_id", fmt.Sprintf("%d", memberID)) + } + + return nil +} + +// validateTargetStorage ensures the target storage exists and is Raft-enabled. +func (s *Server) validateTargetStorage(storageName string) (*raftmgr.RaftEnabledStorage, error) { + node, ok := s.node.(*raftmgr.Node) + if !ok { + return nil, structerr.NewInternal("node is not Raft-enabled") + } + + storageInterface, err := node.GetStorage(storageName) + if err != nil { + return nil, structerr.NewNotFound("target storage %s not found", storageName). + WithMetadata("storage_name", storageName) + } + + raftStorage, ok := storageInterface.(*raftmgr.RaftEnabledStorage) + if !ok { + return nil, structerr.NewFailedPrecondition("target storage %s is not Raft-enabled", storageName) + } + + return raftStorage, nil +} + +// checkMembershipConflict verifies that the target storage is not already a member of the partition. +func checkMembershipConflict(partitionKey *gitalypb.RaftPartitionKey, targetStorage string, routingTable raftmgr.RoutingTable) error { + entry, err := routingTable.GetEntry(partitionKey) + if err != nil { + // Partition not in routing table yet - this shouldn't happen since we found the replica, + // but we'll allow it to proceed + return nil + } + + for _, replica := range entry.Replicas { + if replica.GetStorageName() == targetStorage { + return structerr.NewAlreadyExists( + "storage %s already has a replica (member ID %d) in partition %s", + targetStorage, + replica.GetMemberId(), + partitionKey.GetValue(), + ) + } + } + + return nil +} + +// executeMembershipChange performs the actual membership change by calling the appropriate +// low-level method (AddNode or AddLearner) based on the requested replica type. +func (s *Server) executeMembershipChange(ctx context.Context, replica raftmgr.RaftReplica, req *gitalypb.AddReplicaRequest) error { + switch req.GetReplicaType() { + case gitalypb.ReplicaID_REPLICA_TYPE_VOTER: + return replica.AddNode(ctx, req.GetTargetAddress(), req.GetTargetStorage()) + case gitalypb.ReplicaID_REPLICA_TYPE_LEARNER: + return replica.AddLearner(ctx, req.GetTargetAddress(), req.GetTargetStorage()) + default: + return structerr.NewInvalidArgument("invalid replica_type: %v", req.GetReplicaType()) + } +} + +// buildAddReplicaResponse constructs the response from the current cluster state. +func (s *Server) buildAddReplicaResponse( + partitionKey *gitalypb.RaftPartitionKey, + targetStorage string, + replicaType gitalypb.ReplicaID_ReplicaType, + routingTable raftmgr.RoutingTable, + replicaRegistry raftmgr.ReplicaRegistry, +) (*gitalypb.AddReplicaResponse, error) { + entry, err := routingTable.GetEntry(partitionKey) + if err != nil { + return nil, structerr.NewInternal("failed to get routing table entry after adding replica: %w", err) + } + + newReplica := findReplicaByStorage(entry.Replicas, targetStorage) + if newReplica == nil { + return nil, structerr.NewInternal( + "newly added replica for storage %s not found in routing table", + targetStorage, + ) + } + + partitionInfo := &gitalypb.RaftClusterInfoResponse{ + ClusterId: s.cfg.Raft.ClusterID, + PartitionKey: partitionKey, + LeaderId: entry.LeaderID, + Term: entry.Term, + Index: entry.Index, + RelativePath: entry.RelativePath, + } + + if replica, err := replicaRegistry.GetReplica(partitionKey); err == nil && replica != nil { + state := replica.GetCurrentState() + partitionInfo.Term = state.Term + partitionInfo.Index = state.Index + } + + partitionInfo.Replicas = make([]*gitalypb.RaftClusterInfoResponse_ReplicaStatus, 0, len(entry.Replicas)) + for _, replicaID := range entry.Replicas { + status := &gitalypb.RaftClusterInfoResponse_ReplicaStatus{ + ReplicaId: replicaID, + IsLeader: replicaID.GetMemberId() == entry.LeaderID, + IsHealthy: replicaID.GetMetadata() != nil && replicaID.GetMetadata().GetAddress() != "", + State: determineReplicaState(replicaID, entry.LeaderID), + LastIndex: entry.Index, + MatchIndex: entry.Index, + } + partitionInfo.Replicas = append(partitionInfo.Replicas, status) + } + + return &gitalypb.AddReplicaResponse{ + NewReplicaId: newReplica, + PartitionInfo: partitionInfo, + ReplicaType: replicaType, + }, nil +} + +// findReplicaByStorage locates a replica in the routing table by storage name. +func findReplicaByStorage(replicas []*gitalypb.ReplicaID, storageName string) *gitalypb.ReplicaID { + for _, replica := range replicas { + if replica.GetStorageName() == storageName { + return replica + } + } + return nil +} + +// determineReplicaState determines the Raft state string for a replica based on routing table info. +func determineReplicaState(replicaID *gitalypb.ReplicaID, leaderID uint64) string { + if replicaID.GetMemberId() == leaderID { + return "leader" + } + return "follower" +} diff --git a/internal/gitaly/service/raft/add_replica_test.go b/internal/gitaly/service/raft/add_replica_test.go new file mode 100644 index 0000000000..f002f7ba58 --- /dev/null +++ b/internal/gitaly/service/raft/add_replica_test.go @@ -0,0 +1,352 @@ +package raft + +import ( + "context" + "fmt" + "net" + "slices" + "testing" + "time" + + "github.com/stretchr/testify/require" + "gitlab.com/gitlab-org/gitaly/v18/internal/git/gittest" + "gitlab.com/gitlab-org/gitaly/v18/internal/gitaly/config" + "gitlab.com/gitlab-org/gitaly/v18/internal/gitaly/service" + "gitlab.com/gitlab-org/gitaly/v18/internal/gitaly/storage" + "gitlab.com/gitlab-org/gitaly/v18/internal/gitaly/storage/raftmgr" + partitionlog "gitlab.com/gitlab-org/gitaly/v18/internal/gitaly/storage/storagemgr/partition/log" + "gitlab.com/gitlab-org/gitaly/v18/internal/grpc/client" + "gitlab.com/gitlab-org/gitaly/v18/internal/testhelper" + "gitlab.com/gitlab-org/gitaly/v18/internal/testhelper/testcfg" + "gitlab.com/gitlab-org/gitaly/v18/proto/go/gitalypb" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" +) + +func TestAddReplica_Success(t *testing.T) { + t.Parallel() + + testCases := []struct { + desc string + replicaType gitalypb.ReplicaID_ReplicaType + }{ + { + desc: "add voter replica", + replicaType: gitalypb.ReplicaID_REPLICA_TYPE_VOTER, + }, + { + desc: "add learner replica", + replicaType: gitalypb.ReplicaID_REPLICA_TYPE_LEARNER, + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + ctx := testhelper.Context(t) + + replicaOne, partitionKey, cfgOne, err := createAndInitializeReplica(t, ctx, storageOne, 1) + require.NoError(t, err) + t.Cleanup(func() { + err := replicaOne.Close() + require.NoError(t, err) + }) + + require.Eventually(t, func() bool { + return replicaOne.AppendedLSN() > 1 + }, 10*time.Second, 5*time.Millisecond) + + nodeTwo, cfgTwo, err := createRaftNodeWithStorage(t, storageTwo) + require.NoError(t, err) + + conn := dialService(t, ctx, cfgOne) + client := gitalypb.NewRaftServiceClient(conn) + + resp, err := client.AddReplica(ctx, &gitalypb.AddReplicaRequest{ + PartitionKey: partitionKey, + TargetStorage: storageTwo, + TargetAddress: cfgTwo.SocketPath, + ReplicaType: tc.replicaType, + }) + + require.NoError(t, err) + require.NotNil(t, resp) + + require.NotNil(t, resp.GetNewReplicaId()) + require.Equal(t, storageTwo, resp.GetNewReplicaId().GetStorageName()) + require.Equal(t, tc.replicaType, resp.GetNewReplicaId().GetType()) + require.Equal(t, partitionKey.GetValue(), resp.GetNewReplicaId().GetPartitionKey().GetValue()) + + require.Equal(t, tc.replicaType, resp.GetReplicaType()) + + require.NotNil(t, resp.GetPartitionInfo()) + require.Len(t, resp.GetPartitionInfo().GetReplicas(), 2) + + storage2, err := nodeTwo.GetStorage(storageTwo) + require.NoError(t, err) + + raftStorage2 := storage2.(*raftmgr.RaftEnabledStorage) + routingTable2 := raftStorage2.GetRoutingTable() + + // Wait for routing table to be updated on the target node + require.Eventually(t, func() bool { + entry, err := routingTable2.GetEntry(partitionKey) + if err != nil { + return false + } + if len(entry.Replicas) != 2 { + return false + } + found := slices.ContainsFunc(entry.Replicas, func(replica *gitalypb.ReplicaID) bool { + return replica.GetStorageName() == storageTwo && + replica.GetType() == tc.replicaType + }) + return found + }, 10*time.Second, 100*time.Millisecond, "replica should be added to routing table on target node") + }) + } +} + +func TestAddReplica_ValidationErrors(t *testing.T) { + t.Parallel() + + ctx := testhelper.Context(t) + partitionKey := raftmgr.NewPartitionKey(storageOne, 1) + + _, cfg, err := createRaftNodeWithStorage(t, storageOne) + require.NoError(t, err) + + conn := gittest.DialService(t, ctx, cfg) + client := gitalypb.NewRaftServiceClient(conn) + + testCases := []struct { + desc string + req *gitalypb.AddReplicaRequest + expectedCode codes.Code + expectedError string + }{ + { + desc: "missing partition key", + req: &gitalypb.AddReplicaRequest{ + TargetStorage: storageTwo, + TargetAddress: "address", + ReplicaType: gitalypb.ReplicaID_REPLICA_TYPE_VOTER, + }, + expectedCode: codes.InvalidArgument, + expectedError: "partition_key is required", + }, + { + desc: "empty partition key value", + req: &gitalypb.AddReplicaRequest{ + PartitionKey: &gitalypb.RaftPartitionKey{Value: ""}, + TargetStorage: storageTwo, + TargetAddress: "address", + ReplicaType: gitalypb.ReplicaID_REPLICA_TYPE_VOTER, + }, + expectedCode: codes.InvalidArgument, + expectedError: "partition_key value is empty", + }, + { + desc: "missing target storage", + req: &gitalypb.AddReplicaRequest{ + PartitionKey: partitionKey, + TargetAddress: "address", + ReplicaType: gitalypb.ReplicaID_REPLICA_TYPE_VOTER, + }, + expectedCode: codes.InvalidArgument, + expectedError: "target_storage is required", + }, + { + desc: "missing target address", + req: &gitalypb.AddReplicaRequest{ + PartitionKey: partitionKey, + TargetStorage: storageTwo, + ReplicaType: gitalypb.ReplicaID_REPLICA_TYPE_VOTER, + }, + expectedCode: codes.InvalidArgument, + expectedError: "target_address is required", + }, + { + desc: "unspecified replica type", + req: &gitalypb.AddReplicaRequest{ + PartitionKey: partitionKey, + TargetStorage: storageTwo, + TargetAddress: "address", + ReplicaType: gitalypb.ReplicaID_REPLICA_TYPE_UNSPECIFIED, + }, + expectedCode: codes.InvalidArgument, + expectedError: "replica_type must be specified", + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + resp, err := client.AddReplica(ctx, tc.req) + + require.Nil(t, resp) + testhelper.RequireGrpcCode(t, err, tc.expectedCode) + require.Contains(t, err.Error(), tc.expectedError) + }) + } +} + +func TestAddReplica_PartitionNotFound(t *testing.T) { + t.Parallel() + + ctx := testhelper.Context(t) + + _, cfg, err := createRaftNodeWithStorage(t, storageOne) + require.NoError(t, err) + + conn := gittest.DialService(t, ctx, cfg) + client := gitalypb.NewRaftServiceClient(conn) + + nonExistentKey := &gitalypb.RaftPartitionKey{Value: "nonexistent"} + + resp, err := client.AddReplica(ctx, &gitalypb.AddReplicaRequest{ + PartitionKey: nonExistentKey, + TargetStorage: storageTwo, + TargetAddress: "address", + ReplicaType: gitalypb.ReplicaID_REPLICA_TYPE_VOTER, + }) + + require.Nil(t, resp) + testhelper.RequireGrpcCode(t, err, codes.NotFound) +} + +func TestAddReplica_TargetStorageNotFound(t *testing.T) { + t.Parallel() + + ctx := testhelper.Context(t) + + replicaOne, partitionKey, cfgOne, err := createAndInitializeReplica(t, ctx, storageOne, 1) + require.NoError(t, err) + t.Cleanup(func() { + err := replicaOne.Close() + require.NoError(t, err) + }) + + require.Eventually(t, func() bool { + return replicaOne.AppendedLSN() > 1 + }, 10*time.Second, 5*time.Millisecond) + + conn := dialService(t, ctx, cfgOne) + client := gitalypb.NewRaftServiceClient(conn) + + resp, err := client.AddReplica(ctx, &gitalypb.AddReplicaRequest{ + PartitionKey: partitionKey, + TargetStorage: "nonexistent-storage", + TargetAddress: "address", + ReplicaType: gitalypb.ReplicaID_REPLICA_TYPE_VOTER, + }) + + require.Nil(t, resp) + testhelper.RequireGrpcCode(t, err, codes.NotFound) +} + +func TestAddReplica_MemberAlreadyExists(t *testing.T) { + t.Parallel() + + ctx := testhelper.Context(t) + + replicaOne, partitionKey, cfgOne, err := createAndInitializeReplica(t, ctx, storageOne, 1) + require.NoError(t, err) + t.Cleanup(func() { + err := replicaOne.Close() + require.NoError(t, err) + }) + + require.Eventually(t, func() bool { + return replicaOne.AppendedLSN() > 1 + }, 10*time.Second, 5*time.Millisecond) + + _, cfgTwo, err := createRaftNodeWithStorage(t, storageTwo) + require.NoError(t, err) + + conn := dialService(t, ctx, cfgOne) + client := gitalypb.NewRaftServiceClient(conn) + + resp, err := client.AddReplica(ctx, &gitalypb.AddReplicaRequest{ + PartitionKey: partitionKey, + TargetStorage: storageTwo, + TargetAddress: cfgTwo.SocketPath, + ReplicaType: gitalypb.ReplicaID_REPLICA_TYPE_VOTER, + }) + require.NoError(t, err) + require.NotNil(t, resp) + + resp2, err := client.AddReplica(ctx, &gitalypb.AddReplicaRequest{ + PartitionKey: partitionKey, + TargetStorage: storageTwo, + TargetAddress: cfgTwo.SocketPath, + ReplicaType: gitalypb.ReplicaID_REPLICA_TYPE_VOTER, + }) + + require.Nil(t, resp2) + testhelper.RequireGrpcCode(t, err, codes.AlreadyExists) +} + +func createAndInitializeReplica( + t *testing.T, + ctx context.Context, + storageName string, + partitionID storage.PartitionID, +) (*raftmgr.Replica, *gitalypb.RaftPartitionKey, config.Cfg, error) { + metrics := raftmgr.NewMetrics() + + // Configure node with both storages so AddReplica can validate target storage + cfg := testcfg.Build(t, testcfg.WithStorages(storageOne, storageTwo)) + cfg.Raft = raftConfigsForTest(t) + + logger := testhelper.SharedLogger(t) + stagingDir := testhelper.TempDir(t) + stateDir := testhelper.TempDir(t) + posTracker := partitionlog.NewPositionTracker() + + dbMgr := setupDB(t, ctx, logger, cfg) + t.Cleanup(dbMgr.Close) + + db, err := dbMgr.GetDB(storageName) + require.NoError(t, err) + + logStore, err := raftmgr.NewReplicaLogStore(storageName, partitionID, cfg.Raft, db, stagingDir, stateDir, &raftmgr.MockConsumer{}, posTracker, logger, metrics) + require.NoError(t, err) + + conns := client.NewPool(client.WithDialOptions(client.UnaryInterceptor(), client.StreamInterceptor())) + t.Cleanup(func() { + err := conns.Close() + require.NoError(t, err) + }) + + // before initializing new node we need to set the socket path + socketPath := testhelper.GetTemporaryGitalySocketFileName(t) + listener, err := net.Listen("unix", socketPath) + require.NoError(t, err) + cfg.SocketPath = socketPath + + srv := grpc.NewServer() + + raftNode, err := raftmgr.NewNode(cfg, logger, dbMgr, conns) + require.NoError(t, err) + + gitalypb.RegisterRaftServiceServer(srv, NewServer(&service.Dependencies{ + Node: raftNode, + Cfg: cfg, + })) + + go testhelper.MustServe(t, srv, listener) + + t.Cleanup(func() { + srv.GracefulStop() + }) + + raftFactory := raftmgr.DefaultFactoryWithNode(cfg.Raft, raftNode) + partitionKey := raftmgr.NewPartitionKey(storageName, partitionID) + ctx = storage.ContextWithPartitionInfo(ctx, partitionKey, 1, fmt.Sprintf("@hashed/%s.git", partitionKey.GetValue())) + replica, err := raftFactory(ctx, storageName, logStore, logger, metrics) + require.NoError(t, err) + + err = replica.Initialize(ctx, 0) + require.NoError(t, err) + + return replica, partitionKey, cfg, nil +} diff --git a/internal/gitaly/service/raft/testhelper.go b/internal/gitaly/service/raft/testhelper.go index 86a6a26ff3..ff9d597832 100644 --- a/internal/gitaly/service/raft/testhelper.go +++ b/internal/gitaly/service/raft/testhelper.go @@ -1,20 +1,24 @@ package raft import ( + "context" "path/filepath" "testing" "github.com/stretchr/testify/require" + gitalyauth "gitlab.com/gitlab-org/gitaly/v18/auth" "gitlab.com/gitlab-org/gitaly/v18/internal/gitaly/config" "gitlab.com/gitlab-org/gitaly/v18/internal/gitaly/storage" "gitlab.com/gitlab-org/gitaly/v18/internal/gitaly/storage/keyvalue" "gitlab.com/gitlab-org/gitaly/v18/internal/gitaly/storage/keyvalue/databasemgr" "gitlab.com/gitlab-org/gitaly/v18/internal/gitaly/storage/raftmgr" + "gitlab.com/gitlab-org/gitaly/v18/internal/grpc/client" "gitlab.com/gitlab-org/gitaly/v18/internal/helper" "gitlab.com/gitlab-org/gitaly/v18/internal/log" "gitlab.com/gitlab-org/gitaly/v18/internal/testhelper" "gitlab.com/gitlab-org/gitaly/v18/internal/testhelper/testcfg" "gitlab.com/gitlab-org/gitaly/v18/proto/go/gitalypb" + "google.golang.org/grpc" ) const ( @@ -158,3 +162,18 @@ type mockNonRaftNode struct{} func (m *mockNonRaftNode) GetStorage(storageName string) (storage.Storage, error) { return nil, nil } + +func dialService(t *testing.T, ctx context.Context, cfg config.Cfg) *grpc.ClientConn { + dialOptions := []grpc.DialOption{client.UnaryInterceptor(), client.StreamInterceptor()} + if cfg.Auth.Token != "" { + dialOptions = append(dialOptions, grpc.WithPerRPCCredentials(gitalyauth.RPCCredentialsV2(cfg.Auth.Token))) + } + + addr, err := cfg.GetAddressWithScheme() + require.NoError(t, err) + + conn, err := client.New(ctx, addr, client.WithGrpcOptions(dialOptions)) + require.NoError(t, err) + t.Cleanup(func() { testhelper.MustClose(t, conn) }) + return conn +} -- GitLab From 77479731f995cd178e6f4d3b8969b7586fc7a4e0 Mon Sep 17 00:00:00 2001 From: Quang-Minh Nguyen Date: Fri, 10 Oct 2025 17:55:05 +0700 Subject: [PATCH 3/4] raft: Integrate JoinCluster into AddLearner with rollback support The current AddLearner implementation only proposes a configuration change without ensuring the learner node actually joins the cluster. This creates an incomplete state where the learner is registered in the routing table but hasn't initialized its Raft state machine, preventing it from participating in log replication. This commit extends AddLearner to call JoinCluster on the target node immediately after the configuration change commits. This ensures the learner properly initializes its Raft instance and begins receiving log entries. When JoinCluster fails, the learner is automatically removed from the cluster to maintain consistency. --- .../gitaly/service/raft/add_replica_test.go | 12 +- internal/gitaly/storage/raftmgr/replica.go | 23 ++- .../gitaly/storage/raftmgr/replica_test.go | 170 ++++++++++++++++++ 3 files changed, 196 insertions(+), 9 deletions(-) diff --git a/internal/gitaly/service/raft/add_replica_test.go b/internal/gitaly/service/raft/add_replica_test.go index f002f7ba58..a458295de0 100644 --- a/internal/gitaly/service/raft/add_replica_test.go +++ b/internal/gitaly/service/raft/add_replica_test.go @@ -46,10 +46,10 @@ func TestAddReplica_Success(t *testing.T) { replicaOne, partitionKey, cfgOne, err := createAndInitializeReplica(t, ctx, storageOne, 1) require.NoError(t, err) - t.Cleanup(func() { + defer func() { err := replicaOne.Close() require.NoError(t, err) - }) + }() require.Eventually(t, func() bool { return replicaOne.AppendedLSN() > 1 @@ -220,10 +220,10 @@ func TestAddReplica_TargetStorageNotFound(t *testing.T) { replicaOne, partitionKey, cfgOne, err := createAndInitializeReplica(t, ctx, storageOne, 1) require.NoError(t, err) - t.Cleanup(func() { + defer func() { err := replicaOne.Close() require.NoError(t, err) - }) + }() require.Eventually(t, func() bool { return replicaOne.AppendedLSN() > 1 @@ -250,10 +250,10 @@ func TestAddReplica_MemberAlreadyExists(t *testing.T) { replicaOne, partitionKey, cfgOne, err := createAndInitializeReplica(t, ctx, storageOne, 1) require.NoError(t, err) - t.Cleanup(func() { + defer func() { err := replicaOne.Close() require.NoError(t, err) - }) + }() require.Eventually(t, func() bool { return replicaOne.AppendedLSN() > 1 diff --git a/internal/gitaly/storage/raftmgr/replica.go b/internal/gitaly/storage/raftmgr/replica.go index c0bff5eaa1..2611c2b7ce 100644 --- a/internal/gitaly/storage/raftmgr/replica.go +++ b/internal/gitaly/storage/raftmgr/replica.go @@ -1096,7 +1096,7 @@ func (replica *Replica) proposeMembershipChange( return replica.proposeAddNode(ctx, changeType, destinationStorageName, memberID, metadata) case ConfChangeAddLearnerNode: - return replica.proposeAddLearner(ctx, changeType, memberID, metadata) + return replica.proposeAddLearner(ctx, changeType, destinationStorageName, memberID, metadata) default: return fmt.Errorf("config change type %v not supported", confChangeType) @@ -1134,8 +1134,25 @@ func (replica *Replica) proposeAddNode(ctx context.Context, changeType, destinat return nil } -func (replica *Replica) proposeAddLearner(ctx context.Context, changeType string, memberID uint64, metadata *gitalypb.ReplicaID_Metadata) error { - return replica.proposeConfChange(ctx, changeType, memberID, "", ConfChangeAddLearnerNode, metadata) +func (replica *Replica) proposeAddLearner(ctx context.Context, changeType string, destinationStorageName string, memberID uint64, metadata *gitalypb.ReplicaID_Metadata) (returnedErr error) { + // First, propose the configuration change + if err := replica.proposeConfChange(ctx, changeType, memberID, destinationStorageName, ConfChangeAddLearnerNode, metadata); err != nil { + return err + } + + // After conf change commits, call JoinCluster on the target node + if err := replica.joinCluster(ctx, memberID, destinationStorageName, metadata); err != nil { + // If join fails, attempt to remove the node from the cluster + replica.logger.WithError(err).Warn("join cluster failed for learner, attempting to remove node") + + if removeErr := replica.proposeConfChange(ctx, string(removeNode), memberID, "", ConfChangeRemoveNode, nil); removeErr != nil { + replica.logger.WithError(removeErr).Error("failed to remove learner node after join failure") + } + + return fmt.Errorf("join cluster failed: %w", err) + } + + return nil } func (replica *Replica) proposeConfChange( diff --git a/internal/gitaly/storage/raftmgr/replica_test.go b/internal/gitaly/storage/raftmgr/replica_test.go index 81b24f1d7a..fcd55efb6c 100644 --- a/internal/gitaly/storage/raftmgr/replica_test.go +++ b/internal/gitaly/storage/raftmgr/replica_test.go @@ -2276,3 +2276,173 @@ func TestReplica_GetCurrentState(t *testing.T) { } }) } + +func TestReplica_AddLearner(t *testing.T) { + t.Parallel() + + createTestNode := func(t *testing.T, ctx context.Context, memberID uint64, partitionID storage.PartitionID, raftCfg config.Raft, metrics *Metrics, opts ...OptionFunc) (*Replica, string, *grpc.Server) { + cfg := testcfg.Build(t) + logger := testhelper.NewLogger(t) + registry := NewReplicaRegistry() + db, _ := dbSetup(t, ctx, cfg, testhelper.TempDir(t), cfg.Storages[0].Name, logger) + routingTable := NewKVRoutingTable(db) + + transport := NewGrpcTransport(logger, cfg, routingTable, registry, nil) + socketPath, srv := createTempServer(t, transport) + + replica, err := createRaftReplica(t, ctx, memberID, socketPath, raftCfg, partitionID, metrics, opts...) + require.NoError(t, err) + + partitionKey := NewPartitionKey(cfg.Storages[0].Name, partitionID) + + registry.RegisterReplica(partitionKey, replica) + transport.registry = registry + + err = replica.Initialize(ctx, 0) + require.NoError(t, err) + + return replica, socketPath, srv + } + + waitForLeadership := func(t *testing.T, replica *Replica, timeout time.Duration) { + require.Eventually(t, func() bool { + return replica.AppendedLSN() > 1 && replica.leadership.IsLeader() + }, timeout, 5*time.Millisecond, "replica should become leader") + } + + drainNotificationQueues := func(t *testing.T, replicas ...*Replica) { + for _, replica := range replicas { + require.Eventually(t, func() bool { + select { + case err := <-replica.GetNotificationQueue(): + if err != nil { + require.NoError(t, err) + return false + } + return true + default: + return false + } + }, waitTimeout, 10*time.Millisecond) + } + } + + t.Run("successful learner addition with metadata", func(t *testing.T) { + t.Parallel() + + ctx := testhelper.Context(t) + raftCfg := raftConfigsForTest(t) + metrics := NewMetrics() + partitionID := storage.PartitionID(1) + + replica, socketPath, srv := createTestNode(t, ctx, 1, partitionID, raftCfg, metrics) + replicaAddress := "unix://" + socketPath + defer func() { + srv.Stop() + require.NoError(t, replica.Close()) + }() + + waitForLeadership(t, replica, waitTimeout) + + raftEnabledStorage := replica.raftEnabledStorage + require.NotNil(t, raftEnabledStorage, "storage should be a RaftEnabledStorage") + + routingTable := raftEnabledStorage.GetRoutingTable() + partitionKey := replica.partitionKey + + // Create second node for learner + replicaTwo, socketPathTwo, srvTwo := createTestNode(t, ctx, 3, partitionID, raftCfg, metrics) + replicaTwoAddress := "unix://" + socketPathTwo + defer func() { + srvTwo.Stop() + require.NoError(t, replicaTwo.Close()) + }() + + registryTwo := replicaTwo.raftEnabledStorage.GetReplicaRegistry() + registryTwo.RegisterReplica(partitionKey, replicaTwo) + + routingTableTwo := replicaTwo.raftEnabledStorage.GetRoutingTable() + err := routingTableTwo.UpsertEntry(RoutingTableEntry{ + Replicas: []*gitalypb.ReplicaID{ + { + PartitionKey: partitionKey, + Type: gitalypb.ReplicaID_REPLICA_TYPE_VOTER, + MemberId: 1, + Metadata: &gitalypb.ReplicaID_Metadata{ + Address: replicaAddress, + }, + }, + }, + Term: 1, + Index: 2, + }) + + require.NoError(t, err) + destination := "default" + + err = replica.AddLearner(ctx, replicaTwoAddress, destination) + require.NoError(t, err, "adding learner should succeed when leader") + + // Verify learner was added with correct type + require.Eventually(t, func() bool { + entry, err := routingTable.GetEntry(partitionKey) + if err != nil { + return false + } + return slices.ContainsFunc(entry.Replicas, func(r *gitalypb.ReplicaID) bool { + return r.GetMetadata().GetAddress() == replicaTwoAddress && + r.GetType() == gitalypb.ReplicaID_REPLICA_TYPE_LEARNER + }) + }, waitTimeout, 5*time.Millisecond, "routing table should be updated with learner") + + // Verify learner has RelativePath in metadata + entry, err := routingTable.GetEntry(partitionKey) + require.NoError(t, err) + + learnerIdx := slices.IndexFunc(entry.Replicas, func(r *gitalypb.ReplicaID) bool { + return r.GetType() == gitalypb.ReplicaID_REPLICA_TYPE_LEARNER + }) + require.NotEqual(t, -1, learnerIdx, "learner should be in routing table") + + learnerReplica := entry.Replicas[learnerIdx] + require.NotNil(t, learnerReplica.GetMetadata(), "learner should have metadata") + require.NotEmpty(t, learnerReplica.GetMetadata().GetAddress(), "learner should have Address in metadata") + + drainNotificationQueues(t, replica, replicaTwo) + + testhelper.RequirePromMetrics(t, metrics, ` + # HELP gitaly_raft_membership_changes_total Counter of Raft membership changes by type + # TYPE gitaly_raft_membership_changes_total counter + gitaly_raft_membership_changes_total{change_type="add_learner"} 1 + `, + "gitaly_raft_membership_changes_total", + ) + }) + + t.Run("fails when node is not leader", func(t *testing.T) { + t.Parallel() + + ctx := testhelper.Context(t) + raftCfg := raftConfigsForTest(t) + metrics := NewMetrics() + partitionID := storage.PartitionID(1) + + replica, _, srv := createTestNode(t, ctx, 1, partitionID, raftCfg, metrics) + defer func() { + srv.Stop() + require.NoError(t, replica.Close()) + }() + + // Set a random leader ID to simulate a non-leader + replica.leadership.SetLeader(999, false) + + err := replica.AddLearner(ctx, "gitaly-node-2:8075", storageName) + require.EqualError(t, err, "replica is not the leader", "adding learner should fail when not leader") + + testhelper.RequirePromMetrics(t, metrics, ` + # HELP gitaly_raft_membership_errors_total Counter of Raft membership operation errors + # TYPE gitaly_raft_membership_errors_total counter + gitaly_raft_membership_errors_total{change_type="add_learner",reason="not_leader"} 1 + `) + }) +} -- GitLab From 96294363040db874ccdb91e1ce80ebbfb0accfed Mon Sep 17 00:00:00 2001 From: Divya Rani Date: Tue, 25 Nov 2025 19:54:10 +0530 Subject: [PATCH 4/4] raft: Fix data race --- internal/gitaly/storage/raftmgr/replica.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/internal/gitaly/storage/raftmgr/replica.go b/internal/gitaly/storage/raftmgr/replica.go index 2611c2b7ce..ad5f9f7560 100644 --- a/internal/gitaly/storage/raftmgr/replica.go +++ b/internal/gitaly/storage/raftmgr/replica.go @@ -1039,11 +1039,17 @@ func (replica *Replica) signalError(err error) { // IsStarted implements RaftReplica.IsStarted func (replica *Replica) IsStarted() bool { + replica.mutex.Lock() + defer replica.mutex.Unlock() + return replica.started } // Step processes a Raft message from a remote node func (replica *Replica) Step(ctx context.Context, msg raftpb.Message) error { + replica.mutex.Lock() + defer replica.mutex.Unlock() + if !replica.started { return fmt.Errorf("raft replica not started") } @@ -1245,6 +1251,9 @@ func (replica *Replica) joinCluster(ctx context.Context, targetMemberID uint64, // GetLeaderID returns the leader ID of the replica. func (replica *Replica) GetLeaderID() (uint64, error) { + replica.mutex.Lock() + defer replica.mutex.Unlock() + if !replica.started { return 0, fmt.Errorf("raft replica not started") } @@ -1254,6 +1263,9 @@ func (replica *Replica) GetLeaderID() (uint64, error) { // GetMemberID returns the member ID of the replica. func (replica *Replica) GetMemberID() (uint64, error) { + replica.mutex.Lock() + defer replica.mutex.Unlock() + if !replica.started { return 0, fmt.Errorf("raft replica not started") } -- GitLab