diff --git a/internal/gitaly/service/raft/add_replica.go b/internal/gitaly/service/raft/add_replica.go new file mode 100644 index 0000000000000000000000000000000000000000..b21a878f305cceaeee69c51617c246f6b1d6001e --- /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 0000000000000000000000000000000000000000..a458295de0b372065833536ce6fbc4b989ca02f7 --- /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) + defer 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) + defer 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) + defer 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 86a6a26ff3f40cb07d0349f0dd06d734f893bd59..ff9d5978321a38bdf15b614d884535c26f8eeec6 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 +} diff --git a/internal/gitaly/storage/raftmgr/replica.go b/internal/gitaly/storage/raftmgr/replica.go index c0bff5eaa1293fb564e6ba1c4c5e9572439d0d2f..ad5f9f7560b89b7963fc95e1a980a0c56ebd21ed 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") } @@ -1096,7 +1102,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 +1140,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( @@ -1228,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") } @@ -1237,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") } diff --git a/internal/gitaly/storage/raftmgr/replica_test.go b/internal/gitaly/storage/raftmgr/replica_test.go index 81b24f1d7a9941a1df625f1791f36a536dc9bc0b..fcd55efb6c79b8487bc330800b832b3c0b832638 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 + `) + }) +} diff --git a/proto/cluster.proto b/proto/cluster.proto index 552eb70ccd8498b0a69cdd01cbbb45fe82b3db06..9a204eb200c6d49aca2df4682a555e2c4114f3de 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 a7c411dc6692e4e9fe228bfdcc9ce3cd78ef0b13..5d5e2c82a486954c96b1b6155dae8e3238a6e402 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 fa31ee93c2a0589a596da0ab14ee8cf5ce80eaeb..428ec3bb8127f0c71e4da3d115a47593141092dd 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{ {