diff --git a/adapter/distribution_server.go b/adapter/distribution_server.go index d0abb3fe5..300dbc58c 100644 --- a/adapter/distribution_server.go +++ b/adapter/distribution_server.go @@ -222,7 +222,7 @@ func (s *DistributionServer) saveSplitResultViaCoordinator( } nextRouteID := right.RouteID + 1 - ops, err := buildCatalogSplitOps(parentID, left, right, nextVersion, nextRouteID) + ops, err := buildCatalogSplitOps(parentID, left, right, nextVersion, nextRouteID, s.catalog.AllowsRouteDescriptorV2Writes()) if err != nil { return distribution.CatalogSnapshot{}, grpcStatusErrorf(codes.Internal, "build split mutations: %v", err) } @@ -242,6 +242,7 @@ func buildCatalogSplitOps( right distribution.RouteDescriptor, nextVersion uint64, nextRouteID uint64, + allowRouteDescriptorV2Writes bool, ) ([]*kv.Elem[kv.OP], error) { // SplitRange mutates the catalog surgically: delete one parent route, add two // children, bump the version, and advance the next-route-id counter. @@ -251,7 +252,7 @@ func buildCatalogSplitOps( Key: distribution.CatalogRouteKey(parentID), }) for _, route := range []distribution.RouteDescriptor{left, right} { - encoded, err := distribution.EncodeRouteDescriptor(route) + encoded, err := distribution.EncodeRouteDescriptorForCatalogWrite(route, allowRouteDescriptorV2Writes) if err != nil { return nil, errors.WithStack(err) } @@ -384,20 +385,26 @@ func splitCatalogRoutes( ) (distribution.RouteDescriptor, distribution.RouteDescriptor) { // parent and splitKey are already cloned before this point and are immutable here. left := distribution.RouteDescriptor{ - RouteID: leftID, - Start: parent.Start, - End: splitKey, - GroupID: parent.GroupID, - State: parent.State, - ParentRouteID: parent.RouteID, + RouteID: leftID, + Start: parent.Start, + End: splitKey, + GroupID: parent.GroupID, + State: parent.State, + ParentRouteID: parent.RouteID, + StagedVisibilityActive: parent.StagedVisibilityActive, + MigrationJobID: parent.MigrationJobID, + MinWriteTSExclusive: parent.MinWriteTSExclusive, } right := distribution.RouteDescriptor{ - RouteID: rightID, - Start: splitKey, - End: parent.End, - GroupID: parent.GroupID, - State: parent.State, - ParentRouteID: parent.RouteID, + RouteID: rightID, + Start: splitKey, + End: parent.End, + GroupID: parent.GroupID, + State: parent.State, + ParentRouteID: parent.RouteID, + StagedVisibilityActive: parent.StagedVisibilityActive, + MigrationJobID: parent.MigrationJobID, + MinWriteTSExclusive: parent.MinWriteTSExclusive, } return left, right } @@ -447,12 +454,15 @@ func toProtoRouteDescriptors(routes []distribution.RouteDescriptor) []*pb.RouteD func toProtoRouteDescriptor(route distribution.RouteDescriptor) *pb.RouteDescriptor { return &pb.RouteDescriptor{ - RouteId: route.RouteID, - Start: distribution.CloneBytes(route.Start), - End: distribution.CloneBytes(route.End), - RaftGroupId: route.GroupID, - State: toProtoRouteState(route.State), - ParentRouteId: route.ParentRouteID, + RouteId: route.RouteID, + Start: distribution.CloneBytes(route.Start), + End: distribution.CloneBytes(route.End), + RaftGroupId: route.GroupID, + State: toProtoRouteState(route.State), + ParentRouteId: route.ParentRouteID, + StagedVisibilityActive: route.StagedVisibilityActive, + MigrationJobId: route.MigrationJobID, + MinWriteTsExclusive: route.MinWriteTSExclusive, } } diff --git a/adapter/distribution_server_test.go b/adapter/distribution_server_test.go index 030116c34..c28a6ee9a 100644 --- a/adapter/distribution_server_test.go +++ b/adapter/distribution_server_test.go @@ -76,15 +76,18 @@ func TestDistributionServerListRoutes_ReadsDurableCatalog(t *testing.T) { t.Parallel() ctx := context.Background() - catalog := distribution.NewCatalogStore(store.NewMVCCStore()) + catalog := distribution.NewCatalogStore(store.NewMVCCStore(), distribution.WithCatalogRouteDescriptorV2Writes(true)) saved, err := catalog.Save(ctx, 0, []distribution.RouteDescriptor{ { - RouteID: 2, - Start: []byte("m"), - End: nil, - GroupID: 2, - State: distribution.RouteStateWriteFenced, - ParentRouteID: 1, + RouteID: 2, + Start: []byte("m"), + End: nil, + GroupID: 2, + State: distribution.RouteStateWriteFenced, + ParentRouteID: 1, + StagedVisibilityActive: true, + MigrationJobID: 42, + MinWriteTSExclusive: 99, }, { RouteID: 1, @@ -111,6 +114,9 @@ func TestDistributionServerListRoutes_ReadsDurableCatalog(t *testing.T) { require.Equal(t, uint64(2), resp.Routes[1].RouteId) require.Nil(t, resp.Routes[1].End) require.Equal(t, pb.RouteState_ROUTE_STATE_WRITE_FENCED, resp.Routes[1].State) + require.True(t, resp.Routes[1].StagedVisibilityActive) + require.Equal(t, uint64(42), resp.Routes[1].MigrationJobId) + require.Equal(t, uint64(99), resp.Routes[1].MinWriteTsExclusive) } func TestDistributionServerListRoutes_RequiresCatalog(t *testing.T) { @@ -128,15 +134,16 @@ func TestDistributionServerSplitRange_Success(t *testing.T) { ctx := context.Background() baseStore := store.NewMVCCStore() - catalog := distribution.NewCatalogStore(baseStore) + catalog := distribution.NewCatalogStore(baseStore, distribution.WithCatalogRouteDescriptorV2Writes(true)) saved, err := catalog.Save(ctx, 0, []distribution.RouteDescriptor{ { - RouteID: 1, - Start: []byte(""), - End: []byte("m"), - GroupID: 1, - State: distribution.RouteStateActive, - ParentRouteID: 0, + RouteID: 1, + Start: []byte(""), + End: []byte("m"), + GroupID: 1, + State: distribution.RouteStateActive, + ParentRouteID: 0, + MinWriteTSExclusive: 99, }, { RouteID: 2, @@ -169,11 +176,13 @@ func TestDistributionServerSplitRange_Success(t *testing.T) { require.Equal(t, []byte("g"), resp.Left.End) require.Equal(t, uint64(1), resp.Left.RaftGroupId) require.Equal(t, uint64(1), resp.Left.ParentRouteId) + require.Equal(t, uint64(99), resp.Left.MinWriteTsExclusive) require.Equal(t, uint64(4), resp.Right.RouteId) require.Equal(t, []byte("g"), resp.Right.Start) require.Equal(t, []byte("m"), resp.Right.End) require.Equal(t, uint64(1), resp.Right.RaftGroupId) require.Equal(t, uint64(1), resp.Right.ParentRouteId) + require.Equal(t, uint64(99), resp.Right.MinWriteTsExclusive) snapshot, err := catalog.Snapshot(ctx) require.NoError(t, err) @@ -181,16 +190,20 @@ func TestDistributionServerSplitRange_Success(t *testing.T) { require.Len(t, snapshot.Routes, 3) // Catalog snapshots are sorted by range start key. require.Equal(t, uint64(3), snapshot.Routes[0].RouteID) + require.Equal(t, uint64(99), snapshot.Routes[0].MinWriteTSExclusive) require.Equal(t, uint64(4), snapshot.Routes[1].RouteID) + require.Equal(t, uint64(99), snapshot.Routes[1].MinWriteTSExclusive) require.Equal(t, uint64(2), snapshot.Routes[2].RouteID) require.Equal(t, uint64(2), engine.Version()) leftRoute, ok := engine.GetRoute([]byte("b")) require.True(t, ok) require.Equal(t, uint64(3), leftRoute.RouteID) + require.Equal(t, uint64(99), leftRoute.MinWriteTSExclusive) rightRoute, ok := engine.GetRoute([]byte("h")) require.True(t, ok) require.Equal(t, uint64(4), rightRoute.RouteID) + require.Equal(t, uint64(99), rightRoute.MinWriteTSExclusive) } func TestDistributionServerSplitRange_RequiresCoordinator(t *testing.T) { @@ -524,7 +537,7 @@ func TestBuildCatalogSplitOps_UsesSurgicalSplitMutations(t *testing.T) { ParentRouteID: 1, } - ops, err := buildCatalogSplitOps(1, left, right, 2, 5) + ops, err := buildCatalogSplitOps(1, left, right, 2, 5, false) require.NoError(t, err) require.Len(t, ops, 5) require.Equal(t, kv.Del, ops[0].Op) diff --git a/adapter/grpc.go b/adapter/grpc.go index 712efd911..773dacb93 100644 --- a/adapter/grpc.go +++ b/adapter/grpc.go @@ -34,6 +34,22 @@ type GRPCServer struct { pb.UnimplementedTransactionalKVServer } +type rawReadFenceGetter interface { + GetAtWithReadFence(ctx context.Context, key []byte, ts uint64, groupID uint64, readRouteVersion uint64) ([]byte, error) +} + +type rawReadFenceCommitTSReader interface { + LatestCommitTSWithReadFence(ctx context.Context, key []byte, readRouteVersion uint64) (uint64, bool, error) +} + +type rawReadFenceScanner interface { + ScanAtWithReadFence(ctx context.Context, start []byte, end []byte, limit int, ts uint64, reverse bool, groupID uint64, readRouteVersion uint64, routeStart []byte, routeEnd []byte) ([]*store.KVPair, error) +} + +type rawReadFenceVersioner interface { + ReadRouteVersion() uint64 +} + type GRPCServerOption func(*GRPCServer) type rawGroupGetter interface { @@ -98,7 +114,9 @@ func (r *GRPCServer) RawGet(ctx context.Context, req *pb.RawGetRequest) (*pb.Raw var v []byte var err error - if groupID := req.GetGroupId(); groupID != 0 { + if fenceGetter, ok := r.store.(rawReadFenceGetter); ok { + v, err = fenceGetter.GetAtWithReadFence(ctx, req.Key, readTS, req.GetGroupId(), r.readRouteVersion(req.GetReadRouteVersion())) + } else if groupID := req.GetGroupId(); groupID != 0 { groupGetter, ok := r.store.(rawGroupGetter) if !ok { return nil, errors.WithStack(status.Error(codes.FailedPrecondition, "raw get with explicit group requires a group-aware store")) @@ -137,7 +155,14 @@ func (r *GRPCServer) RawLatestCommitTS(ctx context.Context, req *pb.RawLatestCom }, nil } - ts, exists, err := r.store.LatestCommitTS(ctx, key) + var ts uint64 + var exists bool + var err error + if fenceReader, ok := r.store.(rawReadFenceCommitTSReader); ok { + ts, exists, err = fenceReader.LatestCommitTSWithReadFence(ctx, key, r.readRouteVersion(req.GetReadRouteVersion())) + } else { + ts, exists, err = r.store.LatestCommitTS(ctx, key) + } if err != nil { return nil, errors.WithStack(err) } @@ -159,21 +184,7 @@ func (r *GRPCServer) RawScanAt(ctx context.Context, req *pb.RawScanAtRequest) (* readTS = globalSnapshotTS(ctx, r.clock(), r.store) } - var res []*store.KVPair - if groupID := req.GetGroupId(); groupID != 0 { - if req.GetReverse() { - return &pb.RawScanAtResponse{Kv: nil}, errors.WithStack(status.Error(codes.InvalidArgument, "raw scan with explicit group does not support reverse scans")) - } - groupScanner, ok := r.store.(rawGroupScanner) - if !ok { - return &pb.RawScanAtResponse{Kv: nil}, errors.WithStack(status.Error(codes.FailedPrecondition, "raw scan with explicit group requires a group-aware store")) - } - res, err = groupScanner.ScanGroupAt(ctx, groupID, req.StartKey, req.EndKey, limit, readTS) - } else if req.GetReverse() { - res, err = r.store.ReverseScanAt(ctx, req.StartKey, req.EndKey, limit, readTS) - } else { - res, err = r.store.ScanAt(ctx, req.StartKey, req.EndKey, limit, readTS) - } + res, err := r.rawScanAt(ctx, req, limit, readTS) if err != nil { if errors.Is(err, store.ErrReadTSCompacted) { return &pb.RawScanAtResponse{Kv: nil}, errors.WithStack(status.Error(codes.FailedPrecondition, store.ErrReadTSCompacted.Error())) @@ -184,6 +195,62 @@ func (r *GRPCServer) RawScanAt(ctx context.Context, req *pb.RawScanAtRequest) (* return &pb.RawScanAtResponse{Kv: rawKvPairs(res)}, nil } +func (r *GRPCServer) rawScanAt(ctx context.Context, req *pb.RawScanAtRequest, limit int, readTS uint64) ([]*store.KVPair, error) { + if fenceScanner, ok := r.store.(rawReadFenceScanner); ok { + if req.GetGroupId() != 0 && req.GetReverse() && !req.GetRouteBoundsPresent() { + return nil, errors.WithStack(status.Error(codes.InvalidArgument, "raw scan with explicit group does not support reverse scans")) + } + routeStart, routeEnd := rawScanRouteBounds(req) + res, err := fenceScanner.ScanAtWithReadFence(ctx, req.StartKey, req.EndKey, limit, readTS, req.GetReverse(), req.GetGroupId(), r.readRouteVersion(req.GetReadRouteVersion()), routeStart, routeEnd) + return res, errors.WithStack(err) + } + if req.GetGroupId() != 0 && req.GetReverse() { + return nil, errors.WithStack(status.Error(codes.InvalidArgument, "raw scan with explicit group does not support reverse scans")) + } + if groupID := req.GetGroupId(); groupID != 0 { + groupScanner, ok := r.store.(rawGroupScanner) + if !ok { + return nil, errors.WithStack(status.Error(codes.FailedPrecondition, "raw scan with explicit group requires a group-aware store")) + } + res, err := groupScanner.ScanGroupAt(ctx, groupID, req.StartKey, req.EndKey, limit, readTS) + return res, errors.WithStack(err) + } + if req.GetReverse() { + res, err := r.store.ReverseScanAt(ctx, req.StartKey, req.EndKey, limit, readTS) + return res, errors.WithStack(err) + } + res, err := r.store.ScanAt(ctx, req.StartKey, req.EndKey, limit, readTS) + return res, errors.WithStack(err) +} + +func rawScanRouteBounds(req *pb.RawScanAtRequest) ([]byte, []byte) { + if req == nil { + return nil, nil + } + routeStart := req.GetRouteStart() + routeEnd := req.GetRouteEnd() + if req.GetRouteBoundsPresent() { + if routeStart == nil { + routeStart = []byte{} + } + if routeEnd == nil { + routeEnd = []byte{} + } + } + return routeStart, routeEnd +} + +func (r *GRPCServer) readRouteVersion(requested uint64) uint64 { + if requested != 0 { + return requested + } + versioner, ok := r.store.(rawReadFenceVersioner) + if !ok { + return 0 + } + return versioner.ReadRouteVersion() +} + func rawScanLimit(limit64 int64) (int, error) { if limit64 < 0 { return 0, errors.WithStack(kv.ErrInvalidRequest) diff --git a/adapter/grpc_test.go b/adapter/grpc_test.go index fd1393cd5..781aa9f47 100644 --- a/adapter/grpc_test.go +++ b/adapter/grpc_test.go @@ -12,8 +12,11 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/grpc" + "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials/insecure" _ "google.golang.org/grpc/health" + "google.golang.org/grpc/status" + goproto "google.golang.org/protobuf/proto" ) func Test_value_can_be_deleted(t *testing.T) { @@ -209,6 +212,192 @@ func TestGRPCServer_RawScanAt_UsesExplicitGroup(t *testing.T) { require.Equal(t, []byte("z"), st.scanEnd) } +type recordingRawReadFenceStore struct { + store.MVCCStore + + routeVersion uint64 + getReadRouteVersion uint64 + latestReadRouteVersion uint64 + scanReadRouteVersion uint64 + scanReadRouteStart []byte + scanReadRouteEnd []byte + scanReverse bool + scanGroupID uint64 + scanRouteBoundsPresent bool + callerSuppliedGetSeen uint64 + callerSuppliedScanSeen uint64 + callerSuppliedLatestSeen uint64 +} + +func (s *recordingRawReadFenceStore) ReadRouteVersion() uint64 { + return s.routeVersion +} + +func (s *recordingRawReadFenceStore) GetAtWithReadFence(_ context.Context, _ []byte, _ uint64, _ uint64, readRouteVersion uint64) ([]byte, error) { + s.getReadRouteVersion = readRouteVersion + if readRouteVersion == 99 { + s.callerSuppliedGetSeen = readRouteVersion + } + return []byte("v"), nil +} + +func (s *recordingRawReadFenceStore) LatestCommitTSWithReadFence(_ context.Context, _ []byte, readRouteVersion uint64) (uint64, bool, error) { + s.latestReadRouteVersion = readRouteVersion + if readRouteVersion == 98 { + s.callerSuppliedLatestSeen = readRouteVersion + } + return 10, true, nil +} + +func (s *recordingRawReadFenceStore) ScanAtWithReadFence(_ context.Context, start []byte, _ []byte, _ int, _ uint64, reverse bool, groupID uint64, readRouteVersion uint64, routeStart []byte, routeEnd []byte) ([]*store.KVPair, error) { + s.scanReadRouteVersion = readRouteVersion + s.scanReadRouteStart = cloneTestBytes(routeStart) + s.scanReadRouteEnd = cloneTestBytes(routeEnd) + s.scanReverse = reverse + s.scanGroupID = groupID + s.scanRouteBoundsPresent = routeStart != nil || routeEnd != nil + if readRouteVersion == 97 { + s.callerSuppliedScanSeen = readRouteVersion + } + return []*store.KVPair{{Key: append([]byte(nil), start...), Value: []byte("v")}}, nil +} + +func cloneTestBytes(b []byte) []byte { + if b == nil { + return nil + } + return append([]byte{}, b...) +} + +func TestGRPCServer_RawReadFenceHelpersStampCurrentRouteVersion(t *testing.T) { + t.Parallel() + + ctx := context.Background() + st := &recordingRawReadFenceStore{MVCCStore: store.NewMVCCStore(), routeVersion: 55} + s := NewGRPCServer(st, nil) + + _, err := s.RawGet(ctx, &pb.RawGetRequest{Key: []byte("k"), Ts: 10}) + require.NoError(t, err) + _, err = s.RawLatestCommitTS(ctx, &pb.RawLatestCommitTSRequest{Key: []byte("k")}) + require.NoError(t, err) + _, err = s.RawScanAt(ctx, &pb.RawScanAtRequest{StartKey: []byte("a"), EndKey: []byte("z"), Limit: 10, Ts: 10}) + require.NoError(t, err) + + require.Equal(t, uint64(55), st.getReadRouteVersion) + require.Equal(t, uint64(55), st.latestReadRouteVersion) + require.Equal(t, uint64(55), st.scanReadRouteVersion) +} + +func TestGRPCServer_RawReadFenceHelpersKeepCallerRouteVersion(t *testing.T) { + t.Parallel() + + ctx := context.Background() + st := &recordingRawReadFenceStore{MVCCStore: store.NewMVCCStore(), routeVersion: 55} + s := NewGRPCServer(st, nil) + + _, err := s.RawGet(ctx, &pb.RawGetRequest{Key: []byte("k"), Ts: 10, ReadRouteVersion: 99}) + require.NoError(t, err) + _, err = s.RawLatestCommitTS(ctx, &pb.RawLatestCommitTSRequest{Key: []byte("k"), ReadRouteVersion: 98}) + require.NoError(t, err) + _, err = s.RawScanAt(ctx, &pb.RawScanAtRequest{ + StartKey: []byte("a"), + EndKey: []byte("z"), + Limit: 10, + Ts: 10, + ReadRouteVersion: 97, + RouteStart: []byte("m"), + RouteEnd: []byte("z"), + }) + require.NoError(t, err) + + require.Equal(t, uint64(99), st.callerSuppliedGetSeen) + require.Equal(t, uint64(98), st.callerSuppliedLatestSeen) + require.Equal(t, uint64(97), st.callerSuppliedScanSeen) + require.Equal(t, []byte("m"), st.scanReadRouteStart) + require.Equal(t, []byte("z"), st.scanReadRouteEnd) +} + +func TestGRPCServer_RawScanAt_PreservesFullRangeRouteBoundsPresence(t *testing.T) { + t.Parallel() + + ctx := context.Background() + st := &recordingRawReadFenceStore{MVCCStore: store.NewMVCCStore(), routeVersion: 55} + s := NewGRPCServer(st, nil) + + wire, err := goproto.Marshal(&pb.RawScanAtRequest{ + StartKey: []byte("!redis|meta|"), + EndKey: []byte("!redis|meta}"), + Limit: 10, + Ts: 10, + ReadRouteVersion: 97, + RouteStart: []byte{}, + RouteEnd: []byte{}, + RouteBoundsPresent: true, + }) + require.NoError(t, err) + + var decoded pb.RawScanAtRequest + require.NoError(t, goproto.Unmarshal(wire, &decoded)) + require.True(t, decoded.GetRouteBoundsPresent()) + require.Nil(t, decoded.RouteStart) + require.Nil(t, decoded.RouteEnd) + + _, err = s.RawScanAt(ctx, &decoded) + require.NoError(t, err) + require.Equal(t, uint64(97), st.scanReadRouteVersion) + require.True(t, st.scanRouteBoundsPresent) + require.NotNil(t, st.scanReadRouteStart) + require.NotNil(t, st.scanReadRouteEnd) + require.Empty(t, st.scanReadRouteStart) + require.Empty(t, st.scanReadRouteEnd) +} + +func TestGRPCServer_RawScanAt_GroupedReverseStaysInvalidArgumentWithReadFenceStore(t *testing.T) { + t.Parallel() + + ctx := context.Background() + st := &recordingRawReadFenceStore{MVCCStore: store.NewMVCCStore(), routeVersion: 55} + s := NewGRPCServer(st, nil) + + _, err := s.RawScanAt(ctx, &pb.RawScanAtRequest{ + StartKey: []byte("a"), + EndKey: []byte("z"), + Limit: 10, + Ts: 10, + GroupId: 42, + Reverse: true, + }) + require.Error(t, err) + require.Equal(t, codes.InvalidArgument, status.Code(err)) + require.Zero(t, st.scanReadRouteVersion) +} + +func TestGRPCServer_RawScanAt_AllowsRouteBoundGroupedReverseWithReadFenceStore(t *testing.T) { + t.Parallel() + + ctx := context.Background() + st := &recordingRawReadFenceStore{MVCCStore: store.NewMVCCStore(), routeVersion: 55} + s := NewGRPCServer(st, nil) + + _, err := s.RawScanAt(ctx, &pb.RawScanAtRequest{ + StartKey: []byte("!redis|meta|"), + EndKey: []byte("!redis|meta}"), + Limit: 10, + Ts: 10, + GroupId: 42, + Reverse: true, + RouteStart: []byte("m"), + RouteBoundsPresent: true, + }) + require.NoError(t, err) + require.Equal(t, uint64(55), st.scanReadRouteVersion) + require.Equal(t, uint64(42), st.scanGroupID) + require.True(t, st.scanReverse) + require.Equal(t, []byte("m"), st.scanReadRouteStart) + require.NotNil(t, st.scanReadRouteEnd) + require.Empty(t, st.scanReadRouteEnd) +} + func TestGRPCServer_Scan_RejectsOversizedLimit(t *testing.T) { t.Parallel() diff --git a/distribution/catalog.go b/distribution/catalog.go index 496340cf5..4c2093840 100644 --- a/distribution/catalog.go +++ b/distribution/catalog.go @@ -22,7 +22,10 @@ const ( catalogVersionCodecVersion byte = 1 catalogRouteCodecVersionMin byte = 1 - catalogRouteCodecVersion byte = 1 + catalogRouteCodecVersionV1 byte = 1 + catalogRouteCodecVersionV2 byte = 2 + catalogRouteCodecVersion byte = catalogRouteCodecVersionV2 + catalogRouteV2TailSize = 1 + catalogUint64Bytes + catalogUint64Bytes catalogScanPageSize = 256 catalogSaveMetaMutationCount = 2 @@ -43,6 +46,7 @@ var ( ErrCatalogInvalidRouteState = errors.New("catalog route state is invalid") ErrCatalogInvalidRouteKey = errors.New("catalog route key is invalid") ErrCatalogRouteKeyIDMismatch = errors.New("catalog route key and record route id mismatch") + ErrCatalogRouteV2WriteDisabled = errors.New("catalog route descriptor v2 writes are disabled") ) // RouteState describes the control-plane state of a route. @@ -70,12 +74,15 @@ func (s RouteState) valid() bool { // RouteDescriptor is the durable representation of a route. type RouteDescriptor struct { - RouteID uint64 - Start []byte - End []byte - GroupID uint64 - State RouteState - ParentRouteID uint64 + RouteID uint64 + Start []byte + End []byte + GroupID uint64 + State RouteState + ParentRouteID uint64 + StagedVisibilityActive bool + MigrationJobID uint64 + MinWriteTSExclusive uint64 } // CatalogSnapshot is a point-in-time snapshot of the route catalog. @@ -87,12 +94,31 @@ type CatalogSnapshot struct { // CatalogStore provides persistence helpers for route catalog state. type CatalogStore struct { - store store.MVCCStore + store store.MVCCStore + allowRouteDescriptorV2Writes bool +} + +type CatalogStoreOption func(*CatalogStore) + +func WithCatalogRouteDescriptorV2Writes(enabled bool) CatalogStoreOption { + return func(s *CatalogStore) { + s.allowRouteDescriptorV2Writes = enabled + } } // NewCatalogStore creates a route catalog persistence helper. -func NewCatalogStore(st store.MVCCStore) *CatalogStore { - return &CatalogStore{store: st} +func NewCatalogStore(st store.MVCCStore, opts ...CatalogStoreOption) *CatalogStore { + s := &CatalogStore{store: st} + for _, opt := range opts { + if opt != nil { + opt(s) + } + } + return s +} + +func (s *CatalogStore) AllowsRouteDescriptorV2Writes() bool { + return s != nil && s.allowRouteDescriptorV2Writes } // CatalogVersionKey returns the reserved key used for catalog version storage. @@ -173,7 +199,11 @@ func EncodeRouteDescriptor(route RouteDescriptor) ([]byte, error) { } out := make([]byte, 0, routeDescriptorEncodedSize(route)) - out = append(out, catalogRouteCodecVersion) + version := catalogRouteCodecVersionV1 + if routeDescriptorRequiresV2(route) { + version = catalogRouteCodecVersionV2 + } + out = append(out, version) out = appendU64(out, route.RouteID) out = appendU64(out, route.GroupID) out = append(out, byte(route.State)) @@ -183,23 +213,32 @@ func EncodeRouteDescriptor(route RouteDescriptor) ([]byte, error) { if route.End == nil { out = append(out, 0) - return out, nil + } else { + out = append(out, 1) + out = appendU64(out, uint64(len(route.End))) + out = append(out, route.End...) } - out = append(out, 1) - out = appendU64(out, uint64(len(route.End))) - out = append(out, route.End...) - + if version == catalogRouteCodecVersionV2 { + out = appendRouteDescriptorV2Tail(out, route) + } return out, nil } +func EncodeRouteDescriptorForCatalogWrite(route RouteDescriptor, allowV2 bool) ([]byte, error) { + if routeDescriptorRequiresV2(route) && !allowV2 { + return nil, errors.WithStack(ErrCatalogRouteV2WriteDisabled) + } + return EncodeRouteDescriptor(route) +} + // DecodeRouteDescriptor deserializes a route descriptor record. func DecodeRouteDescriptor(raw []byte) (RouteDescriptor, error) { if len(raw) < 1 { return RouteDescriptor{}, errors.WithStack(ErrCatalogInvalidRouteRecord) } version := raw[0] - if version < catalogRouteCodecVersionMin { + if version < catalogRouteCodecVersionMin || version > catalogRouteCodecVersion { return RouteDescriptor{}, errors.Wrapf(ErrCatalogInvalidRouteRecord, "unsupported version %d", raw[0]) } @@ -212,8 +251,8 @@ func DecodeRouteDescriptor(raw []byte) (RouteDescriptor, error) { if err != nil { return RouteDescriptor{}, err } - if version == catalogRouteCodecVersion && r.Len() != 0 { - return RouteDescriptor{}, errors.WithStack(ErrCatalogInvalidRouteRecord) + if err := decodeRouteDescriptorTail(version, r, &route); err != nil { + return RouteDescriptor{}, err } if err := validateRouteDescriptor(route); err != nil { return RouteDescriptor{}, err @@ -302,7 +341,7 @@ func (s *CatalogStore) Save(ctx context.Context, expectedVersion uint64, routes if err != nil { return CatalogSnapshot{}, err } - mutations, err := s.buildSaveMutations(ctx, plan) + mutations, err := s.buildSaveMutations(ctx, &plan) if err != nil { return CatalogSnapshot{}, err } @@ -363,18 +402,27 @@ func validateRouteDescriptor(route RouteDescriptor) error { if route.End != nil && bytes.Compare(route.Start, route.End) >= 0 { return errors.WithStack(ErrCatalogInvalidRouteRange) } + if route.StagedVisibilityActive && route.MigrationJobID == 0 { + return errors.WithStack(ErrCatalogInvalidRouteRecord) + } + if !route.StagedVisibilityActive && route.MigrationJobID != 0 { + return errors.WithStack(ErrCatalogInvalidRouteRecord) + } return nil } // CloneRouteDescriptor returns a deep copy of route. func CloneRouteDescriptor(route RouteDescriptor) RouteDescriptor { return RouteDescriptor{ - RouteID: route.RouteID, - Start: CloneBytes(route.Start), - End: CloneBytes(route.End), - GroupID: route.GroupID, - State: route.State, - ParentRouteID: route.ParentRouteID, + RouteID: route.RouteID, + Start: CloneBytes(route.Start), + End: CloneBytes(route.End), + GroupID: route.GroupID, + State: route.State, + ParentRouteID: route.ParentRouteID, + StagedVisibilityActive: route.StagedVisibilityActive, + MigrationJobID: route.MigrationJobID, + MinWriteTSExclusive: route.MinWriteTSExclusive, } } @@ -586,11 +634,16 @@ func (s *CatalogStore) prepareSave(ctx context.Context, expectedVersion uint64, }, nil } -func (s *CatalogStore) buildSaveMutations(ctx context.Context, plan savePlan) ([]*store.KVPairMutation, error) { +func (s *CatalogStore) buildSaveMutations(ctx context.Context, plan *savePlan) ([]*store.KVPairMutation, error) { + if plan == nil { + return nil, errors.WithStack(ErrCatalogStoreRequired) + } existingRoutes, err := s.routesAt(ctx, plan.readTS) if err != nil { return nil, err } + plan.routes = mergeRouteDescriptorWriteFloors(existingRoutes, plan.routes) + nextRouteID, err := s.nextRouteIDAt(ctx, plan.readTS) if err != nil { return nil, err @@ -607,7 +660,7 @@ func (s *CatalogStore) buildSaveMutations(ctx context.Context, plan savePlan) ([ mutations := make([]*store.KVPairMutation, 0, len(existingRoutes)+len(plan.routes)+catalogSaveMetaMutationCount) mutations = appendDeleteRouteMutations(mutations, existingRoutes, plan.routes) - mutations, err = appendUpsertRouteMutations(mutations, existingRoutes, plan.routes) + mutations, err = appendUpsertRouteMutations(mutations, existingRoutes, plan.routes, s.allowRouteDescriptorV2Writes) if err != nil { return nil, err } @@ -624,6 +677,26 @@ func (s *CatalogStore) buildSaveMutations(ctx context.Context, plan savePlan) ([ return mutations, nil } +func mergeRouteDescriptorWriteFloors(existing []RouteDescriptor, desired []RouteDescriptor) []RouteDescriptor { + if len(existing) == 0 || len(desired) == 0 { + return desired + } + existingByID := make(map[uint64]RouteDescriptor, len(existing)) + for _, route := range existing { + existingByID[route.RouteID] = route + } + for i := range desired { + existingRoute, ok := existingByID[desired[i].RouteID] + if !ok { + continue + } + if existingRoute.MinWriteTSExclusive > desired[i].MinWriteTSExclusive { + desired[i].MinWriteTSExclusive = existingRoute.MinWriteTSExclusive + } + } + return desired +} + func (s *CatalogStore) applySaveMutations(ctx context.Context, plan savePlan, mutations []*store.KVPairMutation) error { commitTS, err := s.commitTSForApply(plan.minCommitTS) if err != nil { @@ -669,7 +742,7 @@ func appendDeleteRouteMutations(out []*store.KVPairMutation, existing []RouteDes return out } -func appendUpsertRouteMutations(out []*store.KVPairMutation, existing []RouteDescriptor, desired []RouteDescriptor) ([]*store.KVPairMutation, error) { +func appendUpsertRouteMutations(out []*store.KVPairMutation, existing []RouteDescriptor, desired []RouteDescriptor, allowRouteDescriptorV2Writes bool) ([]*store.KVPairMutation, error) { existingByID := make(map[uint64]RouteDescriptor, len(existing)) for _, route := range existing { existingByID[route.RouteID] = route @@ -679,7 +752,7 @@ func appendUpsertRouteMutations(out []*store.KVPairMutation, existing []RouteDes if existingRoute, ok := existingByID[route.RouteID]; ok && routeDescriptorEqual(existingRoute, route) { continue } - encoded, err := EncodeRouteDescriptor(route) + encoded, err := EncodeRouteDescriptorForCatalogWrite(route, allowRouteDescriptorV2Writes) if err != nil { return nil, err } @@ -698,7 +771,10 @@ func routeDescriptorEqual(left, right RouteDescriptor) bool { bytes.Equal(left.End, right.End) && left.GroupID == right.GroupID && left.State == right.State && - left.ParentRouteID == right.ParentRouteID + left.ParentRouteID == right.ParentRouteID && + left.StagedVisibilityActive == right.StagedVisibilityActive && + left.MigrationJobID == right.MigrationJobID && + left.MinWriteTSExclusive == right.MinWriteTSExclusive } func appendU64(dst []byte, v uint64) []byte { @@ -712,9 +788,66 @@ func routeDescriptorEncodedSize(route RouteDescriptor) int { if route.End != nil { size += catalogUint64Bytes + len(route.End) } + if routeDescriptorRequiresV2(route) { + size += catalogRouteV2TailSize + } return size } +func routeDescriptorRequiresV2(route RouteDescriptor) bool { + return route.StagedVisibilityActive || route.MigrationJobID != 0 || route.MinWriteTSExclusive != 0 +} + +func appendRouteDescriptorV2Tail(out []byte, route RouteDescriptor) []byte { + if route.StagedVisibilityActive { + out = append(out, 1) + } else { + out = append(out, 0) + } + out = appendU64(out, route.MigrationJobID) + out = appendU64(out, route.MinWriteTSExclusive) + return out +} + +func decodeRouteDescriptorTail(version byte, r *bytes.Reader, route *RouteDescriptor) error { + switch version { + case catalogRouteCodecVersionV1: + if r.Len() != 0 { + return errors.WithStack(ErrCatalogInvalidRouteRecord) + } + return nil + case catalogRouteCodecVersionV2: + return decodeRouteDescriptorV2Tail(r, route) + default: + return errors.Wrapf(ErrCatalogInvalidRouteRecord, "unsupported version %d", version) + } +} + +func decodeRouteDescriptorV2Tail(r *bytes.Reader, route *RouteDescriptor) error { + if r.Len() != catalogRouteV2TailSize { + return errors.WithStack(ErrCatalogInvalidRouteRecord) + } + stagedRaw, err := r.ReadByte() + if err != nil { + return errors.WithStack(err) + } + switch stagedRaw { + case 0: + route.StagedVisibilityActive = false + case 1: + route.StagedVisibilityActive = true + default: + return errors.WithStack(ErrCatalogInvalidRouteRecord) + } + if err := binary.Read(r, binary.BigEndian, &route.MigrationJobID); err != nil { + return errors.WithStack(err) + } + if err := binary.Read(r, binary.BigEndian, &route.MinWriteTSExclusive); err != nil { + return errors.WithStack(err) + } + return nil +} + func decodeRouteDescriptorHeader(r *bytes.Reader) (RouteDescriptor, error) { var routeID uint64 var groupID uint64 diff --git a/distribution/catalog_test.go b/distribution/catalog_test.go index 96166b449..990c2d9b5 100644 --- a/distribution/catalog_test.go +++ b/distribution/catalog_test.go @@ -63,6 +63,9 @@ func TestRouteDescriptorCodecRoundTrip(t *testing.T) { if err != nil { t.Fatalf("encode route: %v", err) } + if raw[0] != catalogRouteCodecVersionV1 { + t.Fatalf("zero-M2 route encoded version = %d, want v1", raw[0]) + } got, err := DecodeRouteDescriptor(raw) if err != nil { t.Fatalf("decode route: %v", err) @@ -83,6 +86,9 @@ func TestRouteDescriptorCodecRoundTripNilEnd(t *testing.T) { if err != nil { t.Fatalf("encode route: %v", err) } + if raw[0] != catalogRouteCodecVersionV1 { + t.Fatalf("zero-M2 nil-end route encoded version = %d, want v1", raw[0]) + } got, err := DecodeRouteDescriptor(raw) if err != nil { t.Fatalf("decode route: %v", err) @@ -111,34 +117,107 @@ func TestRouteDescriptorCodecRejectsTrailingBytes(t *testing.T) { } } -func TestRouteDescriptorCodecAcceptsForwardVersionTail(t *testing.T) { +func TestRouteDescriptorCodecV2RoundTrip(t *testing.T) { route := RouteDescriptor{ - RouteID: 1, - Start: []byte("a"), - End: []byte("m"), - GroupID: 1, - State: RouteStateActive, - ParentRouteID: 0, + RouteID: 1, + Start: []byte("a"), + End: []byte("m"), + GroupID: 1, + State: RouteStateActive, + ParentRouteID: 0, + StagedVisibilityActive: true, + MigrationJobID: 42, + MinWriteTSExclusive: 99, } raw, err := EncodeRouteDescriptor(route) if err != nil { t.Fatalf("encode route: %v", err) } - raw[0] = catalogRouteCodecVersion + 1 - raw = append(raw, bytes.Repeat([]byte{0xee}, catalogUint64Bytes)...) + if raw[0] != catalogRouteCodecVersionV2 { + t.Fatalf("M2 route encoded version = %d, want v2", raw[0]) + } got, err := DecodeRouteDescriptor(raw) if err != nil { - t.Fatalf("decode forward route: %v", err) + t.Fatalf("decode v2 route: %v", err) } assertRouteEqual(t, route, got) } -func TestRouteDescriptorCodecAcceptsForwardVersionTailWithNilEnd(t *testing.T) { +func TestRouteDescriptorCodecV2RoundTripNilEnd(t *testing.T) { + route := RouteDescriptor{ + RouteID: 1, + Start: []byte("m"), + End: nil, + GroupID: 1, + State: RouteStateActive, + ParentRouteID: 0, + MinWriteTSExclusive: 123, + } + raw, err := EncodeRouteDescriptor(route) + if err != nil { + t.Fatalf("encode route: %v", err) + } + if raw[0] != catalogRouteCodecVersionV2 { + t.Fatalf("M2 nil-end route encoded version = %d, want v2", raw[0]) + } + + got, err := DecodeRouteDescriptor(raw) + if err != nil { + t.Fatalf("decode v2 nil-end route: %v", err) + } + assertRouteEqual(t, route, got) +} + +func TestRouteDescriptorCodecRejectsV2TrailingBytes(t *testing.T) { + route := RouteDescriptor{ + RouteID: 1, + Start: []byte("a"), + End: []byte("m"), + GroupID: 1, + State: RouteStateActive, + StagedVisibilityActive: true, + MigrationJobID: 42, + } + raw, err := EncodeRouteDescriptor(route) + if err != nil { + t.Fatalf("encode route: %v", err) + } + raw = append(raw, 0xff) + + _, err = DecodeRouteDescriptor(raw) + if !errors.Is(err, ErrCatalogInvalidRouteRecord) { + t.Fatalf("expected ErrCatalogInvalidRouteRecord, got %v", err) + } +} + +func TestRouteDescriptorCodecRejectsTruncatedV2Tail(t *testing.T) { + route := RouteDescriptor{ + RouteID: 1, + Start: []byte("a"), + End: nil, + GroupID: 1, + State: RouteStateActive, + StagedVisibilityActive: true, + MigrationJobID: 42, + } + raw, err := EncodeRouteDescriptor(route) + if err != nil { + t.Fatalf("encode route: %v", err) + } + raw = raw[:len(raw)-1] + + _, err = DecodeRouteDescriptor(raw) + if !errors.Is(err, ErrCatalogInvalidRouteRecord) { + t.Fatalf("expected ErrCatalogInvalidRouteRecord, got %v", err) + } +} + +func TestRouteDescriptorCodecRejectsUnknownVersion(t *testing.T) { route := RouteDescriptor{ RouteID: 1, - Start: []byte("m"), - End: nil, + Start: []byte("a"), + End: []byte("m"), GroupID: 1, State: RouteStateActive, ParentRouteID: 0, @@ -148,13 +227,11 @@ func TestRouteDescriptorCodecAcceptsForwardVersionTailWithNilEnd(t *testing.T) { t.Fatalf("encode route: %v", err) } raw[0] = catalogRouteCodecVersion + 1 - raw = append(raw, bytes.Repeat([]byte{0xee}, catalogUint64Bytes)...) - got, err := DecodeRouteDescriptor(raw) - if err != nil { - t.Fatalf("decode forward route: %v", err) + _, err = DecodeRouteDescriptor(raw) + if !errors.Is(err, ErrCatalogInvalidRouteRecord) { + t.Fatalf("expected ErrCatalogInvalidRouteRecord, got %v", err) } - assertRouteEqual(t, route, got) } func TestRouteDescriptorCodecRejectsBelowMinimumVersion(t *testing.T) { @@ -178,6 +255,40 @@ func TestRouteDescriptorCodecRejectsBelowMinimumVersion(t *testing.T) { } } +func TestRouteDescriptorHelpersIncludeMigrationFields(t *testing.T) { + route := RouteDescriptor{ + RouteID: 1, + Start: []byte("a"), + End: []byte("m"), + GroupID: 1, + State: RouteStateActive, + StagedVisibilityActive: true, + MigrationJobID: 42, + MinWriteTSExclusive: 99, + } + cloned := CloneRouteDescriptor(route) + assertRouteEqual(t, route, cloned) + + withoutFloor := cloned + withoutFloor.MinWriteTSExclusive = 0 + if routeDescriptorEqual(route, withoutFloor) { + t.Fatal("routeDescriptorEqual must compare MinWriteTSExclusive") + } + + withoutJob := cloned + withoutJob.MigrationJobID = 100 + if routeDescriptorEqual(route, withoutJob) { + t.Fatal("routeDescriptorEqual must compare MigrationJobID") + } + + withoutStaged := cloned + withoutStaged.StagedVisibilityActive = false + withoutStaged.MigrationJobID = 0 + if routeDescriptorEqual(route, withoutStaged) { + t.Fatal("routeDescriptorEqual must compare StagedVisibilityActive") + } +} + func TestCatalogRouteKeyHelpers(t *testing.T) { key := CatalogRouteKey(11) if !IsCatalogRouteKey(key) { @@ -555,6 +666,53 @@ func TestCatalogStoreSaveDoesNotRewriteUnchangedRoutes(t *testing.T) { } } +func TestCatalogStoreSaveKeepsMinWriteTSExclusiveMonotone(t *testing.T) { + st := store.NewMVCCStore() + cs := NewCatalogStore(st, WithCatalogRouteDescriptorV2Writes(true)) + ctx := context.Background() + + first, err := cs.Save(ctx, 0, []RouteDescriptor{ + {RouteID: 1, Start: []byte(""), End: nil, GroupID: 1, State: RouteStateActive, MinWriteTSExclusive: 80}, + }) + if err != nil { + t.Fatalf("first save: %v", err) + } + + second, err := cs.Save(ctx, first.Version, []RouteDescriptor{ + {RouteID: 1, Start: []byte(""), End: nil, GroupID: 2, State: RouteStateActive, MinWriteTSExclusive: 10}, + }) + if err != nil { + t.Fatalf("second save: %v", err) + } + if second.Routes[0].GroupID != 2 { + t.Fatalf("expected rewritten group 2, got %d", second.Routes[0].GroupID) + } + if second.Routes[0].MinWriteTSExclusive != 80 { + t.Fatalf("expected returned floor 80, got %d", second.Routes[0].MinWriteTSExclusive) + } + + snapshot, err := cs.Snapshot(ctx) + if err != nil { + t.Fatalf("snapshot: %v", err) + } + if snapshot.Routes[0].MinWriteTSExclusive != 80 { + t.Fatalf("expected durable floor 80, got %d", snapshot.Routes[0].MinWriteTSExclusive) + } +} + +func TestCatalogStoreSaveRejectsRouteDescriptorV2WritesWhenDisabled(t *testing.T) { + st := store.NewMVCCStore() + cs := NewCatalogStore(st) + ctx := context.Background() + + _, err := cs.Save(ctx, 0, []RouteDescriptor{ + {RouteID: 1, Start: []byte(""), End: nil, GroupID: 1, State: RouteStateActive, MinWriteTSExclusive: 80}, + }) + if !errors.Is(err, ErrCatalogRouteV2WriteDisabled) { + t.Fatalf("expected ErrCatalogRouteV2WriteDisabled, got %v", err) + } +} + func TestCatalogStoreSaveRejectsVersionOverflow(t *testing.T) { st := store.NewMVCCStore() ctx := context.Background() @@ -611,7 +769,7 @@ func TestCatalogStoreApplySaveMutations_UsesMonotonicCommitTS(t *testing.T) { t.Fatalf("advance LastCommitTS: %v", err) } - mutations, err := cs.buildSaveMutations(ctx, plan) + mutations, err := cs.buildSaveMutations(ctx, &plan) if err != nil { t.Fatalf("buildSaveMutations: %v", err) } @@ -642,6 +800,15 @@ func assertRouteEqual(t *testing.T, want, got RouteDescriptor) { if want.ParentRouteID != got.ParentRouteID { t.Fatalf("parent route id mismatch: want %d, got %d", want.ParentRouteID, got.ParentRouteID) } + if want.StagedVisibilityActive != got.StagedVisibilityActive { + t.Fatalf("staged visibility mismatch: want %v, got %v", want.StagedVisibilityActive, got.StagedVisibilityActive) + } + if want.MigrationJobID != got.MigrationJobID { + t.Fatalf("migration job id mismatch: want %d, got %d", want.MigrationJobID, got.MigrationJobID) + } + if want.MinWriteTSExclusive != got.MinWriteTSExclusive { + t.Fatalf("min write ts mismatch: want %d, got %d", want.MinWriteTSExclusive, got.MinWriteTSExclusive) + } if want.State != got.State { t.Fatalf("state mismatch: want %d, got %d", want.State, got.State) } diff --git a/distribution/engine.go b/distribution/engine.go index bc6613894..f981c1fed 100644 --- a/distribution/engine.go +++ b/distribution/engine.go @@ -25,6 +25,12 @@ type Route struct { GroupID uint64 // State tracks control-plane state for this route. State RouteState + // StagedVisibilityActive allows serving reads to merge staged migration rows. + StagedVisibilityActive bool + // MigrationJobID identifies the active staged migration job. + MigrationJobID uint64 + // MinWriteTSExclusive rejects writes at or below the migration cutover floor. + MinWriteTSExclusive uint64 // Load tracks the number of accesses served by this range. Load uint64 } @@ -341,12 +347,15 @@ func (e *Engine) Stats() []Route { stats := make([]Route, len(e.routes)) for i, r := range e.routes { stats[i] = Route{ - RouteID: r.RouteID, - Start: CloneBytes(r.Start), - End: CloneBytes(r.End), - GroupID: r.GroupID, - State: r.State, - Load: r.Load, + RouteID: r.RouteID, + Start: CloneBytes(r.Start), + End: CloneBytes(r.End), + GroupID: r.GroupID, + State: r.State, + StagedVisibilityActive: r.StagedVisibilityActive, + MigrationJobID: r.MigrationJobID, + MinWriteTSExclusive: r.MinWriteTSExclusive, + Load: r.Load, } } return stats @@ -374,12 +383,15 @@ func (e *Engine) GetIntersectingRoutes(start, end []byte) []Route { } // Route intersects with scan range result = append(result, Route{ - RouteID: r.RouteID, - Start: CloneBytes(r.Start), - End: CloneBytes(r.End), - GroupID: r.GroupID, - State: r.State, - Load: r.Load, + RouteID: r.RouteID, + Start: CloneBytes(r.Start), + End: CloneBytes(r.End), + GroupID: r.GroupID, + State: r.State, + StagedVisibilityActive: r.StagedVisibilityActive, + MigrationJobID: r.MigrationJobID, + MinWriteTSExclusive: r.MinWriteTSExclusive, + Load: r.Load, }) } return result @@ -418,12 +430,15 @@ func routesFromCatalog(routes []RouteDescriptor) ([]Route, error) { } seen[rd.RouteID] = struct{}{} out[i] = Route{ - RouteID: rd.RouteID, - Start: CloneBytes(rd.Start), - End: CloneBytes(rd.End), - GroupID: rd.GroupID, - State: rd.State, - Load: 0, + RouteID: rd.RouteID, + Start: CloneBytes(rd.Start), + End: CloneBytes(rd.End), + GroupID: rd.GroupID, + State: rd.State, + StagedVisibilityActive: rd.StagedVisibilityActive, + MigrationJobID: rd.MigrationJobID, + MinWriteTSExclusive: rd.MinWriteTSExclusive, + Load: 0, } } diff --git a/distribution/engine_test.go b/distribution/engine_test.go index 346e4fa2d..c464ba3fd 100644 --- a/distribution/engine_test.go +++ b/distribution/engine_test.go @@ -86,6 +86,61 @@ func TestNewEngineWithDefaultRoute(t *testing.T) { } } +func TestEngineApplySnapshot_PreservesMigrationRouteFields(t *testing.T) { + t.Parallel() + + e := NewEngine() + err := e.ApplySnapshot(CatalogSnapshot{ + Version: 1, + Routes: []RouteDescriptor{ + { + RouteID: 7, + Start: []byte("a"), + End: []byte("z"), + GroupID: 2, + State: RouteStateMigratingTarget, + StagedVisibilityActive: true, + MigrationJobID: 42, + MinWriteTSExclusive: 99, + }, + }, + }) + if err != nil { + t.Fatalf("ApplySnapshot: %v", err) + } + + route, ok := e.GetRoute([]byte("m")) + if !ok { + t.Fatal("expected route") + } + requireMigrationRouteFields(t, "GetRoute", route) + + stats := e.Stats() + if len(stats) != 1 { + t.Fatalf("expected 1 stat route, got %d", len(stats)) + } + requireMigrationRouteFields(t, "Stats", stats[0]) + + intersections := e.GetIntersectingRoutes([]byte("b"), []byte("c")) + if len(intersections) != 1 { + t.Fatalf("expected 1 intersecting route, got %d", len(intersections)) + } + requireMigrationRouteFields(t, "GetIntersectingRoutes", intersections[0]) +} + +func requireMigrationRouteFields(t *testing.T, label string, route Route) { + t.Helper() + if !route.StagedVisibilityActive { + t.Fatalf("%s lost staged visibility: %+v", label, route) + } + if route.MigrationJobID != 42 { + t.Fatalf("%s lost migration job id: %+v", label, route) + } + if route.MinWriteTSExclusive != 99 { + t.Fatalf("%s lost min write ts: %+v", label, route) + } +} + func TestEngineGetIntersectingRoutes(t *testing.T) { e := NewEngine() e.UpdateRoute([]byte("a"), []byte("m"), 1) diff --git a/kv/leader_routed_store.go b/kv/leader_routed_store.go index d484431a1..4d6631e4c 100644 --- a/kv/leader_routed_store.go +++ b/kv/leader_routed_store.go @@ -71,6 +71,10 @@ func (s *LeaderRoutedStore) leaderAddrForKey(key []byte) string { } func (s *LeaderRoutedStore) proxyRawGet(ctx context.Context, key []byte, ts uint64) ([]byte, error) { + return s.proxyRawGetWithReadFence(ctx, key, ts, 0) +} + +func (s *LeaderRoutedStore) proxyRawGetWithReadFence(ctx context.Context, key []byte, ts uint64, readRouteVersion uint64) ([]byte, error) { addr := s.leaderAddrForKey(key) if addr == "" { return nil, errors.WithStack(ErrLeaderNotFound) @@ -82,7 +86,7 @@ func (s *LeaderRoutedStore) proxyRawGet(ctx context.Context, key []byte, ts uint } cli := pb.NewRawKVClient(conn) - resp, err := cli.RawGet(ctx, &pb.RawGetRequest{Key: key, Ts: ts}) + resp, err := cli.RawGet(ctx, &pb.RawGetRequest{Key: key, Ts: ts, ReadRouteVersion: readRouteVersion}) if err != nil { return nil, errors.WithStack(err) } @@ -94,7 +98,26 @@ func (s *LeaderRoutedStore) proxyRawGet(ctx context.Context, key []byte, ts uint return resp.Value, nil } +func (s *LeaderRoutedStore) GetAtWithReadFence(ctx context.Context, key []byte, ts uint64, groupID uint64, readRouteVersion uint64) ([]byte, error) { + if s == nil || s.local == nil { + return nil, store.ErrKeyNotFound + } + if groupID != 0 { + return nil, store.ErrNotSupported + } + ok, fenceTS := s.leaderFenceTS(ctx, key) + if ok { + val, err := s.local.GetAt(ctx, key, max(ts, fenceTS)) + return val, errors.WithStack(err) + } + return s.proxyRawGetWithReadFence(ctx, key, ts, readRouteVersion) +} + func (s *LeaderRoutedStore) proxyRawLatestCommitTS(ctx context.Context, key []byte) (uint64, bool, error) { + return s.proxyRawLatestCommitTSWithReadFence(ctx, key, 0) +} + +func (s *LeaderRoutedStore) proxyRawLatestCommitTSWithReadFence(ctx context.Context, key []byte, readRouteVersion uint64) (uint64, bool, error) { addr := s.leaderAddrForKey(key) if addr == "" { return 0, false, errors.WithStack(ErrLeaderNotFound) @@ -106,13 +129,24 @@ func (s *LeaderRoutedStore) proxyRawLatestCommitTS(ctx context.Context, key []by } cli := pb.NewRawKVClient(conn) - resp, err := cli.RawLatestCommitTS(ctx, &pb.RawLatestCommitTSRequest{Key: key}) + resp, err := cli.RawLatestCommitTS(ctx, &pb.RawLatestCommitTSRequest{Key: key, ReadRouteVersion: readRouteVersion}) if err != nil { return 0, false, errors.WithStack(err) } return resp.Ts, resp.Exists, nil } +func (s *LeaderRoutedStore) LatestCommitTSWithReadFence(ctx context.Context, key []byte, readRouteVersion uint64) (uint64, bool, error) { + if s == nil || s.local == nil { + return 0, false, nil + } + if s.leaderOKForKey(ctx, key) { + ts, exists, err := s.local.LatestCommitTS(ctx, key) + return ts, exists, errors.WithStack(err) + } + return s.proxyRawLatestCommitTSWithReadFence(ctx, key, readRouteVersion) +} + func (s *LeaderRoutedStore) proxyRawScanAt( ctx context.Context, start []byte, @@ -120,6 +154,20 @@ func (s *LeaderRoutedStore) proxyRawScanAt( limit int, ts uint64, reverse bool, +) ([]*store.KVPair, error) { + return s.proxyRawScanAtWithReadFence(ctx, start, end, limit, ts, reverse, 0, nil, nil) +} + +func (s *LeaderRoutedStore) proxyRawScanAtWithReadFence( + ctx context.Context, + start []byte, + end []byte, + limit int, + ts uint64, + reverse bool, + readRouteVersion uint64, + routeStart []byte, + routeEnd []byte, ) ([]*store.KVPair, error) { addr := s.leaderAddrForKey(start) if addr == "" { @@ -133,11 +181,15 @@ func (s *LeaderRoutedStore) proxyRawScanAt( cli := pb.NewRawKVClient(conn) resp, err := cli.RawScanAt(ctx, &pb.RawScanAtRequest{ - StartKey: start, - EndKey: end, - Limit: int64(limit), - Ts: ts, - Reverse: reverse, + StartKey: start, + EndKey: end, + Limit: int64(limit), + Ts: ts, + Reverse: reverse, + ReadRouteVersion: readRouteVersion, + RouteStart: bytes.Clone(routeStart), + RouteEnd: bytes.Clone(routeEnd), + RouteBoundsPresent: routeScanBoundsPresent(routeStart, routeEnd), }) if err != nil { return nil, errors.WithStack(err) @@ -153,6 +205,63 @@ func (s *LeaderRoutedStore) proxyRawScanAt( return out, nil } +func (s *LeaderRoutedStore) ScanAtWithReadFence(ctx context.Context, start []byte, end []byte, limit int, ts uint64, reverse bool, groupID uint64, readRouteVersion uint64, routeStart []byte, routeEnd []byte) ([]*store.KVPair, error) { + if s == nil || s.local == nil { + return []*store.KVPair{}, nil + } + if limit <= 0 { + return []*store.KVPair{}, nil + } + if groupID != 0 { + return nil, store.ErrNotSupported + } + ok, fenceTS := s.leaderFenceTS(ctx, start) + if !ok { + return s.proxyRawScanAtWithReadFence(ctx, start, end, limit, ts, reverse, readRouteVersion, routeStart, routeEnd) + } + readTS := max(ts, fenceTS) + if routeScanBoundsPresent(routeStart, routeEnd) { + return s.scanLocalRouteFilteredAt(ctx, start, end, limit, readTS, reverse, routeStart, routeEnd) + } + if reverse { + kvs, err := s.local.ReverseScanAt(ctx, start, end, limit, readTS) + return kvs, errors.WithStack(err) + } + kvs, err := s.local.ScanAt(ctx, start, end, limit, readTS) + return kvs, errors.WithStack(err) +} + +func (s *LeaderRoutedStore) scanLocalRouteFilteredAt(ctx context.Context, start []byte, end []byte, limit int, ts uint64, reverse bool, routeStart []byte, routeEnd []byte) ([]*store.KVPair, error) { + out := make([]*store.KVPair, 0, min(limit, routeFilteredScanBatchMin)) + scanStart := start + scanEnd := end + for len(out) < limit { + batchLimit := routeFilteredScanBatchLimit(limit - len(out)) + var ( + kvs []*store.KVPair + err error + ) + if reverse { + kvs, err = s.local.ReverseScanAt(ctx, scanStart, scanEnd, batchLimit, ts) + } else { + kvs, err = s.local.ScanAt(ctx, scanStart, scanEnd, batchLimit, ts) + } + if err != nil { + return nil, errors.WithStack(err) + } + out = appendRouteFilteredKVs(out, kvs, limit, routeStart, routeEnd) + if routeFilteredScanDone(kvs, batchLimit, len(out), limit) { + break + } + var done bool + scanStart, scanEnd, done = nextRouteFilteredScanWindow(kvs, scanStart, scanEnd, reverse) + if done { + break + } + } + return out, nil +} + func (s *LeaderRoutedStore) GetAt(ctx context.Context, key []byte, ts uint64) ([]byte, error) { if s == nil || s.local == nil { return nil, store.ErrKeyNotFound diff --git a/kv/leader_routed_store_test.go b/kv/leader_routed_store_test.go index 1d83ead89..56a7aab7e 100644 --- a/kv/leader_routed_store_test.go +++ b/kv/leader_routed_store_test.go @@ -88,32 +88,39 @@ type fakeRawKVServer struct { getResp *pb.RawGetResponse scanResp *pb.RawScanAtResponse latestResp *pb.RawLatestCommitTSResponse + + lastGetReq *pb.RawGetRequest + lastScanReq *pb.RawScanAtRequest + lastLatestReq *pb.RawLatestCommitTSRequest } -func (f *fakeRawKVServer) RawGet(context.Context, *pb.RawGetRequest) (*pb.RawGetResponse, error) { +func (f *fakeRawKVServer) RawGet(_ context.Context, req *pb.RawGetRequest) (*pb.RawGetResponse, error) { f.mu.Lock() defer f.mu.Unlock() f.getCalls++ + f.lastGetReq = req if f.getResp != nil { return f.getResp, nil } return &pb.RawGetResponse{}, nil } -func (f *fakeRawKVServer) RawScanAt(context.Context, *pb.RawScanAtRequest) (*pb.RawScanAtResponse, error) { +func (f *fakeRawKVServer) RawScanAt(_ context.Context, req *pb.RawScanAtRequest) (*pb.RawScanAtResponse, error) { f.mu.Lock() defer f.mu.Unlock() f.scanCalls++ + f.lastScanReq = req if f.scanResp != nil { return f.scanResp, nil } return &pb.RawScanAtResponse{}, nil } -func (f *fakeRawKVServer) RawLatestCommitTS(context.Context, *pb.RawLatestCommitTSRequest) (*pb.RawLatestCommitTSResponse, error) { +func (f *fakeRawKVServer) RawLatestCommitTS(_ context.Context, req *pb.RawLatestCommitTSRequest) (*pb.RawLatestCommitTSResponse, error) { f.mu.Lock() defer f.mu.Unlock() f.latestCalls++ + f.lastLatestReq = req if f.latestResp != nil { return f.latestResp, nil } @@ -169,6 +176,37 @@ func TestLeaderRoutedStore_UsesLocalStoreWhenLeaderVerified(t *testing.T) { require.Equal(t, uint64(10), ts) } +func TestLeaderRoutedStore_ScanAtWithReadFenceFiltersRouteBoundsLocally(t *testing.T) { + t.Parallel() + + ctx := context.Background() + local := store.NewMVCCStore() + rawPrefix := []byte("!redis|meta|") + left := []byte("!redis|meta|a") + right := []byte("!redis|meta|z") + require.NoError(t, local.PutAt(ctx, left, []byte("left"), 1, 0)) + require.NoError(t, local.PutAt(ctx, right, []byte("right"), 2, 0)) + + coord := &stubLeaderCoordinator{ + isLeader: true, + clock: NewHLC(), + } + s := NewLeaderRoutedStore(local, coord) + t.Cleanup(func() { _ = s.Close() }) + + kvs, err := s.ScanAtWithReadFence(ctx, rawPrefix, prefixScanEnd(rawPrefix), 1, 2, false, 0, 7, []byte("m"), nil) + require.NoError(t, err) + require.Len(t, kvs, 1) + require.Equal(t, right, kvs[0].Key) + require.Equal(t, []byte("right"), kvs[0].Value) + + kvs, err = s.ScanAtWithReadFence(ctx, rawPrefix, prefixScanEnd(rawPrefix), 1, 2, true, 0, 7, []byte{}, []byte("m")) + require.NoError(t, err) + require.Len(t, kvs, 1) + require.Equal(t, left, kvs[0].Key) + require.Equal(t, []byte("left"), kvs[0].Value) +} + func TestLeaderRoutedStore_PrefersLinearizableReadFence(t *testing.T) { t.Parallel() @@ -246,6 +284,48 @@ func TestLeaderRoutedStore_ProxiesReadsWhenFollower(t *testing.T) { require.Equal(t, 1, fake.latestCalls) } +func TestLeaderRoutedStore_ForwardsReadFenceStamps(t *testing.T) { + t.Parallel() + + fake := &fakeRawKVServer{ + getResp: &pb.RawGetResponse{ + Exists: true, + Value: []byte("remote-v"), + }, + scanResp: &pb.RawScanAtResponse{}, + latestResp: &pb.RawLatestCommitTSResponse{ + Ts: 42, + Exists: true, + }, + } + addr, stop := startRawKVServer(t, fake) + t.Cleanup(stop) + + coord := &stubLeaderCoordinator{ + isLeader: false, + leader: addr, + clock: NewHLC(), + } + s := NewLeaderRoutedStore(store.NewMVCCStore(), coord) + t.Cleanup(func() { _ = s.Close() }) + + ctx := context.Background() + _, err := s.GetAtWithReadFence(ctx, []byte("k"), 10, 0, 77) + require.NoError(t, err) + _, _, err = s.LatestCommitTSWithReadFence(ctx, []byte("k"), 78) + require.NoError(t, err) + _, err = s.ScanAtWithReadFence(ctx, []byte("a"), []byte("z"), 10, 11, false, 0, 79, []byte("a"), []byte("m")) + require.NoError(t, err) + + fake.mu.Lock() + defer fake.mu.Unlock() + require.Equal(t, uint64(77), fake.lastGetReq.GetReadRouteVersion()) + require.Equal(t, uint64(78), fake.lastLatestReq.GetReadRouteVersion()) + require.Equal(t, uint64(79), fake.lastScanReq.GetReadRouteVersion()) + require.Equal(t, []byte("a"), fake.lastScanReq.GetRouteStart()) + require.Equal(t, []byte("m"), fake.lastScanReq.GetRouteEnd()) +} + func TestLeaderRoutedStore_ReturnsLeaderNotFoundWhenNoLeaderAddr(t *testing.T) { t.Parallel() diff --git a/kv/shard_key.go b/kv/shard_key.go index 6871ce789..629318e5b 100644 --- a/kv/shard_key.go +++ b/kv/shard_key.go @@ -64,6 +64,9 @@ func normalizeRouteKey(key []byte) []byte { if user := redisRouteKey(key); user != nil { return user } + if user := redisWideColumnRouteKey(key); user != nil { + return user + } if table := dynamoRouteKey(key); table != nil { return table } @@ -79,6 +82,57 @@ func normalizeRouteKey(key []byte) []byte { return key } +func redisWideColumnRouteKey(key []byte) []byte { + if user := redisHashRouteKey(key); user != nil { + return user + } + if user := redisSetRouteKey(key); user != nil { + return user + } + return redisZSetRouteKey(key) +} + +func redisHashRouteKey(key []byte) []byte { + switch { + case store.IsHashMetaDeltaKey(key): + return store.ExtractHashUserKeyFromDelta(key) + case store.IsHashMetaKey(key): + return store.ExtractHashUserKeyFromMeta(key) + case store.IsHashFieldKey(key): + return store.ExtractHashUserKeyFromField(key) + default: + return nil + } +} + +func redisSetRouteKey(key []byte) []byte { + switch { + case store.IsSetMetaDeltaKey(key): + return store.ExtractSetUserKeyFromDelta(key) + case store.IsSetMetaKey(key): + return store.ExtractSetUserKeyFromMeta(key) + case store.IsSetMemberKey(key): + return store.ExtractSetUserKeyFromMember(key) + default: + return nil + } +} + +func redisZSetRouteKey(key []byte) []byte { + switch { + case store.IsZSetMetaDeltaKey(key): + return store.ExtractZSetUserKeyFromDelta(key) + case store.IsZSetMetaKey(key): + return store.ExtractZSetUserKeyFromMeta(key) + case store.IsZSetMemberKey(key): + return store.ExtractZSetUserKeyFromMember(key) + case store.IsZSetScoreKey(key): + return store.ExtractZSetUserKeyFromScore(key) + default: + return nil + } +} + func redisRouteKey(key []byte) []byte { if !bytes.HasPrefix(key, redisInternalRoutePrefixBytes) { return nil diff --git a/kv/shard_key_test.go b/kv/shard_key_test.go index 8257fbb77..560089397 100644 --- a/kv/shard_key_test.go +++ b/kv/shard_key_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/bootjp/elastickv/internal/s3keys" + "github.com/bootjp/elastickv/store" "github.com/stretchr/testify/require" ) @@ -43,6 +44,27 @@ func TestRouteKey_NormalizesRedisTxnWideFenceKeys(t *testing.T) { } } +func TestRouteKey_NormalizesRedisWideColumnKeys(t *testing.T) { + t.Parallel() + + userKey := []byte("user:key") + for _, raw := range [][]byte{ + store.HashMetaDeltaKey(userKey, 10, 0), + store.HashMetaKey(userKey), + store.HashFieldKey(userKey, []byte("field")), + store.SetMetaDeltaKey(userKey, 11, 0), + store.SetMetaKey(userKey), + store.SetMemberKey(userKey, []byte("member")), + store.ZSetMetaDeltaKey(userKey, 12, 0), + store.ZSetMetaKey(userKey), + store.ZSetMemberKey(userKey, []byte("member")), + store.ZSetScoreKey(userKey, 1.5, []byte("member")), + } { + require.Equal(t, userKey, routeKey(raw)) + require.Equal(t, userKey, routeKey(txnLockKey(raw))) + } +} + func TestRouteKey_NormalizesDynamoKeysToTable(t *testing.T) { t.Parallel() diff --git a/kv/shard_store.go b/kv/shard_store.go index bb0c15035..766ffb3a0 100644 --- a/kv/shard_store.go +++ b/kv/shard_store.go @@ -35,7 +35,21 @@ func NewShardStore(engine *distribution.Engine, groups map[uint64]*ShardGroup) * } } +func (s *ShardStore) ReadRouteVersion() uint64 { + if s == nil || s.engine == nil { + return 0 + } + return s.engine.Version() +} + func (s *ShardStore) GetAt(ctx context.Context, key []byte, ts uint64) ([]byte, error) { + return s.GetAtWithReadFence(ctx, key, ts, 0, 0) +} + +func (s *ShardStore) GetAtWithReadFence(ctx context.Context, key []byte, ts uint64, groupID uint64, readRouteVersion uint64) ([]byte, error) { + if groupID != 0 { + return s.getGroupAtWithReadFence(ctx, groupID, key, ts, readRouteVersion) + } g, ok := s.groupForKey(key) if !ok || g.Store == nil { return nil, store.ErrKeyNotFound @@ -50,13 +64,17 @@ func (s *ShardStore) GetAt(ctx context.Context, key []byte, ts uint64) ([]byte, if isLinearizableRaftLeader(ctx, engineForGroup(g)) { return s.leaderGetAt(ctx, g, key, ts) } - return s.proxyRawGet(ctx, g, key, ts, 0) + return s.proxyRawGet(ctx, g, key, ts, 0, readRouteVersion) } // GetGroupAt reads a key from the explicitly selected Raft group. // It is for keyspaces whose owner is resolved outside the byte-range // engine (for example SQS HT-FIFO's (queue, partition) resolver). func (s *ShardStore) GetGroupAt(ctx context.Context, groupID uint64, key []byte, ts uint64) ([]byte, error) { + return s.getGroupAtWithReadFence(ctx, groupID, key, ts, 0) +} + +func (s *ShardStore) getGroupAtWithReadFence(ctx context.Context, groupID uint64, key []byte, ts uint64, readRouteVersion uint64) ([]byte, error) { g, ok := s.groupForID(groupID) if !ok || g.Store == nil { return nil, store.ErrKeyNotFound @@ -68,7 +86,7 @@ func (s *ShardStore) GetGroupAt(ctx context.Context, groupID uint64, key []byte, if isLinearizableRaftLeader(ctx, engineForGroup(g)) { return s.leaderGetAt(ctx, g, key, ts) } - return s.proxyRawGet(ctx, g, key, ts, groupID) + return s.proxyRawGet(ctx, g, key, ts, groupID, readRouteVersion) } func isLinearizableRaftLeader(ctx context.Context, engine raftengine.LeaderView) bool { @@ -185,12 +203,36 @@ func tryEngineLinearizableFence(ctx context.Context, engine raftengine.LeaderVie // a best-effort point-in-time scan. Callers requiring cross-shard consistency // should use a transaction or implement a cross-shard snapshot fence. func (s *ShardStore) ScanAt(ctx context.Context, start []byte, end []byte, limit int, ts uint64) ([]*store.KVPair, error) { + return s.scanAtWithReadFence(ctx, start, end, limit, ts, 0, 0, nil, nil) +} + +func (s *ShardStore) ScanAtWithReadFence(ctx context.Context, start []byte, end []byte, limit int, ts uint64, reverse bool, groupID uint64, readRouteVersion uint64, routeStart []byte, routeEnd []byte) ([]*store.KVPair, error) { + if limit <= 0 { + return []*store.KVPair{}, nil + } + if reverse { + if groupID != 0 { + if routeScanBoundsPresent(routeStart, routeEnd) { + return s.scanRouteAtDirectionWithReadFence(ctx, distribution.Route{GroupID: groupID}, start, end, limit, ts, true, true, readRouteVersion, routeStart, routeEnd) + } + return nil, errors.WithStack(store.ErrNotSupported) + } + return s.reverseScanAtWithReadFence(ctx, start, end, limit, ts, readRouteVersion, routeStart, routeEnd) + } + return s.scanAtWithReadFence(ctx, start, end, limit, ts, groupID, readRouteVersion, routeStart, routeEnd) +} + +func (s *ShardStore) scanAtWithReadFence(ctx context.Context, start []byte, end []byte, limit int, ts uint64, groupID uint64, readRouteVersion uint64, routeStart []byte, routeEnd []byte) ([]*store.KVPair, error) { if limit <= 0 { return []*store.KVPair{}, nil } - routes, clampToRoutes := s.routesForScan(start, end) - out, err := s.scanRoutesAt(ctx, routes, start, end, limit, ts, clampToRoutes) + if groupID != 0 { + return s.scanRouteAtDirectionWithReadFence(ctx, distribution.Route{GroupID: groupID}, start, end, limit, ts, false, true, readRouteVersion, routeStart, routeEnd) + } + + routes, clampToRoutes := s.routesForFencedScan(start, end, routeStart, routeEnd) + out, err := s.scanRoutesAtWithReadFence(ctx, routes, start, end, limit, ts, clampToRoutes, readRouteVersion, routeStart, routeEnd) if err != nil { return nil, err } @@ -229,12 +271,16 @@ func (s *ShardStore) ScanGroupAt(ctx context.Context, groupID uint64, start []by } func (s *ShardStore) ReverseScanAt(ctx context.Context, start []byte, end []byte, limit int, ts uint64) ([]*store.KVPair, error) { + return s.reverseScanAtWithReadFence(ctx, start, end, limit, ts, 0, nil, nil) +} + +func (s *ShardStore) reverseScanAtWithReadFence(ctx context.Context, start []byte, end []byte, limit int, ts uint64, readRouteVersion uint64, routeStart []byte, routeEnd []byte) ([]*store.KVPair, error) { if limit <= 0 { return []*store.KVPair{}, nil } - routes, clampToRoutes := s.routesForScan(start, end) - out, err := s.reverseScanRoutesAt(ctx, routes, start, end, limit, ts, clampToRoutes) + routes, clampToRoutes := s.routesForFencedScan(start, end, routeStart, routeEnd) + out, err := s.reverseScanRoutesAtWithReadFence(ctx, routes, start, end, limit, ts, clampToRoutes, readRouteVersion, routeStart, routeEnd) if err != nil { return nil, err } @@ -281,17 +327,42 @@ func (s *ShardStore) routesForScan(start []byte, end []byte) ([]distribution.Rou return routes, true } -func (s *ShardStore) scanRoutesAt(ctx context.Context, routes []distribution.Route, start []byte, end []byte, limit int, ts uint64, clampToRoutes bool) ([]*store.KVPair, error) { +func (s *ShardStore) routesForFencedScan(start []byte, end []byte, routeStart []byte, routeEnd []byte) ([]distribution.Route, bool) { + if routeScanBoundsPresent(routeStart, routeEnd) { + return s.engine.GetIntersectingRoutes(routeStart, normalizedRouteScanEnd(routeEnd)), false + } + return s.routesForScan(start, end) +} + +func routeScanBoundsPresent(routeStart []byte, routeEnd []byte) bool { + return routeStart != nil || routeEnd != nil +} + +func normalizedRouteScanEnd(routeEnd []byte) []byte { + if len(routeEnd) == 0 { + return nil + } + return routeEnd +} + +func (s *ShardStore) scanRoutesAtWithReadFence(ctx context.Context, routes []distribution.Route, start []byte, end []byte, limit int, ts uint64, clampToRoutes bool, readRouteVersion uint64, routeStart []byte, routeEnd []byte) ([]*store.KVPair, error) { out := make([]*store.KVPair, 0) + seenGroups := make(map[uint64]struct{}) + routeFilterPresent := routeScanBoundsPresent(routeStart, routeEnd) for _, route := range routes { scanStart := start scanEnd := end if clampToRoutes { scanStart = clampScanStart(start, route.Start) scanEnd = clampScanEnd(end, route.End) + } else if !routeFilterPresent { + if _, seen := seenGroups[route.GroupID]; seen { + continue + } + seenGroups[route.GroupID] = struct{}{} } - kvs, err := s.scanRouteAtDirection(ctx, route, scanStart, scanEnd, limit, ts, false, false) + kvs, err := s.scanRouteAtDirectionWithReadFence(ctx, route, scanStart, scanEnd, limit, ts, false, false, readRouteVersion, routeStart, routeEnd) if err != nil { return nil, err } @@ -308,7 +379,7 @@ func (s *ShardStore) scanRoutesAt(ctx context.Context, routes []distribution.Rou return out, nil } -func (s *ShardStore) reverseScanRoutesAt( +func (s *ShardStore) reverseScanRoutesAtWithReadFence( ctx context.Context, routes []distribution.Route, start []byte, @@ -316,13 +387,17 @@ func (s *ShardStore) reverseScanRoutesAt( limit int, ts uint64, clampToRoutes bool, + readRouteVersion uint64, + routeStart []byte, + routeEnd []byte, ) ([]*store.KVPair, error) { out := make([]*store.KVPair, 0) seenGroups := make(map[uint64]struct{}) + routeFilterPresent := routeScanBoundsPresent(routeStart, routeEnd) for i := len(routes) - 1; i >= 0; i-- { route := routes[i] if clampToRoutes { - kvs, done, err := s.clampedReverseScanRouteAt(ctx, route, start, end, limit, len(out), ts) + kvs, done, err := s.clampedReverseScanRouteAtWithReadFence(ctx, route, start, end, limit, len(out), ts, readRouteVersion, routeStart, routeEnd) if err != nil { return nil, err } @@ -338,12 +413,15 @@ func (s *ShardStore) reverseScanRoutesAt( // Fetch up to limit from every route and merge+sort descending so the // result honours the ReverseScanAt contract. // De-duplicate by GroupID: after a range split both halves share the same - // GroupID (same backing shard store), so only scan each group once. - if _, seen := seenGroups[route.GroupID]; seen { - continue + // GroupID (same backing shard store), so only scan each group once unless + // route filters make each descriptor's logical interval distinct. + if !routeFilterPresent { + if _, seen := seenGroups[route.GroupID]; seen { + continue + } + seenGroups[route.GroupID] = struct{}{} } - seenGroups[route.GroupID] = struct{}{} - kvs, err := s.scanRouteAtDirection(ctx, route, start, end, limit, ts, true, false) + kvs, err := s.scanRouteAtDirectionWithReadFence(ctx, route, start, end, limit, ts, true, false, readRouteVersion, routeStart, routeEnd) if err != nil { return nil, err } @@ -352,7 +430,7 @@ func (s *ShardStore) reverseScanRoutesAt( return out, nil } -func (s *ShardStore) clampedReverseScanRouteAt( +func (s *ShardStore) clampedReverseScanRouteAtWithReadFence( ctx context.Context, route distribution.Route, start []byte, @@ -360,6 +438,9 @@ func (s *ShardStore) clampedReverseScanRouteAt( limit int, currentLen int, ts uint64, + readRouteVersion uint64, + routeStart []byte, + routeEnd []byte, ) ([]*store.KVPair, bool, error) { if currentLen >= limit { return nil, true, nil @@ -367,7 +448,7 @@ func (s *ShardStore) clampedReverseScanRouteAt( scanStart := clampScanStart(start, route.Start) scanEnd := clampScanEnd(end, route.End) - kvs, err := s.scanRouteAtDirection(ctx, route, scanStart, scanEnd, limit-currentLen, ts, true, false) + kvs, err := s.scanRouteAtDirectionWithReadFence(ctx, route, scanStart, scanEnd, limit-currentLen, ts, true, false, readRouteVersion, routeStart, routeEnd) if err != nil { return nil, false, err } @@ -383,6 +464,143 @@ func (s *ShardStore) scanRouteAtDirection( ts uint64, reverse bool, explicitGroup bool, +) ([]*store.KVPair, error) { + return s.scanRouteAtDirectionWithReadFence(ctx, route, start, end, limit, ts, reverse, explicitGroup, 0, nil, nil) +} + +func (s *ShardStore) scanRouteAtDirectionWithReadFence( + ctx context.Context, + route distribution.Route, + start []byte, + end []byte, + limit int, + ts uint64, + reverse bool, + explicitGroup bool, + readRouteVersion uint64, + routeStart []byte, + routeEnd []byte, +) ([]*store.KVPair, error) { + if routeScanBoundsPresent(routeStart, routeEnd) { + return s.scanRouteAtDirectionWithReadFenceRouteFilter(ctx, route, start, end, limit, ts, reverse, explicitGroup, readRouteVersion, routeStart, routeEnd) + } + return s.scanRouteAtDirectionWithReadFenceOnce(ctx, route, start, end, limit, ts, reverse, explicitGroup, readRouteVersion, routeStart, routeEnd) +} + +func (s *ShardStore) scanRouteAtDirectionWithReadFenceRouteFilter( + ctx context.Context, + route distribution.Route, + start []byte, + end []byte, + limit int, + ts uint64, + reverse bool, + explicitGroup bool, + readRouteVersion uint64, + routeStart []byte, + routeEnd []byte, +) ([]*store.KVPair, error) { + filterStart, filterEnd, empty := routeScanBoundsForRoute(route, routeStart, routeEnd) + if empty { + return []*store.KVPair{}, nil + } + out := make([]*store.KVPair, 0, min(limit, routeFilteredScanBatchMin)) + scanStart := start + scanEnd := end + for len(out) < limit { + remaining := limit - len(out) + batchLimit := routeFilteredScanBatchLimit(remaining) + kvs, cursorKVs, err := s.scanRouteAtDirectionWithReadFenceRouteFilterPage(ctx, route, scanStart, scanEnd, batchLimit, remaining, ts, reverse, explicitGroup, readRouteVersion, filterStart, filterEnd) + if err != nil { + return nil, err + } + out = appendRouteFilteredKVs(out, kvs, limit, filterStart, filterEnd) + if routeFilteredScanDone(cursorKVs, batchLimit, len(out), limit) { + break + } + var done bool + scanStart, scanEnd, done = nextRouteFilteredScanWindow(cursorKVs, scanStart, scanEnd, reverse) + if done { + break + } + } + return out, nil +} + +func routeScanBoundsForRoute(route distribution.Route, routeStart []byte, routeEnd []byte) ([]byte, []byte, bool) { + start := routeStart + if len(route.Start) > 0 && (len(start) == 0 || bytes.Compare(route.Start, start) > 0) { + start = route.Start + } + end := routeEnd + if len(route.End) > 0 && (len(end) == 0 || bytes.Compare(route.End, end) < 0) { + end = route.End + } + if len(start) > 0 && len(end) > 0 && bytes.Compare(start, end) >= 0 { + return start, end, true + } + return start, end, false +} + +func (s *ShardStore) scanRouteAtDirectionWithReadFenceRouteFilterPage( + ctx context.Context, + route distribution.Route, + start []byte, + end []byte, + limit int, + visibleLimit int, + ts uint64, + reverse bool, + explicitGroup bool, + readRouteVersion uint64, + routeStart []byte, + routeEnd []byte, +) ([]*store.KVPair, []*store.KVPair, error) { + g, ok := s.groupForID(route.GroupID) + if !ok || g == nil || g.Store == nil { + return nil, nil, nil + } + + if engineForGroup(g) == nil { + kvs, err := s.scanRouteLocal(ctx, g, start, end, limit, ts, reverse) + if err != nil { + return nil, nil, errors.WithStack(err) + } + return filterTxnInternalKVs(kvs), kvs, nil + } + + if isLinearizableRaftLeader(ctx, engineForGroup(g)) { + return s.scanRouteAtLeaderRouteFilter(ctx, g, start, end, limit, visibleLimit, ts, reverse, routeStart, routeEnd) + } + + groupID := proxyScanGroupID(route, explicitGroup, routeStart, routeEnd) + kvs, err := s.proxyRawScanAt(ctx, g, start, end, limit, ts, reverse, groupID, readRouteVersion, routeStart, routeEnd) + if err != nil { + return nil, nil, err + } + filtered := filterTxnInternalKVs(kvs) + return filtered, kvs, nil +} + +func proxyScanGroupID(route distribution.Route, explicitGroup bool, routeStart []byte, routeEnd []byte) uint64 { + if explicitGroup || routeScanBoundsPresent(routeStart, routeEnd) { + return route.GroupID + } + return 0 +} + +func (s *ShardStore) scanRouteAtDirectionWithReadFenceOnce( + ctx context.Context, + route distribution.Route, + start []byte, + end []byte, + limit int, + ts uint64, + reverse bool, + explicitGroup bool, + readRouteVersion uint64, + routeStart []byte, + routeEnd []byte, ) ([]*store.KVPair, error) { g, ok := s.groupForID(route.GroupID) if !ok || g == nil || g.Store == nil { @@ -401,11 +619,8 @@ func (s *ShardStore) scanRouteAtDirection( return s.scanRouteAtLeader(ctx, g, start, end, limit, ts, reverse) } - var groupID uint64 - if explicitGroup { - groupID = route.GroupID - } - kvs, err := s.proxyRawScanAt(ctx, g, start, end, limit, ts, reverse, groupID) + groupID := proxyScanGroupID(route, explicitGroup, routeStart, routeEnd) + kvs, err := s.proxyRawScanAt(ctx, g, start, end, limit, ts, reverse, groupID, readRouteVersion, routeStart, routeEnd) if err != nil { return nil, err } @@ -414,6 +629,98 @@ func (s *ShardStore) scanRouteAtDirection( return filterTxnInternalKVs(kvs), nil } +const routeFilteredScanBatchMin = 128 + +func routeFilteredScanBatchLimit(remaining int) int { + if remaining <= 0 { + return 0 + } + limit := remaining + if limit < routeFilteredScanBatchMin { + limit = routeFilteredScanBatchMin + } + if maxLimit := store.MaxDeltaScanLimit + 1; limit > maxLimit { + limit = maxLimit + } + return limit +} + +func routeFilteredScanDone(kvs []*store.KVPair, batchLimit int, outLen int, limit int) bool { + return len(kvs) == 0 || outLen >= limit || len(kvs) < batchLimit +} + +func nextRouteFilteredScanWindow(kvs []*store.KVPair, scanStart []byte, scanEnd []byte, reverse bool) ([]byte, []byte, bool) { + lastKey := kvs[len(kvs)-1].Key + if reverse { + scanEnd = lastKey + done := len(scanEnd) == 0 || (scanStart != nil && bytes.Compare(scanEnd, scanStart) <= 0) + return scanStart, scanEnd, done + } + scanStart = nextScanCursor(lastKey) + done := scanEnd != nil && bytes.Compare(scanStart, scanEnd) >= 0 + return scanStart, scanEnd, done +} + +func appendRouteFilteredKVs(out []*store.KVPair, kvs []*store.KVPair, limit int, routeStart []byte, routeEnd []byte) []*store.KVPair { + for _, kvp := range kvs { + if len(out) >= limit { + break + } + if kvp == nil || !routeKeyInScanBounds(kvp.Key, routeStart, routeEnd) { + continue + } + out = append(out, kvp) + } + return out +} + +func scanRouteFilteredLockBounds(kvs []*store.KVPair, filteredKVs []*store.KVPair, scanStart []byte, scanEnd []byte, pageLimit int, visibleLimit int, reverse bool) ([]byte, []byte) { + lockStart, lockEnd := scanLockBoundsForKVsDirection(filteredKVs, scanStart, scanEnd, visibleLimit, reverse) + pageStart, pageEnd := scanLockBoundsForKVsDirection(kvs, scanStart, scanEnd, pageLimit, reverse) + return intersectScanBounds(lockStart, lockEnd, pageStart, pageEnd) +} + +func intersectScanBounds(aStart []byte, aEnd []byte, bStart []byte, bEnd []byte) ([]byte, []byte) { + return maxScanStart(aStart, bStart), minScanEnd(aEnd, bEnd) +} + +func maxScanStart(a []byte, b []byte) []byte { + if a == nil { + return b + } + if b == nil { + return a + } + if bytes.Compare(a, b) >= 0 { + return a + } + return b +} + +func minScanEnd(a []byte, b []byte) []byte { + if a == nil { + return b + } + if b == nil { + return a + } + if bytes.Compare(a, b) <= 0 { + return a + } + return b +} + +func routeKeyInScanBounds(key []byte, routeStart []byte, routeEnd []byte) bool { + key = routeKey(key) + if len(routeStart) > 0 && bytes.Compare(key, routeStart) < 0 { + return false + } + if len(routeEnd) > 0 && bytes.Compare(key, routeEnd) >= 0 { + return false + } + return true +} + type physicalLimitedStore interface { ScanAtPhysicalLimit(ctx context.Context, start []byte, end []byte, visibleLimit, physicalLimit int, ts uint64) ([]*store.KVPair, bool, error) ReverseScanAtPhysicalLimit(ctx context.Context, start []byte, end []byte, visibleLimit, physicalLimit int, ts uint64) ([]*store.KVPair, bool, error) @@ -522,7 +829,7 @@ func (s *ShardStore) scanRouteAtLeaderPhysicalLimit( if err != nil { return nil, limitReached, errors.WithStack(err) } - lockStart, lockEnd := scanLockBoundsForKVs(kvs, start, end, visibleLimit) + lockStart, lockEnd := scanLockBoundsForKVsDirection(kvs, start, end, visibleLimit, reverse) lockKVs, err := scanTxnLockRangeAt(ctx, g, lockStart, lockEnd, ts, visibleLimit) if err != nil { return nil, limitReached, err @@ -560,14 +867,72 @@ func (s *ShardStore) scanRouteAtLeader( return s.resolveScanLocks(ctx, g, kvs, lockKVs, ts) } +func (s *ShardStore) scanRouteAtLeaderRouteFilter( + ctx context.Context, + g *ShardGroup, + start []byte, + end []byte, + limit int, + visibleLimit int, + ts uint64, + reverse bool, + routeStart []byte, + routeEnd []byte, +) ([]*store.KVPair, []*store.KVPair, error) { + var ( + kvs []*store.KVPair + err error + ) + if reverse { + kvs, err = g.Store.ReverseScanAt(ctx, start, end, limit, ts) + } else { + kvs, err = g.Store.ScanAt(ctx, start, end, limit, ts) + } + if err != nil { + return nil, nil, errors.WithStack(err) + } + filteredKVs := filterRouteScanKVs(kvs, routeStart, routeEnd) + lockStart, lockEnd := scanRouteFilteredLockBounds(kvs, filteredKVs, start, end, limit, visibleLimit, reverse) + lockKVs, err := scanTxnLockRangeAtWithRouteFilter(ctx, g, lockStart, lockEnd, ts, visibleLimit, routeStart, routeEnd) + if err != nil { + return nil, nil, err + } + resolved, err := s.resolveScanLocks(ctx, g, filteredKVs, lockKVs, ts) + return resolved, kvs, err +} + +func filterRouteScanKVs(kvs []*store.KVPair, routeStart []byte, routeEnd []byte) []*store.KVPair { + if len(kvs) == 0 { + return kvs + } + out := make([]*store.KVPair, 0, len(kvs)) + for _, kvp := range kvs { + if kvp == nil || !routeKeyInScanBounds(kvp.Key, routeStart, routeEnd) { + continue + } + out = append(out, kvp) + } + return out +} + func scanLockBoundsForKVs(kvs []*store.KVPair, scanStart []byte, scanEnd []byte, limit int) ([]byte, []byte) { + return scanLockBoundsForKVsDirection(kvs, scanStart, scanEnd, limit, false) +} + +func scanLockBoundsForKVsDirection(kvs []*store.KVPair, scanStart []byte, scanEnd []byte, limit int, reverse bool) ([]byte, []byte) { if countNonInternalKVs(kvs) < limit { return scanStart, scanEnd } - _, lastUserKey, ok := observedScanUserBounds(kvs) + firstUserKey, lastUserKey, ok := observedScanUserBounds(kvs) if !ok { return scanStart, scanEnd } + if reverse { + if len(scanStart) == 0 || bytes.Compare(firstUserKey, scanStart) > 0 { + scanStart = firstUserKey + } + return scanStart, scanEnd + } bound := nextScanCursor(lastUserKey) if scanEnd == nil || bytes.Compare(bound, scanEnd) < 0 { scanEnd = bound @@ -708,6 +1073,10 @@ func (s *ShardStore) ExpireAt(ctx context.Context, key []byte, expireAt uint64, } func (s *ShardStore) LatestCommitTS(ctx context.Context, key []byte) (uint64, bool, error) { + return s.LatestCommitTSWithReadFence(ctx, key, 0) +} + +func (s *ShardStore) LatestCommitTSWithReadFence(ctx context.Context, key []byte, readRouteVersion uint64) (uint64, bool, error) { g, ok := s.groupForKey(key) if !ok || g.Store == nil { return 0, false, nil @@ -734,10 +1103,10 @@ func (s *ShardStore) LatestCommitTS(ctx context.Context, key []byte) (uint64, bo } } - return s.proxyLatestCommitTS(ctx, g, key) + return s.proxyLatestCommitTS(ctx, g, key, readRouteVersion) } -func (s *ShardStore) proxyLatestCommitTS(ctx context.Context, g *ShardGroup, key []byte) (uint64, bool, error) { +func (s *ShardStore) proxyLatestCommitTS(ctx context.Context, g *ShardGroup, key []byte, readRouteVersion uint64) (uint64, bool, error) { engine := engineForGroup(g) if engine == nil { return 0, false, nil @@ -755,7 +1124,7 @@ func (s *ShardStore) proxyLatestCommitTS(ctx context.Context, g *ShardGroup, key ctx, cancel := context.WithTimeout(ctx, proxyForwardTimeout) defer cancel() cli := pb.NewRawKVClient(conn) - resp, err := cli.RawLatestCommitTS(ctx, &pb.RawLatestCommitTSRequest{Key: key}) + resp, err := cli.RawLatestCommitTS(ctx, &pb.RawLatestCommitTSRequest{Key: key, ReadRouteVersion: readRouteVersion}) if err != nil { return 0, false, errors.WithStack(err) } @@ -1038,6 +1407,18 @@ func scanTxnLockRangeAt(ctx context.Context, g *ShardGroup, start []byte, end [] return scanTxnLockPagesAt(ctx, g.Store, lockStart, lockEnd, ts, boundedTxnLockScanLimit(limit)) } +func scanTxnLockRangeAtWithRouteFilter(ctx context.Context, g *ShardGroup, start []byte, end []byte, ts uint64, limit int, routeStart []byte, routeEnd []byte) ([]*store.KVPair, error) { + if g == nil || g.Store == nil { + return []*store.KVPair{}, nil + } + if !routeScanBoundsPresent(routeStart, routeEnd) { + return scanTxnLockRangeAt(ctx, g, start, end, ts, limit) + } + + lockStart, lockEnd := txnLockScanBounds(start, end) + return scanTxnLockPagesAtWithRouteFilter(ctx, g.Store, lockStart, lockEnd, ts, boundedTxnLockScanLimit(limit), routeStart, routeEnd) +} + func scanTxnLockPagesAt(ctx context.Context, st store.MVCCStore, start []byte, end []byte, ts uint64, limit int) ([]*store.KVPair, error) { out := make([]*store.KVPair, 0, min(limit, lockPageLimit)) cursor := start @@ -1060,6 +1441,35 @@ func scanTxnLockPagesAt(ctx context.Context, st store.MVCCStore, start []byte, e } } +func scanTxnLockPagesAtWithRouteFilter(ctx context.Context, st store.MVCCStore, start []byte, end []byte, ts uint64, limit int, routeStart []byte, routeEnd []byte) ([]*store.KVPair, error) { + out := make([]*store.KVPair, 0, min(limit, lockPageLimit)) + cursor := start + scanned := 0 + for { + lockKVs, nextCursor, done, err := scanTxnLockPageAt(ctx, st, cursor, end, ts) + if err != nil { + return nil, err + } + scanned += len(lockKVs) + for _, kvp := range lockKVs { + if kvp == nil || !routeKeyInScanBounds(kvp.Key, routeStart, routeEnd) { + continue + } + out = append(out, kvp) + if len(out) > limit { + return nil, errors.Wrapf(ErrTxnLocked, "scan lock budget exceeded for range [%q,%q)", string(start), string(end)) + } + } + if done { + return out, nil + } + if scanned >= limit { + return nil, errors.Wrapf(ErrTxnLocked, "scan lock budget exceeded for range [%q,%q)", string(start), string(end)) + } + cursor = nextCursor + } +} + const lockPageLimit = 256 const maxTxnLockScanResults = 1024 @@ -1636,7 +2046,7 @@ func (s *ShardStore) groupForKey(key []byte) (*ShardGroup, bool) { return g, ok } -func (s *ShardStore) proxyRawGet(ctx context.Context, g *ShardGroup, key []byte, ts uint64, groupID uint64) ([]byte, error) { +func (s *ShardStore) proxyRawGet(ctx context.Context, g *ShardGroup, key []byte, ts uint64, groupID uint64, readRouteVersion uint64) ([]byte, error) { engine := engineForGroup(g) if engine == nil { return nil, store.ErrKeyNotFound @@ -1654,7 +2064,7 @@ func (s *ShardStore) proxyRawGet(ctx context.Context, g *ShardGroup, key []byte, ctx, cancel := context.WithTimeout(ctx, proxyForwardTimeout) defer cancel() cli := pb.NewRawKVClient(conn) - resp, err := cli.RawGet(ctx, &pb.RawGetRequest{Key: key, Ts: ts, GroupId: groupID}) + resp, err := cli.RawGet(ctx, &pb.RawGetRequest{Key: key, Ts: ts, GroupId: groupID, ReadRouteVersion: readRouteVersion}) if err != nil { return nil, errors.WithStack(err) } @@ -1675,6 +2085,9 @@ func (s *ShardStore) proxyRawScanAt( ts uint64, reverse bool, groupID uint64, + readRouteVersion uint64, + routeStart []byte, + routeEnd []byte, ) ([]*store.KVPair, error) { engine := engineForGroup(g) if engine == nil { @@ -1694,12 +2107,16 @@ func (s *ShardStore) proxyRawScanAt( defer cancel() cli := pb.NewRawKVClient(conn) resp, err := cli.RawScanAt(ctx, &pb.RawScanAtRequest{ - StartKey: start, - EndKey: end, - Limit: int64(limit), - Ts: ts, - Reverse: reverse, - GroupId: groupID, + StartKey: start, + EndKey: end, + Limit: int64(limit), + Ts: ts, + Reverse: reverse, + GroupId: groupID, + ReadRouteVersion: readRouteVersion, + RouteStart: bytes.Clone(routeStart), + RouteEnd: bytes.Clone(routeEnd), + RouteBoundsPresent: routeScanBoundsPresent(routeStart, routeEnd), }) if err != nil { return nil, errors.WithStack(err) diff --git a/kv/shard_store_test.go b/kv/shard_store_test.go index 4f0da38b7..c50911b2a 100644 --- a/kv/shard_store_test.go +++ b/kv/shard_store_test.go @@ -6,6 +6,7 @@ import ( "github.com/bootjp/elastickv/distribution" "github.com/bootjp/elastickv/internal/s3keys" + pb "github.com/bootjp/elastickv/proto" "github.com/bootjp/elastickv/store" "github.com/stretchr/testify/require" ) @@ -116,6 +117,267 @@ func TestShardStoreGetGroupAt_UsesExplicitGroup(t *testing.T) { require.ErrorIs(t, err, store.ErrKeyNotFound) } +func TestShardStore_ForwardsReadFenceStamps(t *testing.T) { + t.Parallel() + + fake := &fakeRawKVServer{ + getResp: &pb.RawGetResponse{ + Exists: true, + Value: []byte("remote-v"), + }, + scanResp: &pb.RawScanAtResponse{}, + latestResp: &pb.RawLatestCommitTSResponse{ + Ts: 42, + Exists: true, + }, + } + addr, stop := startRawKVServer(t, fake) + t.Cleanup(stop) + + engine := distribution.NewEngine() + engine.UpdateRoute([]byte(""), nil, 1) + st := NewShardStore(engine, map[uint64]*ShardGroup{ + 1: { + Store: store.NewMVCCStore(), + Engine: &stubFollowerEngine{leaderAddr: addr}, + }, + }) + t.Cleanup(func() { _ = st.Close() }) + + ctx := context.Background() + _, err := st.GetAtWithReadFence(ctx, []byte("k"), 10, 0, 77) + require.NoError(t, err) + _, _, err = st.LatestCommitTSWithReadFence(ctx, []byte("k"), 78) + require.NoError(t, err) + _, err = st.ScanAtWithReadFence(ctx, []byte("a"), []byte("z"), 10, 11, false, 0, 79, []byte("a"), []byte("m")) + require.NoError(t, err) + + fake.mu.Lock() + require.Equal(t, uint64(77), fake.lastGetReq.GetReadRouteVersion()) + require.Equal(t, uint64(78), fake.lastLatestReq.GetReadRouteVersion()) + require.Equal(t, uint64(79), fake.lastScanReq.GetReadRouteVersion()) + require.Equal(t, uint64(1), fake.lastScanReq.GetGroupId()) + require.Equal(t, []byte("a"), fake.lastScanReq.GetRouteStart()) + require.Equal(t, []byte("m"), fake.lastScanReq.GetRouteEnd()) + require.True(t, fake.lastScanReq.GetRouteBoundsPresent()) + fake.mu.Unlock() + + _, err = st.ScanAtWithReadFence(ctx, []byte("a"), []byte("z"), 10, 11, false, 0, 80, []byte{}, []byte{}) + require.NoError(t, err) + + fake.mu.Lock() + require.Equal(t, uint64(1), fake.lastScanReq.GetGroupId()) + require.Equal(t, uint64(80), fake.lastScanReq.GetReadRouteVersion()) + require.True(t, fake.lastScanReq.GetRouteBoundsPresent()) + fake.mu.Unlock() + + _, err = st.ScanAtWithReadFence(ctx, []byte("a"), []byte("z"), 10, 11, false, 0, 81, nil, nil) + require.NoError(t, err) + + fake.mu.Lock() + defer fake.mu.Unlock() + require.Equal(t, uint64(0), fake.lastScanReq.GetGroupId()) + require.Equal(t, uint64(81), fake.lastScanReq.GetReadRouteVersion()) + require.False(t, fake.lastScanReq.GetRouteBoundsPresent()) +} + +func TestShardStoreScanAtWithReadFence_RoutesUsingSuppliedBounds(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + engine := distribution.NewEngine() + engine.UpdateRoute([]byte(""), []byte("m"), 1) + engine.UpdateRoute([]byte("m"), nil, 2) + + groups := map[uint64]*ShardGroup{ + 1: {Store: store.NewMVCCStore()}, + 2: {Store: store.NewMVCCStore()}, + } + st := NewShardStore(engine, groups) + + rawPrefix := []byte("!redis|meta|") + first := []byte("!redis|meta|x") + second := []byte("!redis|meta|y") + require.NoError(t, groups[2].Store.PutAt(ctx, first, []byte("v1"), 1, 0)) + require.NoError(t, groups[2].Store.PutAt(ctx, second, []byte("v2"), 2, 0)) + + kvs, err := st.ScanAtWithReadFence(ctx, rawPrefix, prefixScanEnd(rawPrefix), 10, 2, false, 0, 7, []byte("m"), nil) + require.NoError(t, err) + require.Len(t, kvs, 2) + require.Equal(t, first, kvs[0].Key) + require.Equal(t, second, kvs[1].Key) + + kvs, err = st.ScanAtWithReadFence(ctx, rawPrefix, prefixScanEnd(rawPrefix), 10, 2, true, 0, 7, []byte("m"), nil) + require.NoError(t, err) + require.Len(t, kvs, 2) + require.Equal(t, second, kvs[0].Key) + require.Equal(t, first, kvs[1].Key) +} + +func TestShardStoreScanAtWithReadFence_ScansSameGroupSuppliedBoundsAcrossRouteIntervals(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + engine := distribution.NewEngine() + engine.UpdateRoute([]byte(""), []byte("m"), 1) + engine.UpdateRoute([]byte("m"), nil, 1) + + groups := map[uint64]*ShardGroup{ + 1: {Store: store.NewMVCCStore()}, + } + st := NewShardStore(engine, groups) + + rawPrefix := []byte("!redis|meta|") + first := []byte("!redis|meta|a") + second := []byte("!redis|meta|z") + require.NoError(t, groups[1].Store.PutAt(ctx, first, []byte("v1"), 1, 0)) + require.NoError(t, groups[1].Store.PutAt(ctx, second, []byte("v2"), 2, 0)) + + kvs, err := st.ScanAtWithReadFence(ctx, rawPrefix, prefixScanEnd(rawPrefix), 10, 2, false, 0, 7, []byte("a"), []byte("z")) + require.NoError(t, err) + require.Len(t, kvs, 1) + require.Equal(t, first, kvs[0].Key) + + kvs, err = st.ScanAtWithReadFence(ctx, rawPrefix, prefixScanEnd(rawPrefix), 10, 2, false, 0, 7, []byte("a"), nil) + require.NoError(t, err) + require.Len(t, kvs, 2) + require.Equal(t, first, kvs[0].Key) + require.Equal(t, second, kvs[1].Key) + + kvs, err = st.ScanAtWithReadFence(ctx, rawPrefix, prefixScanEnd(rawPrefix), 10, 2, true, 0, 7, []byte("a"), nil) + require.NoError(t, err) + require.Len(t, kvs, 2) + require.Equal(t, second, kvs[0].Key) + require.Equal(t, first, kvs[1].Key) +} + +func TestShardStoreScanAtWithReadFence_FiltersWideRedisKeysByUserKey(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + engine := distribution.NewEngine() + engine.UpdateRoute([]byte(""), []byte("m"), 1) + engine.UpdateRoute([]byte("m"), nil, 1) + + groups := map[uint64]*ShardGroup{ + 1: {Store: store.NewMVCCStore()}, + } + st := NewShardStore(engine, groups) + + rawPrefix := []byte("!hs|") + left := store.HashFieldKey([]byte("alpha"), []byte("f")) + right := store.HashFieldKey([]byte("zulu"), []byte("f")) + require.NoError(t, groups[1].Store.PutAt(ctx, left, []byte("left"), 1, 0)) + require.NoError(t, groups[1].Store.PutAt(ctx, right, []byte("right"), 2, 0)) + + kvs, err := st.ScanAtWithReadFence(ctx, rawPrefix, prefixScanEnd(rawPrefix), 1, 2, false, 0, 7, []byte("m"), nil) + require.NoError(t, err) + require.Len(t, kvs, 1) + require.Equal(t, right, kvs[0].Key) + + kvs, err = st.ScanAtWithReadFence(ctx, rawPrefix, prefixScanEnd(rawPrefix), 1, 2, true, 0, 7, []byte{}, []byte("m")) + require.NoError(t, err) + require.Len(t, kvs, 1) + require.Equal(t, left, kvs[0].Key) +} + +func TestShardStoreScanAtWithReadFence_FiltersSuppliedBoundsByRouteKey(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + engine := distribution.NewEngine() + engine.UpdateRoute([]byte(""), []byte("m"), 1) + engine.UpdateRoute([]byte("m"), nil, 1) + + groups := map[uint64]*ShardGroup{ + 1: {Store: store.NewMVCCStore()}, + } + st := NewShardStore(engine, groups) + + rawPrefix := []byte("!redis|meta|") + left := []byte("!redis|meta|a") + right := []byte("!redis|meta|z") + require.NoError(t, groups[1].Store.PutAt(ctx, left, []byte("left"), 1, 0)) + require.NoError(t, groups[1].Store.PutAt(ctx, right, []byte("right"), 2, 0)) + + kvs, err := st.ScanAtWithReadFence(ctx, rawPrefix, prefixScanEnd(rawPrefix), 1, 2, false, 0, 7, []byte("m"), nil) + require.NoError(t, err) + require.Len(t, kvs, 1) + require.Equal(t, right, kvs[0].Key) + + kvs, err = st.ScanAtWithReadFence(ctx, rawPrefix, prefixScanEnd(rawPrefix), 1, 2, true, 0, 7, []byte{}, []byte("m")) + require.NoError(t, err) + require.Len(t, kvs, 1) + require.Equal(t, left, kvs[0].Key) +} + +func TestShardStoreScanAtWithReadFence_FiltersByEachRouteBounds(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + engine := distribution.NewEngine() + engine.UpdateRoute([]byte("a"), []byte("m"), 1) + engine.UpdateRoute([]byte("m"), []byte("z"), 2) + + groups := map[uint64]*ShardGroup{ + 1: {Store: store.NewMVCCStore()}, + 2: {Store: store.NewMVCCStore()}, + } + st := NewShardStore(engine, groups) + + rawPrefix := []byte("!redis|meta|") + left := []byte("!redis|meta|b") + staleRightOnLeftGroup := []byte("!redis|meta|x") + right := []byte("!redis|meta|y") + require.NoError(t, groups[1].Store.PutAt(ctx, left, []byte("left"), 1, 0)) + require.NoError(t, groups[1].Store.PutAt(ctx, staleRightOnLeftGroup, []byte("stale"), 2, 0)) + require.NoError(t, groups[2].Store.PutAt(ctx, right, []byte("right"), 3, 0)) + + kvs, err := st.ScanAtWithReadFence(ctx, rawPrefix, prefixScanEnd(rawPrefix), 10, 3, false, 0, 7, []byte("a"), []byte("z")) + require.NoError(t, err) + require.Len(t, kvs, 2) + require.Equal(t, left, kvs[0].Key) + require.Equal(t, right, kvs[1].Key) +} + +func TestShardStoreScanAtWithReadFence_AllowsExplicitGroupRouteBoundReverse(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + engine := distribution.NewEngine() + engine.UpdateRoute([]byte(""), nil, 1) + + groups := map[uint64]*ShardGroup{ + 1: {Store: store.NewMVCCStore()}, + } + st := NewShardStore(engine, groups) + + rawPrefix := []byte("!redis|meta|") + left := []byte("!redis|meta|a") + right := []byte("!redis|meta|z") + require.NoError(t, groups[1].Store.PutAt(ctx, left, []byte("left"), 1, 0)) + require.NoError(t, groups[1].Store.PutAt(ctx, right, []byte("right"), 2, 0)) + + _, err := st.ScanAtWithReadFence(ctx, rawPrefix, prefixScanEnd(rawPrefix), 1, 2, true, 1, 7, nil, nil) + require.ErrorIs(t, err, store.ErrNotSupported) + + kvs, err := st.ScanAtWithReadFence(ctx, rawPrefix, prefixScanEnd(rawPrefix), -1, 2, true, 1, 7, []byte("m"), nil) + require.NoError(t, err) + require.Empty(t, kvs) + + kvs, err = st.ScanAtWithReadFence(ctx, rawPrefix, prefixScanEnd(rawPrefix), 1, 2, true, 1, 7, []byte("m"), nil) + require.NoError(t, err) + require.Len(t, kvs, 1) + require.Equal(t, right, kvs[0].Key) + require.Equal(t, []byte("right"), kvs[0].Value) +} + func TestShardStoreScanAt_IncludesS3ManifestKeysAcrossShards(t *testing.T) { t.Parallel() @@ -377,6 +639,19 @@ func TestScanLockBoundsForKVs_ReverseOrder(t *testing.T) { require.Equal(t, nextScanCursor([]byte("c")), lockEnd) } +func TestScanLockBoundsForKVsDirection_ReverseUsesReturnedWindow(t *testing.T) { + t.Parallel() + + kvs := []*store.KVPair{ + {Key: []byte("z"), Value: []byte("vz")}, + {Key: []byte("y"), Value: []byte("vy")}, + } + + lockStart, lockEnd := scanLockBoundsForKVsDirection(kvs, []byte("a"), []byte("zz"), 2, true) + require.Equal(t, []byte("y"), lockStart) + require.Equal(t, []byte("zz"), lockEnd) +} + func TestScanLockBoundsForKVs_PreservesOriginalStart(t *testing.T) { t.Parallel() diff --git a/kv/shard_store_txn_lock_test.go b/kv/shard_store_txn_lock_test.go index 8c50ff3a4..d30491c64 100644 --- a/kv/shard_store_txn_lock_test.go +++ b/kv/shard_store_txn_lock_test.go @@ -2,6 +2,7 @@ package kv import ( "context" + "fmt" "math" "testing" @@ -281,6 +282,152 @@ func TestShardStoreScanAt_ReturnsTxnLockedForPendingLockWithoutCommittedValue(t require.True(t, errors.Is(err, ErrTxnLocked), "expected ErrTxnLocked, got %v", err) } +func TestShardStoreScanAtWithReadFence_SkipsOutOfRoutePendingLock(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + engine := distribution.NewEngine() + engine.UpdateRoute([]byte(""), []byte("m"), 1) + engine.UpdateRoute([]byte("m"), nil, 1) + + st1 := store.NewMVCCStore() + r1, stop1 := newSingleRaft(t, "g1", NewKvFSMWithHLC(st1, NewHLC())) + defer stop1() + + groups := map[uint64]*ShardGroup{ + 1: {Engine: r1, Store: st1, Txn: NewLeaderProxyWithEngine(r1)}, + } + shardStore := NewShardStore(engine, groups) + + rawPrefix := []byte("!redis|meta|") + left := []byte("!redis|meta|a") + right := []byte("!redis|meta|z") + require.NoError(t, st1.PutAt(ctx, right, []byte("right"), 1, 0)) + + startTS := uint64(2) + _, err := groups[1].Txn.Commit(context.Background(), []*pb.Request{makePrepareRequest(startTS, left, []byte("left"), left)}) + require.NoError(t, err) + + kvs, err := shardStore.ScanAtWithReadFence(ctx, rawPrefix, prefixScanEnd(rawPrefix), 1, ^uint64(0), false, 0, shardStore.ReadRouteVersion(), []byte("m"), nil) + require.NoError(t, err) + require.Len(t, kvs, 1) + require.Equal(t, right, kvs[0].Key) + require.Equal(t, []byte("right"), kvs[0].Value) +} + +func TestShardStoreScanAtWithReadFence_BoundsOutOfRouteLockScan(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + engine := distribution.NewEngine() + engine.UpdateRoute([]byte(""), []byte("m"), 1) + engine.UpdateRoute([]byte("m"), nil, 1) + + st1 := store.NewMVCCStore() + r1, stop1 := newSingleRaft(t, "g1", NewKvFSMWithHLC(st1, NewHLC())) + defer stop1() + + groups := map[uint64]*ShardGroup{ + 1: {Engine: r1, Store: st1, Txn: NewLeaderProxyWithEngine(r1)}, + } + shardStore := NewShardStore(engine, groups) + + rawPrefix := []byte("!redis|meta|") + right := []byte("!redis|meta|z") + require.NoError(t, st1.PutAt(ctx, right, []byte("right"), 1, 0)) + + require.Equal(t, lockPageLimit, boundedTxnLockScanLimit(1)) + for i := uint64(0); i <= lockPageLimit; i++ { + key := []byte(fmt.Sprintf("!redis|meta|a%04d", i)) + lock := encodeTxnLock(txnLock{ + StartTS: 10 + i, + TTLExpireAt: ^uint64(0), + PrimaryKey: key, + }) + require.NoError(t, st1.PutAt(ctx, txnLockKey(key), lock, 10+i, 0)) + } + + _, err := shardStore.ScanAtWithReadFence(ctx, rawPrefix, prefixScanEnd(rawPrefix), 1, ^uint64(0), false, 0, shardStore.ReadRouteVersion(), []byte("m"), nil) + require.Error(t, err) + require.True(t, errors.Is(err, ErrTxnLocked), "expected ErrTxnLocked, got %v", err) +} + +func TestShardStoreScanAtWithReadFence_BoundsLockScanToCurrentRawPage(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + engine := distribution.NewEngine() + engine.UpdateRoute([]byte(""), []byte("m"), 1) + engine.UpdateRoute([]byte("m"), nil, 1) + + st1 := store.NewMVCCStore() + r1, stop1 := newSingleRaft(t, "g1", NewKvFSMWithHLC(st1, NewHLC())) + defer stop1() + + groups := map[uint64]*ShardGroup{ + 1: {Engine: r1, Store: st1, Txn: NewLeaderProxyWithEngine(r1)}, + } + shardStore := NewShardStore(engine, groups) + + rawPrefix := []byte("!redis|meta|") + for i := uint64(0); i < routeFilteredScanBatchMin; i++ { + key := []byte(fmt.Sprintf("!redis|meta|a%04d", i)) + require.NoError(t, st1.PutAt(ctx, key, []byte("left"), i+1, 0)) + } + + right := []byte("!redis|meta|m001") + require.NoError(t, st1.PutAt(ctx, right, []byte("right"), 1000, 0)) + + farLocked := []byte("!redis|meta|z999") + lock := encodeTxnLock(txnLock{ + StartTS: 2000, + TTLExpireAt: ^uint64(0), + PrimaryKey: farLocked, + }) + require.NoError(t, st1.PutAt(ctx, txnLockKey(farLocked), lock, 2000, 0)) + + kvs, err := shardStore.ScanAtWithReadFence(ctx, rawPrefix, prefixScanEnd(rawPrefix), 1, ^uint64(0), false, 0, shardStore.ReadRouteVersion(), []byte("m"), nil) + require.NoError(t, err) + require.Len(t, kvs, 1) + require.Equal(t, right, kvs[0].Key) + require.Equal(t, []byte("right"), kvs[0].Value) +} + +func TestShardStoreScanAtWithReadFence_ReverseBoundsLockScanToPage(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + engine := distribution.NewEngine() + engine.UpdateRoute([]byte(""), nil, 1) + + st1 := store.NewMVCCStore() + r1, stop1 := newSingleRaft(t, "g1", NewKvFSMWithHLC(st1, NewHLC())) + defer stop1() + + groups := map[uint64]*ShardGroup{ + 1: {Engine: r1, Store: st1, Txn: NewLeaderProxyWithEngine(r1)}, + } + shardStore := NewShardStore(engine, groups) + + rawPrefix := []byte("!redis|meta|") + left := []byte("!redis|meta|a") + right := []byte("!redis|meta|z") + require.NoError(t, st1.PutAt(ctx, right, []byte("right"), 1, 0)) + + startTS := uint64(2) + _, err := groups[1].Txn.Commit(context.Background(), []*pb.Request{makePrepareRequest(startTS, left, []byte("left"), left)}) + require.NoError(t, err) + + kvs, err := shardStore.ScanAtWithReadFence(ctx, rawPrefix, prefixScanEnd(rawPrefix), 1, ^uint64(0), true, 1, shardStore.ReadRouteVersion(), []byte(""), nil) + require.NoError(t, err) + require.Len(t, kvs, 1) + require.Equal(t, right, kvs[0].Key) +} + func TestShardStoreScanAt_ReturnsTxnLockedWhenPendingLockExceedsUserLimit(t *testing.T) { t.Parallel() diff --git a/proto/distribution.pb.go b/proto/distribution.pb.go index c51d2aac2..21d15180c 100644 --- a/proto/distribution.pb.go +++ b/proto/distribution.pb.go @@ -437,15 +437,18 @@ func (x *GetTimestampResponse) GetTimestamp() uint64 { } type RouteDescriptor struct { - state protoimpl.MessageState `protogen:"open.v1"` - RouteId uint64 `protobuf:"varint,1,opt,name=route_id,json=routeId,proto3" json:"route_id,omitempty"` - Start []byte `protobuf:"bytes,2,opt,name=start,proto3" json:"start,omitempty"` - End []byte `protobuf:"bytes,3,opt,name=end,proto3" json:"end,omitempty"` - RaftGroupId uint64 `protobuf:"varint,4,opt,name=raft_group_id,json=raftGroupId,proto3" json:"raft_group_id,omitempty"` - State RouteState `protobuf:"varint,5,opt,name=state,proto3,enum=RouteState" json:"state,omitempty"` - ParentRouteId uint64 `protobuf:"varint,6,opt,name=parent_route_id,json=parentRouteId,proto3" json:"parent_route_id,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + RouteId uint64 `protobuf:"varint,1,opt,name=route_id,json=routeId,proto3" json:"route_id,omitempty"` + Start []byte `protobuf:"bytes,2,opt,name=start,proto3" json:"start,omitempty"` + End []byte `protobuf:"bytes,3,opt,name=end,proto3" json:"end,omitempty"` + RaftGroupId uint64 `protobuf:"varint,4,opt,name=raft_group_id,json=raftGroupId,proto3" json:"raft_group_id,omitempty"` + State RouteState `protobuf:"varint,5,opt,name=state,proto3,enum=RouteState" json:"state,omitempty"` + ParentRouteId uint64 `protobuf:"varint,6,opt,name=parent_route_id,json=parentRouteId,proto3" json:"parent_route_id,omitempty"` + StagedVisibilityActive bool `protobuf:"varint,7,opt,name=staged_visibility_active,json=stagedVisibilityActive,proto3" json:"staged_visibility_active,omitempty"` + MigrationJobId uint64 `protobuf:"varint,8,opt,name=migration_job_id,json=migrationJobId,proto3" json:"migration_job_id,omitempty"` + MinWriteTsExclusive uint64 `protobuf:"varint,9,opt,name=min_write_ts_exclusive,json=minWriteTsExclusive,proto3" json:"min_write_ts_exclusive,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *RouteDescriptor) Reset() { @@ -520,6 +523,27 @@ func (x *RouteDescriptor) GetParentRouteId() uint64 { return 0 } +func (x *RouteDescriptor) GetStagedVisibilityActive() bool { + if x != nil { + return x.StagedVisibilityActive + } + return false +} + +func (x *RouteDescriptor) GetMigrationJobId() uint64 { + if x != nil { + return x.MigrationJobId + } + return 0 +} + +func (x *RouteDescriptor) GetMinWriteTsExclusive() uint64 { + if x != nil { + return x.MinWriteTsExclusive + } + return 0 +} + type SplitJobBracketProgress struct { state protoimpl.MessageState `protogen:"open.v1"` BracketId uint64 `protobuf:"varint,1,opt,name=bracket_id,json=bracketId,proto3" json:"bracket_id,omitempty"` @@ -1128,88 +1152,847 @@ func (x *SplitRangeResponse) GetRight() *RouteDescriptor { return nil } -var File_distribution_proto protoreflect.FileDescriptor +type StartSplitMigrationRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ExpectedCatalogVersion uint64 `protobuf:"varint,1,opt,name=expected_catalog_version,json=expectedCatalogVersion,proto3" json:"expected_catalog_version,omitempty"` + RouteId uint64 `protobuf:"varint,2,opt,name=route_id,json=routeId,proto3" json:"route_id,omitempty"` + SplitKey []byte `protobuf:"bytes,3,opt,name=split_key,json=splitKey,proto3" json:"split_key,omitempty"` + TargetGroupId uint64 `protobuf:"varint,4,opt,name=target_group_id,json=targetGroupId,proto3" json:"target_group_id,omitempty"` + Options map[string]string `protobuf:"bytes,5,rep,name=options,proto3" json:"options,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} -const file_distribution_proto_rawDesc = "" + - "\n" + - "\x12distribution.proto\"#\n" + - "\x0fGetRouteRequest\x12\x10\n" + - "\x03key\x18\x01 \x01(\fR\x03key\"^\n" + - "\x10GetRouteResponse\x12\x14\n" + - "\x05start\x18\x01 \x01(\fR\x05start\x12\x10\n" + - "\x03end\x18\x02 \x01(\fR\x03end\x12\"\n" + - "\rraft_group_id\x18\x03 \x01(\x04R\vraftGroupId\"\x15\n" + - "\x13GetTimestampRequest\"4\n" + - "\x14GetTimestampResponse\x12\x1c\n" + - "\ttimestamp\x18\x01 \x01(\x04R\ttimestamp\"\xc3\x01\n" + - "\x0fRouteDescriptor\x12\x19\n" + - "\broute_id\x18\x01 \x01(\x04R\arouteId\x12\x14\n" + - "\x05start\x18\x02 \x01(\fR\x05start\x12\x10\n" + - "\x03end\x18\x03 \x01(\fR\x03end\x12\"\n" + - "\rraft_group_id\x18\x04 \x01(\x04R\vraftGroupId\x12!\n" + - "\x05state\x18\x05 \x01(\x0e2\v.RouteStateR\x05state\x12&\n" + - "\x0fparent_route_id\x18\x06 \x01(\x04R\rparentRouteId\"\xb0\x02\n" + - "\x17SplitJobBracketProgress\x12\x1d\n" + - "\n" + - "bracket_id\x18\x01 \x01(\x04R\tbracketId\x12\x16\n" + - "\x06family\x18\x02 \x01(\rR\x06family\x127\n" + - "\fexport_phase\x18\x03 \x01(\x0e2\x14.SplitJobExportPhaseR\vexportPhase\x12\x16\n" + - "\x06cursor\x18\x04 \x01(\fR\x06cursor\x12\x12\n" + - "\x04done\x18\x05 \x01(\bR\x04done\x12#\n" + - "\rscanned_bytes\x18\x06 \x01(\x04R\fscannedBytes\x12#\n" + - "\raccepted_rows\x18\a \x01(\x04R\facceptedRows\x12/\n" + - "\x14last_acked_batch_seq\x18\b \x01(\x04R\x11lastAckedBatchSeq\"\xe9\f\n" + - "\bSplitJob\x12\x15\n" + - "\x06job_id\x18\x01 \x01(\x04R\x05jobId\x12&\n" + - "\x0fsource_route_id\x18\x02 \x01(\x04R\rsourceRouteId\x12\x1b\n" + - "\tsplit_key\x18\x03 \x01(\fR\bsplitKey\x12&\n" + - "\x0ftarget_group_id\x18\x04 \x01(\x04R\rtargetGroupId\x12$\n" + - "\x05phase\x18\x05 \x01(\x0e2\x0e.SplitJobPhaseR\x05phase\x12/\n" + - "\vretry_phase\x18\x06 \x01(\x0e2\x0e.SplitJobPhaseR\n" + - "retryPhase\x12<\n" + - "\x12abandon_from_phase\x18\a \x01(\x0e2\x0e.SplitJobPhaseR\x10abandonFromPhase\x12\x1f\n" + - "\vsnapshot_ts\x18\b \x01(\x04R\n" + - "snapshotTs\x127\n" + - "\x18snapshot_min_admitted_ts\x18\t \x01(\x04R\x15snapshotMinAdmittedTs\x12.\n" + - "\x13write_tracker_armed\x18\n" + - " \x01(\bR\x11writeTrackerArmed\x12\x1f\n" + - "\vdelta_floor\x18\v \x01(\x04R\n" + - "deltaFloor\x12;\n" + - "\x1apost_fence_drain_completed\x18\f \x01(\bR\x17postFenceDrainCompleted\x12\x19\n" + - "\bfence_ts\x18\r \x01(\x04R\afenceTs\x12'\n" + - "\x0fcutover_version\x18\x0e \x01(\x04R\x0ecutoverVersion\x12N\n" + - "\x18cutover_read_fence_state\x18\x0f \x01(\x0e2\x15.SplitJobBarrierStateR\x15cutoverReadFenceState\x12X\n" + - "\x1dtarget_staged_readiness_state\x18\x10 \x01(\x0e2\x15.SplitJobBarrierStateR\x1atargetStagedReadinessState\x12M\n" + - "$source_cutover_read_fence_ack_cursor\x18\x11 \x01(\fR\x1fsourceCutoverReadFenceAckCursor\x12J\n" + - "\"target_staged_readiness_ack_cursor\x18\x12 \x01(\fR\x1etargetStagedReadinessAckCursor\x12\x16\n" + - "\x06cursor\x18\x13 \x01(\fR\x06cursor\x12&\n" + - "\x0fmax_imported_ts\x18\x14 \x01(\x04R\rmaxImportedTs\x122\n" + - "\x15target_promotion_done\x18\x15 \x01(\bR\x13targetPromotionDone\x124\n" + - "\x16promotion_completed_ts\x18\x16 \x01(\x04R\x14promotionCompletedTs\x122\n" + - "\x15fence_catalog_version\x18\x17 \x01(\x04R\x13fenceCatalogVersion\x12(\n" + - "\x10fence_ack_cursor\x18\x18 \x01(\fR\x0efenceAckCursor\x129\n" + - "\x19source_cutover_ack_cursor\x18\x19 \x01(\fR\x16sourceCutoverAckCursor\x127\n" + - "\x18source_read_drain_cursor\x18\x1a \x01(\fR\x15sourceReadDrainCursor\x12N\n" + - "$target_cleared_descriptor_ack_cursor\x18\x1b \x01(\fR targetClearedDescriptorAckCursor\x12C\n" + - "\x10bracket_progress\x18\x1c \x03(\v2\x18.SplitJobBracketProgressR\x0fbracketProgress\x125\n" + - "\x17source_retention_pin_ts\x18\x1d \x01(\x04R\x14sourceRetentionPinTs\x12\x1d\n" + - "\n" + - "last_error\x18\x1e \x01(\tR\tlastError\x12\"\n" + - "\rstarted_at_ms\x18\x1f \x01(\x03R\vstartedAtMs\x12\"\n" + - "\rupdated_at_ms\x18 \x01(\x03R\vupdatedAtMs\x12$\n" + - "\x0eterminal_at_ms\x18! \x01(\x03R\fterminalAtMs\"\x13\n" + - "\x11ListRoutesRequest\"g\n" + - "\x12ListRoutesResponse\x12'\n" + - "\x0fcatalog_version\x18\x01 \x01(\x04R\x0ecatalogVersion\x12(\n" + - "\x06routes\x18\x02 \x03(\v2\x10.RouteDescriptorR\x06routes\"\x85\x01\n" + - "\x11SplitRangeRequest\x128\n" + - "\x18expected_catalog_version\x18\x01 \x01(\x04R\x16expectedCatalogVersion\x12\x19\n" + - "\broute_id\x18\x02 \x01(\x04R\arouteId\x12\x1b\n" + - "\tsplit_key\x18\x03 \x01(\fR\bsplitKey\"\x8b\x01\n" + - "\x12SplitRangeResponse\x12'\n" + - "\x0fcatalog_version\x18\x01 \x01(\x04R\x0ecatalogVersion\x12$\n" + - "\x04left\x18\x02 \x01(\v2\x10.RouteDescriptorR\x04left\x12&\n" + - "\x05right\x18\x03 \x01(\v2\x10.RouteDescriptorR\x05right*\xa3\x01\n" + +func (x *StartSplitMigrationRequest) Reset() { + *x = StartSplitMigrationRequest{} + mi := &file_distribution_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StartSplitMigrationRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StartSplitMigrationRequest) ProtoMessage() {} + +func (x *StartSplitMigrationRequest) ProtoReflect() protoreflect.Message { + mi := &file_distribution_proto_msgTypes[11] + 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 StartSplitMigrationRequest.ProtoReflect.Descriptor instead. +func (*StartSplitMigrationRequest) Descriptor() ([]byte, []int) { + return file_distribution_proto_rawDescGZIP(), []int{11} +} + +func (x *StartSplitMigrationRequest) GetExpectedCatalogVersion() uint64 { + if x != nil { + return x.ExpectedCatalogVersion + } + return 0 +} + +func (x *StartSplitMigrationRequest) GetRouteId() uint64 { + if x != nil { + return x.RouteId + } + return 0 +} + +func (x *StartSplitMigrationRequest) GetSplitKey() []byte { + if x != nil { + return x.SplitKey + } + return nil +} + +func (x *StartSplitMigrationRequest) GetTargetGroupId() uint64 { + if x != nil { + return x.TargetGroupId + } + return 0 +} + +func (x *StartSplitMigrationRequest) GetOptions() map[string]string { + if x != nil { + return x.Options + } + return nil +} + +type StartSplitMigrationResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + CatalogVersion uint64 `protobuf:"varint,1,opt,name=catalog_version,json=catalogVersion,proto3" json:"catalog_version,omitempty"` + JobId uint64 `protobuf:"varint,2,opt,name=job_id,json=jobId,proto3" json:"job_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StartSplitMigrationResponse) Reset() { + *x = StartSplitMigrationResponse{} + mi := &file_distribution_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StartSplitMigrationResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StartSplitMigrationResponse) ProtoMessage() {} + +func (x *StartSplitMigrationResponse) ProtoReflect() protoreflect.Message { + mi := &file_distribution_proto_msgTypes[12] + 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 StartSplitMigrationResponse.ProtoReflect.Descriptor instead. +func (*StartSplitMigrationResponse) Descriptor() ([]byte, []int) { + return file_distribution_proto_rawDescGZIP(), []int{12} +} + +func (x *StartSplitMigrationResponse) GetCatalogVersion() uint64 { + if x != nil { + return x.CatalogVersion + } + return 0 +} + +func (x *StartSplitMigrationResponse) GetJobId() uint64 { + if x != nil { + return x.JobId + } + return 0 +} + +type GetRouteOwnershipRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + CatalogVersion uint64 `protobuf:"varint,2,opt,name=catalog_version,json=catalogVersion,proto3" json:"catalog_version,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetRouteOwnershipRequest) Reset() { + *x = GetRouteOwnershipRequest{} + mi := &file_distribution_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetRouteOwnershipRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetRouteOwnershipRequest) ProtoMessage() {} + +func (x *GetRouteOwnershipRequest) ProtoReflect() protoreflect.Message { + mi := &file_distribution_proto_msgTypes[13] + 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 GetRouteOwnershipRequest.ProtoReflect.Descriptor instead. +func (*GetRouteOwnershipRequest) Descriptor() ([]byte, []int) { + return file_distribution_proto_rawDescGZIP(), []int{13} +} + +func (x *GetRouteOwnershipRequest) GetKey() []byte { + if x != nil { + return x.Key + } + return nil +} + +func (x *GetRouteOwnershipRequest) GetCatalogVersion() uint64 { + if x != nil { + return x.CatalogVersion + } + return 0 +} + +type GetRouteOwnershipResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Route *RouteDescriptor `protobuf:"bytes,1,opt,name=route,proto3" json:"route,omitempty"` + CatalogVersion uint64 `protobuf:"varint,2,opt,name=catalog_version,json=catalogVersion,proto3" json:"catalog_version,omitempty"` + Found bool `protobuf:"varint,3,opt,name=found,proto3" json:"found,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetRouteOwnershipResponse) Reset() { + *x = GetRouteOwnershipResponse{} + mi := &file_distribution_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetRouteOwnershipResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetRouteOwnershipResponse) ProtoMessage() {} + +func (x *GetRouteOwnershipResponse) ProtoReflect() protoreflect.Message { + mi := &file_distribution_proto_msgTypes[14] + 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 GetRouteOwnershipResponse.ProtoReflect.Descriptor instead. +func (*GetRouteOwnershipResponse) Descriptor() ([]byte, []int) { + return file_distribution_proto_rawDescGZIP(), []int{14} +} + +func (x *GetRouteOwnershipResponse) GetRoute() *RouteDescriptor { + if x != nil { + return x.Route + } + return nil +} + +func (x *GetRouteOwnershipResponse) GetCatalogVersion() uint64 { + if x != nil { + return x.CatalogVersion + } + return 0 +} + +func (x *GetRouteOwnershipResponse) GetFound() bool { + if x != nil { + return x.Found + } + return false +} + +type GetIntersectingRoutesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Start []byte `protobuf:"bytes,1,opt,name=start,proto3" json:"start,omitempty"` + End []byte `protobuf:"bytes,2,opt,name=end,proto3" json:"end,omitempty"` + CatalogVersion uint64 `protobuf:"varint,3,opt,name=catalog_version,json=catalogVersion,proto3" json:"catalog_version,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetIntersectingRoutesRequest) Reset() { + *x = GetIntersectingRoutesRequest{} + mi := &file_distribution_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetIntersectingRoutesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetIntersectingRoutesRequest) ProtoMessage() {} + +func (x *GetIntersectingRoutesRequest) ProtoReflect() protoreflect.Message { + mi := &file_distribution_proto_msgTypes[15] + 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 GetIntersectingRoutesRequest.ProtoReflect.Descriptor instead. +func (*GetIntersectingRoutesRequest) Descriptor() ([]byte, []int) { + return file_distribution_proto_rawDescGZIP(), []int{15} +} + +func (x *GetIntersectingRoutesRequest) GetStart() []byte { + if x != nil { + return x.Start + } + return nil +} + +func (x *GetIntersectingRoutesRequest) GetEnd() []byte { + if x != nil { + return x.End + } + return nil +} + +func (x *GetIntersectingRoutesRequest) GetCatalogVersion() uint64 { + if x != nil { + return x.CatalogVersion + } + return 0 +} + +type GetIntersectingRoutesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Routes []*RouteDescriptor `protobuf:"bytes,1,rep,name=routes,proto3" json:"routes,omitempty"` + CatalogVersion uint64 `protobuf:"varint,2,opt,name=catalog_version,json=catalogVersion,proto3" json:"catalog_version,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetIntersectingRoutesResponse) Reset() { + *x = GetIntersectingRoutesResponse{} + mi := &file_distribution_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetIntersectingRoutesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetIntersectingRoutesResponse) ProtoMessage() {} + +func (x *GetIntersectingRoutesResponse) ProtoReflect() protoreflect.Message { + mi := &file_distribution_proto_msgTypes[16] + 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 GetIntersectingRoutesResponse.ProtoReflect.Descriptor instead. +func (*GetIntersectingRoutesResponse) Descriptor() ([]byte, []int) { + return file_distribution_proto_rawDescGZIP(), []int{16} +} + +func (x *GetIntersectingRoutesResponse) GetRoutes() []*RouteDescriptor { + if x != nil { + return x.Routes + } + return nil +} + +func (x *GetIntersectingRoutesResponse) GetCatalogVersion() uint64 { + if x != nil { + return x.CatalogVersion + } + return 0 +} + +type GetSplitJobRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + JobId uint64 `protobuf:"varint,1,opt,name=job_id,json=jobId,proto3" json:"job_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetSplitJobRequest) Reset() { + *x = GetSplitJobRequest{} + mi := &file_distribution_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetSplitJobRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetSplitJobRequest) ProtoMessage() {} + +func (x *GetSplitJobRequest) ProtoReflect() protoreflect.Message { + mi := &file_distribution_proto_msgTypes[17] + 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 GetSplitJobRequest.ProtoReflect.Descriptor instead. +func (*GetSplitJobRequest) Descriptor() ([]byte, []int) { + return file_distribution_proto_rawDescGZIP(), []int{17} +} + +func (x *GetSplitJobRequest) GetJobId() uint64 { + if x != nil { + return x.JobId + } + return 0 +} + +type GetSplitJobResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Job *SplitJob `protobuf:"bytes,1,opt,name=job,proto3" json:"job,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetSplitJobResponse) Reset() { + *x = GetSplitJobResponse{} + mi := &file_distribution_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetSplitJobResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetSplitJobResponse) ProtoMessage() {} + +func (x *GetSplitJobResponse) ProtoReflect() protoreflect.Message { + mi := &file_distribution_proto_msgTypes[18] + 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 GetSplitJobResponse.ProtoReflect.Descriptor instead. +func (*GetSplitJobResponse) Descriptor() ([]byte, []int) { + return file_distribution_proto_rawDescGZIP(), []int{18} +} + +func (x *GetSplitJobResponse) GetJob() *SplitJob { + if x != nil { + return x.Job + } + return nil +} + +type ListSplitJobsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + SinceTerminalAtMs uint64 `protobuf:"varint,1,opt,name=since_terminal_at_ms,json=sinceTerminalAtMs,proto3" json:"since_terminal_at_ms,omitempty"` + Phase string `protobuf:"bytes,2,opt,name=phase,proto3" json:"phase,omitempty"` + PageCursor []byte `protobuf:"bytes,3,opt,name=page_cursor,json=pageCursor,proto3" json:"page_cursor,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListSplitJobsRequest) Reset() { + *x = ListSplitJobsRequest{} + mi := &file_distribution_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListSplitJobsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListSplitJobsRequest) ProtoMessage() {} + +func (x *ListSplitJobsRequest) ProtoReflect() protoreflect.Message { + mi := &file_distribution_proto_msgTypes[19] + 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 ListSplitJobsRequest.ProtoReflect.Descriptor instead. +func (*ListSplitJobsRequest) Descriptor() ([]byte, []int) { + return file_distribution_proto_rawDescGZIP(), []int{19} +} + +func (x *ListSplitJobsRequest) GetSinceTerminalAtMs() uint64 { + if x != nil { + return x.SinceTerminalAtMs + } + return 0 +} + +func (x *ListSplitJobsRequest) GetPhase() string { + if x != nil { + return x.Phase + } + return "" +} + +func (x *ListSplitJobsRequest) GetPageCursor() []byte { + if x != nil { + return x.PageCursor + } + return nil +} + +type ListSplitJobsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Jobs []*SplitJob `protobuf:"bytes,1,rep,name=jobs,proto3" json:"jobs,omitempty"` + NextPageCursor []byte `protobuf:"bytes,2,opt,name=next_page_cursor,json=nextPageCursor,proto3" json:"next_page_cursor,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListSplitJobsResponse) Reset() { + *x = ListSplitJobsResponse{} + mi := &file_distribution_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListSplitJobsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListSplitJobsResponse) ProtoMessage() {} + +func (x *ListSplitJobsResponse) ProtoReflect() protoreflect.Message { + mi := &file_distribution_proto_msgTypes[20] + 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 ListSplitJobsResponse.ProtoReflect.Descriptor instead. +func (*ListSplitJobsResponse) Descriptor() ([]byte, []int) { + return file_distribution_proto_rawDescGZIP(), []int{20} +} + +func (x *ListSplitJobsResponse) GetJobs() []*SplitJob { + if x != nil { + return x.Jobs + } + return nil +} + +func (x *ListSplitJobsResponse) GetNextPageCursor() []byte { + if x != nil { + return x.NextPageCursor + } + return nil +} + +type AbandonSplitJobRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + JobId uint64 `protobuf:"varint,1,opt,name=job_id,json=jobId,proto3" json:"job_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AbandonSplitJobRequest) Reset() { + *x = AbandonSplitJobRequest{} + mi := &file_distribution_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AbandonSplitJobRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AbandonSplitJobRequest) ProtoMessage() {} + +func (x *AbandonSplitJobRequest) ProtoReflect() protoreflect.Message { + mi := &file_distribution_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 AbandonSplitJobRequest.ProtoReflect.Descriptor instead. +func (*AbandonSplitJobRequest) Descriptor() ([]byte, []int) { + return file_distribution_proto_rawDescGZIP(), []int{21} +} + +func (x *AbandonSplitJobRequest) GetJobId() uint64 { + if x != nil { + return x.JobId + } + return 0 +} + +type AbandonSplitJobResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AbandonSplitJobResponse) Reset() { + *x = AbandonSplitJobResponse{} + mi := &file_distribution_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AbandonSplitJobResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AbandonSplitJobResponse) ProtoMessage() {} + +func (x *AbandonSplitJobResponse) ProtoReflect() protoreflect.Message { + mi := &file_distribution_proto_msgTypes[22] + 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 AbandonSplitJobResponse.ProtoReflect.Descriptor instead. +func (*AbandonSplitJobResponse) Descriptor() ([]byte, []int) { + return file_distribution_proto_rawDescGZIP(), []int{22} +} + +type RetrySplitJobRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + JobId uint64 `protobuf:"varint,1,opt,name=job_id,json=jobId,proto3" json:"job_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RetrySplitJobRequest) Reset() { + *x = RetrySplitJobRequest{} + mi := &file_distribution_proto_msgTypes[23] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RetrySplitJobRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RetrySplitJobRequest) ProtoMessage() {} + +func (x *RetrySplitJobRequest) ProtoReflect() protoreflect.Message { + mi := &file_distribution_proto_msgTypes[23] + 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 RetrySplitJobRequest.ProtoReflect.Descriptor instead. +func (*RetrySplitJobRequest) Descriptor() ([]byte, []int) { + return file_distribution_proto_rawDescGZIP(), []int{23} +} + +func (x *RetrySplitJobRequest) GetJobId() uint64 { + if x != nil { + return x.JobId + } + return 0 +} + +type RetrySplitJobResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RetrySplitJobResponse) Reset() { + *x = RetrySplitJobResponse{} + mi := &file_distribution_proto_msgTypes[24] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RetrySplitJobResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RetrySplitJobResponse) ProtoMessage() {} + +func (x *RetrySplitJobResponse) ProtoReflect() protoreflect.Message { + mi := &file_distribution_proto_msgTypes[24] + 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 RetrySplitJobResponse.ProtoReflect.Descriptor instead. +func (*RetrySplitJobResponse) Descriptor() ([]byte, []int) { + return file_distribution_proto_rawDescGZIP(), []int{24} +} + +var File_distribution_proto protoreflect.FileDescriptor + +const file_distribution_proto_rawDesc = "" + + "\n" + + "\x12distribution.proto\"#\n" + + "\x0fGetRouteRequest\x12\x10\n" + + "\x03key\x18\x01 \x01(\fR\x03key\"^\n" + + "\x10GetRouteResponse\x12\x14\n" + + "\x05start\x18\x01 \x01(\fR\x05start\x12\x10\n" + + "\x03end\x18\x02 \x01(\fR\x03end\x12\"\n" + + "\rraft_group_id\x18\x03 \x01(\x04R\vraftGroupId\"\x15\n" + + "\x13GetTimestampRequest\"4\n" + + "\x14GetTimestampResponse\x12\x1c\n" + + "\ttimestamp\x18\x01 \x01(\x04R\ttimestamp\"\xdc\x02\n" + + "\x0fRouteDescriptor\x12\x19\n" + + "\broute_id\x18\x01 \x01(\x04R\arouteId\x12\x14\n" + + "\x05start\x18\x02 \x01(\fR\x05start\x12\x10\n" + + "\x03end\x18\x03 \x01(\fR\x03end\x12\"\n" + + "\rraft_group_id\x18\x04 \x01(\x04R\vraftGroupId\x12!\n" + + "\x05state\x18\x05 \x01(\x0e2\v.RouteStateR\x05state\x12&\n" + + "\x0fparent_route_id\x18\x06 \x01(\x04R\rparentRouteId\x128\n" + + "\x18staged_visibility_active\x18\a \x01(\bR\x16stagedVisibilityActive\x12(\n" + + "\x10migration_job_id\x18\b \x01(\x04R\x0emigrationJobId\x123\n" + + "\x16min_write_ts_exclusive\x18\t \x01(\x04R\x13minWriteTsExclusive\"\xb0\x02\n" + + "\x17SplitJobBracketProgress\x12\x1d\n" + + "\n" + + "bracket_id\x18\x01 \x01(\x04R\tbracketId\x12\x16\n" + + "\x06family\x18\x02 \x01(\rR\x06family\x127\n" + + "\fexport_phase\x18\x03 \x01(\x0e2\x14.SplitJobExportPhaseR\vexportPhase\x12\x16\n" + + "\x06cursor\x18\x04 \x01(\fR\x06cursor\x12\x12\n" + + "\x04done\x18\x05 \x01(\bR\x04done\x12#\n" + + "\rscanned_bytes\x18\x06 \x01(\x04R\fscannedBytes\x12#\n" + + "\raccepted_rows\x18\a \x01(\x04R\facceptedRows\x12/\n" + + "\x14last_acked_batch_seq\x18\b \x01(\x04R\x11lastAckedBatchSeq\"\xe9\f\n" + + "\bSplitJob\x12\x15\n" + + "\x06job_id\x18\x01 \x01(\x04R\x05jobId\x12&\n" + + "\x0fsource_route_id\x18\x02 \x01(\x04R\rsourceRouteId\x12\x1b\n" + + "\tsplit_key\x18\x03 \x01(\fR\bsplitKey\x12&\n" + + "\x0ftarget_group_id\x18\x04 \x01(\x04R\rtargetGroupId\x12$\n" + + "\x05phase\x18\x05 \x01(\x0e2\x0e.SplitJobPhaseR\x05phase\x12/\n" + + "\vretry_phase\x18\x06 \x01(\x0e2\x0e.SplitJobPhaseR\n" + + "retryPhase\x12<\n" + + "\x12abandon_from_phase\x18\a \x01(\x0e2\x0e.SplitJobPhaseR\x10abandonFromPhase\x12\x1f\n" + + "\vsnapshot_ts\x18\b \x01(\x04R\n" + + "snapshotTs\x127\n" + + "\x18snapshot_min_admitted_ts\x18\t \x01(\x04R\x15snapshotMinAdmittedTs\x12.\n" + + "\x13write_tracker_armed\x18\n" + + " \x01(\bR\x11writeTrackerArmed\x12\x1f\n" + + "\vdelta_floor\x18\v \x01(\x04R\n" + + "deltaFloor\x12;\n" + + "\x1apost_fence_drain_completed\x18\f \x01(\bR\x17postFenceDrainCompleted\x12\x19\n" + + "\bfence_ts\x18\r \x01(\x04R\afenceTs\x12'\n" + + "\x0fcutover_version\x18\x0e \x01(\x04R\x0ecutoverVersion\x12N\n" + + "\x18cutover_read_fence_state\x18\x0f \x01(\x0e2\x15.SplitJobBarrierStateR\x15cutoverReadFenceState\x12X\n" + + "\x1dtarget_staged_readiness_state\x18\x10 \x01(\x0e2\x15.SplitJobBarrierStateR\x1atargetStagedReadinessState\x12M\n" + + "$source_cutover_read_fence_ack_cursor\x18\x11 \x01(\fR\x1fsourceCutoverReadFenceAckCursor\x12J\n" + + "\"target_staged_readiness_ack_cursor\x18\x12 \x01(\fR\x1etargetStagedReadinessAckCursor\x12\x16\n" + + "\x06cursor\x18\x13 \x01(\fR\x06cursor\x12&\n" + + "\x0fmax_imported_ts\x18\x14 \x01(\x04R\rmaxImportedTs\x122\n" + + "\x15target_promotion_done\x18\x15 \x01(\bR\x13targetPromotionDone\x124\n" + + "\x16promotion_completed_ts\x18\x16 \x01(\x04R\x14promotionCompletedTs\x122\n" + + "\x15fence_catalog_version\x18\x17 \x01(\x04R\x13fenceCatalogVersion\x12(\n" + + "\x10fence_ack_cursor\x18\x18 \x01(\fR\x0efenceAckCursor\x129\n" + + "\x19source_cutover_ack_cursor\x18\x19 \x01(\fR\x16sourceCutoverAckCursor\x127\n" + + "\x18source_read_drain_cursor\x18\x1a \x01(\fR\x15sourceReadDrainCursor\x12N\n" + + "$target_cleared_descriptor_ack_cursor\x18\x1b \x01(\fR targetClearedDescriptorAckCursor\x12C\n" + + "\x10bracket_progress\x18\x1c \x03(\v2\x18.SplitJobBracketProgressR\x0fbracketProgress\x125\n" + + "\x17source_retention_pin_ts\x18\x1d \x01(\x04R\x14sourceRetentionPinTs\x12\x1d\n" + + "\n" + + "last_error\x18\x1e \x01(\tR\tlastError\x12\"\n" + + "\rstarted_at_ms\x18\x1f \x01(\x03R\vstartedAtMs\x12\"\n" + + "\rupdated_at_ms\x18 \x01(\x03R\vupdatedAtMs\x12$\n" + + "\x0eterminal_at_ms\x18! \x01(\x03R\fterminalAtMs\"\x13\n" + + "\x11ListRoutesRequest\"g\n" + + "\x12ListRoutesResponse\x12'\n" + + "\x0fcatalog_version\x18\x01 \x01(\x04R\x0ecatalogVersion\x12(\n" + + "\x06routes\x18\x02 \x03(\v2\x10.RouteDescriptorR\x06routes\"\x85\x01\n" + + "\x11SplitRangeRequest\x128\n" + + "\x18expected_catalog_version\x18\x01 \x01(\x04R\x16expectedCatalogVersion\x12\x19\n" + + "\broute_id\x18\x02 \x01(\x04R\arouteId\x12\x1b\n" + + "\tsplit_key\x18\x03 \x01(\fR\bsplitKey\"\x8b\x01\n" + + "\x12SplitRangeResponse\x12'\n" + + "\x0fcatalog_version\x18\x01 \x01(\x04R\x0ecatalogVersion\x12$\n" + + "\x04left\x18\x02 \x01(\v2\x10.RouteDescriptorR\x04left\x12&\n" + + "\x05right\x18\x03 \x01(\v2\x10.RouteDescriptorR\x05right\"\xb6\x02\n" + + "\x1aStartSplitMigrationRequest\x128\n" + + "\x18expected_catalog_version\x18\x01 \x01(\x04R\x16expectedCatalogVersion\x12\x19\n" + + "\broute_id\x18\x02 \x01(\x04R\arouteId\x12\x1b\n" + + "\tsplit_key\x18\x03 \x01(\fR\bsplitKey\x12&\n" + + "\x0ftarget_group_id\x18\x04 \x01(\x04R\rtargetGroupId\x12B\n" + + "\aoptions\x18\x05 \x03(\v2(.StartSplitMigrationRequest.OptionsEntryR\aoptions\x1a:\n" + + "\fOptionsEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"]\n" + + "\x1bStartSplitMigrationResponse\x12'\n" + + "\x0fcatalog_version\x18\x01 \x01(\x04R\x0ecatalogVersion\x12\x15\n" + + "\x06job_id\x18\x02 \x01(\x04R\x05jobId\"U\n" + + "\x18GetRouteOwnershipRequest\x12\x10\n" + + "\x03key\x18\x01 \x01(\fR\x03key\x12'\n" + + "\x0fcatalog_version\x18\x02 \x01(\x04R\x0ecatalogVersion\"\x82\x01\n" + + "\x19GetRouteOwnershipResponse\x12&\n" + + "\x05route\x18\x01 \x01(\v2\x10.RouteDescriptorR\x05route\x12'\n" + + "\x0fcatalog_version\x18\x02 \x01(\x04R\x0ecatalogVersion\x12\x14\n" + + "\x05found\x18\x03 \x01(\bR\x05found\"o\n" + + "\x1cGetIntersectingRoutesRequest\x12\x14\n" + + "\x05start\x18\x01 \x01(\fR\x05start\x12\x10\n" + + "\x03end\x18\x02 \x01(\fR\x03end\x12'\n" + + "\x0fcatalog_version\x18\x03 \x01(\x04R\x0ecatalogVersion\"r\n" + + "\x1dGetIntersectingRoutesResponse\x12(\n" + + "\x06routes\x18\x01 \x03(\v2\x10.RouteDescriptorR\x06routes\x12'\n" + + "\x0fcatalog_version\x18\x02 \x01(\x04R\x0ecatalogVersion\"+\n" + + "\x12GetSplitJobRequest\x12\x15\n" + + "\x06job_id\x18\x01 \x01(\x04R\x05jobId\"2\n" + + "\x13GetSplitJobResponse\x12\x1b\n" + + "\x03job\x18\x01 \x01(\v2\t.SplitJobR\x03job\"~\n" + + "\x14ListSplitJobsRequest\x12/\n" + + "\x14since_terminal_at_ms\x18\x01 \x01(\x04R\x11sinceTerminalAtMs\x12\x14\n" + + "\x05phase\x18\x02 \x01(\tR\x05phase\x12\x1f\n" + + "\vpage_cursor\x18\x03 \x01(\fR\n" + + "pageCursor\"`\n" + + "\x15ListSplitJobsResponse\x12\x1d\n" + + "\x04jobs\x18\x01 \x03(\v2\t.SplitJobR\x04jobs\x12(\n" + + "\x10next_page_cursor\x18\x02 \x01(\fR\x0enextPageCursor\"/\n" + + "\x16AbandonSplitJobRequest\x12\x15\n" + + "\x06job_id\x18\x01 \x01(\x04R\x05jobId\"\x19\n" + + "\x17AbandonSplitJobResponse\"-\n" + + "\x14RetrySplitJobRequest\x12\x15\n" + + "\x06job_id\x18\x01 \x01(\x04R\x05jobId\"\x17\n" + + "\x15RetrySplitJobResponse*\xa3\x01\n" + "\n" + "RouteState\x12\x1b\n" + "\x17ROUTE_STATE_UNSPECIFIED\x10\x00\x12\x16\n" + @@ -1238,14 +2021,21 @@ const file_distribution_proto_rawDesc = "" + "\x13SplitJobExportPhase\x12\x1f\n" + "\x1bSPLIT_JOB_EXPORT_PHASE_NONE\x10\x00\x12#\n" + "\x1fSPLIT_JOB_EXPORT_PHASE_BACKFILL\x10\x01\x12%\n" + - "!SPLIT_JOB_EXPORT_PHASE_DELTA_COPY\x10\x022\xf2\x01\n" + + "!SPLIT_JOB_EXPORT_PHASE_DELTA_COPY\x10\x022\xf6\x05\n" + "\fDistribution\x121\n" + "\bGetRoute\x12\x10.GetRouteRequest\x1a\x11.GetRouteResponse\"\x00\x12=\n" + "\fGetTimestamp\x12\x14.GetTimestampRequest\x1a\x15.GetTimestampResponse\"\x00\x127\n" + "\n" + "ListRoutes\x12\x12.ListRoutesRequest\x1a\x13.ListRoutesResponse\"\x00\x127\n" + "\n" + - "SplitRange\x12\x12.SplitRangeRequest\x1a\x13.SplitRangeResponse\"\x00B#Z!github.com/bootjp/elastickv/protob\x06proto3" + "SplitRange\x12\x12.SplitRangeRequest\x1a\x13.SplitRangeResponse\"\x00\x12R\n" + + "\x13StartSplitMigration\x12\x1b.StartSplitMigrationRequest\x1a\x1c.StartSplitMigrationResponse\"\x00\x12L\n" + + "\x11GetRouteOwnership\x12\x19.GetRouteOwnershipRequest\x1a\x1a.GetRouteOwnershipResponse\"\x00\x12X\n" + + "\x15GetIntersectingRoutes\x12\x1d.GetIntersectingRoutesRequest\x1a\x1e.GetIntersectingRoutesResponse\"\x00\x12:\n" + + "\vGetSplitJob\x12\x13.GetSplitJobRequest\x1a\x14.GetSplitJobResponse\"\x00\x12@\n" + + "\rListSplitJobs\x12\x15.ListSplitJobsRequest\x1a\x16.ListSplitJobsResponse\"\x00\x12F\n" + + "\x0fAbandonSplitJob\x12\x17.AbandonSplitJobRequest\x1a\x18.AbandonSplitJobResponse\"\x00\x12@\n" + + "\rRetrySplitJob\x12\x15.RetrySplitJobRequest\x1a\x16.RetrySplitJobResponse\"\x00B#Z!github.com/bootjp/elastickv/protob\x06proto3" var ( file_distribution_proto_rawDescOnce sync.Once @@ -1260,23 +2050,38 @@ func file_distribution_proto_rawDescGZIP() []byte { } var file_distribution_proto_enumTypes = make([]protoimpl.EnumInfo, 4) -var file_distribution_proto_msgTypes = make([]protoimpl.MessageInfo, 11) +var file_distribution_proto_msgTypes = make([]protoimpl.MessageInfo, 26) var file_distribution_proto_goTypes = []any{ - (RouteState)(0), // 0: RouteState - (SplitJobPhase)(0), // 1: SplitJobPhase - (SplitJobBarrierState)(0), // 2: SplitJobBarrierState - (SplitJobExportPhase)(0), // 3: SplitJobExportPhase - (*GetRouteRequest)(nil), // 4: GetRouteRequest - (*GetRouteResponse)(nil), // 5: GetRouteResponse - (*GetTimestampRequest)(nil), // 6: GetTimestampRequest - (*GetTimestampResponse)(nil), // 7: GetTimestampResponse - (*RouteDescriptor)(nil), // 8: RouteDescriptor - (*SplitJobBracketProgress)(nil), // 9: SplitJobBracketProgress - (*SplitJob)(nil), // 10: SplitJob - (*ListRoutesRequest)(nil), // 11: ListRoutesRequest - (*ListRoutesResponse)(nil), // 12: ListRoutesResponse - (*SplitRangeRequest)(nil), // 13: SplitRangeRequest - (*SplitRangeResponse)(nil), // 14: SplitRangeResponse + (RouteState)(0), // 0: RouteState + (SplitJobPhase)(0), // 1: SplitJobPhase + (SplitJobBarrierState)(0), // 2: SplitJobBarrierState + (SplitJobExportPhase)(0), // 3: SplitJobExportPhase + (*GetRouteRequest)(nil), // 4: GetRouteRequest + (*GetRouteResponse)(nil), // 5: GetRouteResponse + (*GetTimestampRequest)(nil), // 6: GetTimestampRequest + (*GetTimestampResponse)(nil), // 7: GetTimestampResponse + (*RouteDescriptor)(nil), // 8: RouteDescriptor + (*SplitJobBracketProgress)(nil), // 9: SplitJobBracketProgress + (*SplitJob)(nil), // 10: SplitJob + (*ListRoutesRequest)(nil), // 11: ListRoutesRequest + (*ListRoutesResponse)(nil), // 12: ListRoutesResponse + (*SplitRangeRequest)(nil), // 13: SplitRangeRequest + (*SplitRangeResponse)(nil), // 14: SplitRangeResponse + (*StartSplitMigrationRequest)(nil), // 15: StartSplitMigrationRequest + (*StartSplitMigrationResponse)(nil), // 16: StartSplitMigrationResponse + (*GetRouteOwnershipRequest)(nil), // 17: GetRouteOwnershipRequest + (*GetRouteOwnershipResponse)(nil), // 18: GetRouteOwnershipResponse + (*GetIntersectingRoutesRequest)(nil), // 19: GetIntersectingRoutesRequest + (*GetIntersectingRoutesResponse)(nil), // 20: GetIntersectingRoutesResponse + (*GetSplitJobRequest)(nil), // 21: GetSplitJobRequest + (*GetSplitJobResponse)(nil), // 22: GetSplitJobResponse + (*ListSplitJobsRequest)(nil), // 23: ListSplitJobsRequest + (*ListSplitJobsResponse)(nil), // 24: ListSplitJobsResponse + (*AbandonSplitJobRequest)(nil), // 25: AbandonSplitJobRequest + (*AbandonSplitJobResponse)(nil), // 26: AbandonSplitJobResponse + (*RetrySplitJobRequest)(nil), // 27: RetrySplitJobRequest + (*RetrySplitJobResponse)(nil), // 28: RetrySplitJobResponse + nil, // 29: StartSplitMigrationRequest.OptionsEntry } var file_distribution_proto_depIdxs = []int32{ 0, // 0: RouteDescriptor.state:type_name -> RouteState @@ -1290,19 +2095,38 @@ var file_distribution_proto_depIdxs = []int32{ 8, // 8: ListRoutesResponse.routes:type_name -> RouteDescriptor 8, // 9: SplitRangeResponse.left:type_name -> RouteDescriptor 8, // 10: SplitRangeResponse.right:type_name -> RouteDescriptor - 4, // 11: Distribution.GetRoute:input_type -> GetRouteRequest - 6, // 12: Distribution.GetTimestamp:input_type -> GetTimestampRequest - 11, // 13: Distribution.ListRoutes:input_type -> ListRoutesRequest - 13, // 14: Distribution.SplitRange:input_type -> SplitRangeRequest - 5, // 15: Distribution.GetRoute:output_type -> GetRouteResponse - 7, // 16: Distribution.GetTimestamp:output_type -> GetTimestampResponse - 12, // 17: Distribution.ListRoutes:output_type -> ListRoutesResponse - 14, // 18: Distribution.SplitRange:output_type -> SplitRangeResponse - 15, // [15:19] is the sub-list for method output_type - 11, // [11:15] is the sub-list for method input_type - 11, // [11:11] is the sub-list for extension type_name - 11, // [11:11] is the sub-list for extension extendee - 0, // [0:11] is the sub-list for field type_name + 29, // 11: StartSplitMigrationRequest.options:type_name -> StartSplitMigrationRequest.OptionsEntry + 8, // 12: GetRouteOwnershipResponse.route:type_name -> RouteDescriptor + 8, // 13: GetIntersectingRoutesResponse.routes:type_name -> RouteDescriptor + 10, // 14: GetSplitJobResponse.job:type_name -> SplitJob + 10, // 15: ListSplitJobsResponse.jobs:type_name -> SplitJob + 4, // 16: Distribution.GetRoute:input_type -> GetRouteRequest + 6, // 17: Distribution.GetTimestamp:input_type -> GetTimestampRequest + 11, // 18: Distribution.ListRoutes:input_type -> ListRoutesRequest + 13, // 19: Distribution.SplitRange:input_type -> SplitRangeRequest + 15, // 20: Distribution.StartSplitMigration:input_type -> StartSplitMigrationRequest + 17, // 21: Distribution.GetRouteOwnership:input_type -> GetRouteOwnershipRequest + 19, // 22: Distribution.GetIntersectingRoutes:input_type -> GetIntersectingRoutesRequest + 21, // 23: Distribution.GetSplitJob:input_type -> GetSplitJobRequest + 23, // 24: Distribution.ListSplitJobs:input_type -> ListSplitJobsRequest + 25, // 25: Distribution.AbandonSplitJob:input_type -> AbandonSplitJobRequest + 27, // 26: Distribution.RetrySplitJob:input_type -> RetrySplitJobRequest + 5, // 27: Distribution.GetRoute:output_type -> GetRouteResponse + 7, // 28: Distribution.GetTimestamp:output_type -> GetTimestampResponse + 12, // 29: Distribution.ListRoutes:output_type -> ListRoutesResponse + 14, // 30: Distribution.SplitRange:output_type -> SplitRangeResponse + 16, // 31: Distribution.StartSplitMigration:output_type -> StartSplitMigrationResponse + 18, // 32: Distribution.GetRouteOwnership:output_type -> GetRouteOwnershipResponse + 20, // 33: Distribution.GetIntersectingRoutes:output_type -> GetIntersectingRoutesResponse + 22, // 34: Distribution.GetSplitJob:output_type -> GetSplitJobResponse + 24, // 35: Distribution.ListSplitJobs:output_type -> ListSplitJobsResponse + 26, // 36: Distribution.AbandonSplitJob:output_type -> AbandonSplitJobResponse + 28, // 37: Distribution.RetrySplitJob:output_type -> RetrySplitJobResponse + 27, // [27:38] is the sub-list for method output_type + 16, // [16:27] 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 } func init() { file_distribution_proto_init() } @@ -1316,7 +2140,7 @@ func file_distribution_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_distribution_proto_rawDesc), len(file_distribution_proto_rawDesc)), NumEnums: 4, - NumMessages: 11, + NumMessages: 26, NumExtensions: 0, NumServices: 1, }, diff --git a/proto/distribution.proto b/proto/distribution.proto index 33cc9cb62..eb9746f18 100644 --- a/proto/distribution.proto +++ b/proto/distribution.proto @@ -7,6 +7,13 @@ service Distribution { rpc GetTimestamp (GetTimestampRequest) returns (GetTimestampResponse) {} rpc ListRoutes (ListRoutesRequest) returns (ListRoutesResponse) {} rpc SplitRange (SplitRangeRequest) returns (SplitRangeResponse) {} + rpc StartSplitMigration (StartSplitMigrationRequest) returns (StartSplitMigrationResponse) {} + rpc GetRouteOwnership (GetRouteOwnershipRequest) returns (GetRouteOwnershipResponse) {} + rpc GetIntersectingRoutes (GetIntersectingRoutesRequest) returns (GetIntersectingRoutesResponse) {} + rpc GetSplitJob (GetSplitJobRequest) returns (GetSplitJobResponse) {} + rpc ListSplitJobs (ListSplitJobsRequest) returns (ListSplitJobsResponse) {} + rpc AbandonSplitJob (AbandonSplitJobRequest) returns (AbandonSplitJobResponse) {} + rpc RetrySplitJob (RetrySplitJobRequest) returns (RetrySplitJobResponse) {} } message GetRouteRequest { @@ -42,6 +49,9 @@ message RouteDescriptor { uint64 raft_group_id = 4; RouteState state = 5; uint64 parent_route_id = 6; + bool staged_visibility_active = 7; + uint64 migration_job_id = 8; + uint64 min_write_ts_exclusive = 9; } enum SplitJobPhase { @@ -136,3 +146,69 @@ message SplitRangeResponse { RouteDescriptor left = 2; RouteDescriptor right = 3; } + +message StartSplitMigrationRequest { + uint64 expected_catalog_version = 1; + uint64 route_id = 2; + bytes split_key = 3; + uint64 target_group_id = 4; + map options = 5; +} + +message StartSplitMigrationResponse { + uint64 catalog_version = 1; + uint64 job_id = 2; +} + +message GetRouteOwnershipRequest { + bytes key = 1; + uint64 catalog_version = 2; +} + +message GetRouteOwnershipResponse { + RouteDescriptor route = 1; + uint64 catalog_version = 2; + bool found = 3; +} + +message GetIntersectingRoutesRequest { + bytes start = 1; + bytes end = 2; + uint64 catalog_version = 3; +} + +message GetIntersectingRoutesResponse { + repeated RouteDescriptor routes = 1; + uint64 catalog_version = 2; +} + +message GetSplitJobRequest { + uint64 job_id = 1; +} + +message GetSplitJobResponse { + SplitJob job = 1; +} + +message ListSplitJobsRequest { + uint64 since_terminal_at_ms = 1; + string phase = 2; + bytes page_cursor = 3; +} + +message ListSplitJobsResponse { + repeated SplitJob jobs = 1; + bytes next_page_cursor = 2; +} + +message AbandonSplitJobRequest { + uint64 job_id = 1; +} + +message AbandonSplitJobResponse {} + +message RetrySplitJobRequest { + uint64 job_id = 1; +} + +message RetrySplitJobResponse {} diff --git a/proto/distribution_grpc.pb.go b/proto/distribution_grpc.pb.go index f8d9b82e4..66ebf8511 100644 --- a/proto/distribution_grpc.pb.go +++ b/proto/distribution_grpc.pb.go @@ -19,10 +19,17 @@ import ( const _ = grpc.SupportPackageIsVersion9 const ( - Distribution_GetRoute_FullMethodName = "/Distribution/GetRoute" - Distribution_GetTimestamp_FullMethodName = "/Distribution/GetTimestamp" - Distribution_ListRoutes_FullMethodName = "/Distribution/ListRoutes" - Distribution_SplitRange_FullMethodName = "/Distribution/SplitRange" + Distribution_GetRoute_FullMethodName = "/Distribution/GetRoute" + Distribution_GetTimestamp_FullMethodName = "/Distribution/GetTimestamp" + Distribution_ListRoutes_FullMethodName = "/Distribution/ListRoutes" + Distribution_SplitRange_FullMethodName = "/Distribution/SplitRange" + Distribution_StartSplitMigration_FullMethodName = "/Distribution/StartSplitMigration" + Distribution_GetRouteOwnership_FullMethodName = "/Distribution/GetRouteOwnership" + Distribution_GetIntersectingRoutes_FullMethodName = "/Distribution/GetIntersectingRoutes" + Distribution_GetSplitJob_FullMethodName = "/Distribution/GetSplitJob" + Distribution_ListSplitJobs_FullMethodName = "/Distribution/ListSplitJobs" + Distribution_AbandonSplitJob_FullMethodName = "/Distribution/AbandonSplitJob" + Distribution_RetrySplitJob_FullMethodName = "/Distribution/RetrySplitJob" ) // DistributionClient is the client API for Distribution service. @@ -33,6 +40,13 @@ type DistributionClient interface { GetTimestamp(ctx context.Context, in *GetTimestampRequest, opts ...grpc.CallOption) (*GetTimestampResponse, error) ListRoutes(ctx context.Context, in *ListRoutesRequest, opts ...grpc.CallOption) (*ListRoutesResponse, error) SplitRange(ctx context.Context, in *SplitRangeRequest, opts ...grpc.CallOption) (*SplitRangeResponse, error) + StartSplitMigration(ctx context.Context, in *StartSplitMigrationRequest, opts ...grpc.CallOption) (*StartSplitMigrationResponse, error) + GetRouteOwnership(ctx context.Context, in *GetRouteOwnershipRequest, opts ...grpc.CallOption) (*GetRouteOwnershipResponse, error) + GetIntersectingRoutes(ctx context.Context, in *GetIntersectingRoutesRequest, opts ...grpc.CallOption) (*GetIntersectingRoutesResponse, error) + GetSplitJob(ctx context.Context, in *GetSplitJobRequest, opts ...grpc.CallOption) (*GetSplitJobResponse, error) + ListSplitJobs(ctx context.Context, in *ListSplitJobsRequest, opts ...grpc.CallOption) (*ListSplitJobsResponse, error) + AbandonSplitJob(ctx context.Context, in *AbandonSplitJobRequest, opts ...grpc.CallOption) (*AbandonSplitJobResponse, error) + RetrySplitJob(ctx context.Context, in *RetrySplitJobRequest, opts ...grpc.CallOption) (*RetrySplitJobResponse, error) } type distributionClient struct { @@ -83,6 +97,76 @@ func (c *distributionClient) SplitRange(ctx context.Context, in *SplitRangeReque return out, nil } +func (c *distributionClient) StartSplitMigration(ctx context.Context, in *StartSplitMigrationRequest, opts ...grpc.CallOption) (*StartSplitMigrationResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(StartSplitMigrationResponse) + err := c.cc.Invoke(ctx, Distribution_StartSplitMigration_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *distributionClient) GetRouteOwnership(ctx context.Context, in *GetRouteOwnershipRequest, opts ...grpc.CallOption) (*GetRouteOwnershipResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetRouteOwnershipResponse) + err := c.cc.Invoke(ctx, Distribution_GetRouteOwnership_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *distributionClient) GetIntersectingRoutes(ctx context.Context, in *GetIntersectingRoutesRequest, opts ...grpc.CallOption) (*GetIntersectingRoutesResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetIntersectingRoutesResponse) + err := c.cc.Invoke(ctx, Distribution_GetIntersectingRoutes_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *distributionClient) GetSplitJob(ctx context.Context, in *GetSplitJobRequest, opts ...grpc.CallOption) (*GetSplitJobResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetSplitJobResponse) + err := c.cc.Invoke(ctx, Distribution_GetSplitJob_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *distributionClient) ListSplitJobs(ctx context.Context, in *ListSplitJobsRequest, opts ...grpc.CallOption) (*ListSplitJobsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListSplitJobsResponse) + err := c.cc.Invoke(ctx, Distribution_ListSplitJobs_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *distributionClient) AbandonSplitJob(ctx context.Context, in *AbandonSplitJobRequest, opts ...grpc.CallOption) (*AbandonSplitJobResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(AbandonSplitJobResponse) + err := c.cc.Invoke(ctx, Distribution_AbandonSplitJob_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *distributionClient) RetrySplitJob(ctx context.Context, in *RetrySplitJobRequest, opts ...grpc.CallOption) (*RetrySplitJobResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(RetrySplitJobResponse) + err := c.cc.Invoke(ctx, Distribution_RetrySplitJob_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + // DistributionServer is the server API for Distribution service. // All implementations must embed UnimplementedDistributionServer // for forward compatibility. @@ -91,6 +175,13 @@ type DistributionServer interface { GetTimestamp(context.Context, *GetTimestampRequest) (*GetTimestampResponse, error) ListRoutes(context.Context, *ListRoutesRequest) (*ListRoutesResponse, error) SplitRange(context.Context, *SplitRangeRequest) (*SplitRangeResponse, error) + StartSplitMigration(context.Context, *StartSplitMigrationRequest) (*StartSplitMigrationResponse, error) + GetRouteOwnership(context.Context, *GetRouteOwnershipRequest) (*GetRouteOwnershipResponse, error) + GetIntersectingRoutes(context.Context, *GetIntersectingRoutesRequest) (*GetIntersectingRoutesResponse, error) + GetSplitJob(context.Context, *GetSplitJobRequest) (*GetSplitJobResponse, error) + ListSplitJobs(context.Context, *ListSplitJobsRequest) (*ListSplitJobsResponse, error) + AbandonSplitJob(context.Context, *AbandonSplitJobRequest) (*AbandonSplitJobResponse, error) + RetrySplitJob(context.Context, *RetrySplitJobRequest) (*RetrySplitJobResponse, error) mustEmbedUnimplementedDistributionServer() } @@ -113,6 +204,27 @@ func (UnimplementedDistributionServer) ListRoutes(context.Context, *ListRoutesRe func (UnimplementedDistributionServer) SplitRange(context.Context, *SplitRangeRequest) (*SplitRangeResponse, error) { return nil, status.Error(codes.Unimplemented, "method SplitRange not implemented") } +func (UnimplementedDistributionServer) StartSplitMigration(context.Context, *StartSplitMigrationRequest) (*StartSplitMigrationResponse, error) { + return nil, status.Error(codes.Unimplemented, "method StartSplitMigration not implemented") +} +func (UnimplementedDistributionServer) GetRouteOwnership(context.Context, *GetRouteOwnershipRequest) (*GetRouteOwnershipResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetRouteOwnership not implemented") +} +func (UnimplementedDistributionServer) GetIntersectingRoutes(context.Context, *GetIntersectingRoutesRequest) (*GetIntersectingRoutesResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetIntersectingRoutes not implemented") +} +func (UnimplementedDistributionServer) GetSplitJob(context.Context, *GetSplitJobRequest) (*GetSplitJobResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetSplitJob not implemented") +} +func (UnimplementedDistributionServer) ListSplitJobs(context.Context, *ListSplitJobsRequest) (*ListSplitJobsResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListSplitJobs not implemented") +} +func (UnimplementedDistributionServer) AbandonSplitJob(context.Context, *AbandonSplitJobRequest) (*AbandonSplitJobResponse, error) { + return nil, status.Error(codes.Unimplemented, "method AbandonSplitJob not implemented") +} +func (UnimplementedDistributionServer) RetrySplitJob(context.Context, *RetrySplitJobRequest) (*RetrySplitJobResponse, error) { + return nil, status.Error(codes.Unimplemented, "method RetrySplitJob not implemented") +} func (UnimplementedDistributionServer) mustEmbedUnimplementedDistributionServer() {} func (UnimplementedDistributionServer) testEmbeddedByValue() {} @@ -206,6 +318,132 @@ func _Distribution_SplitRange_Handler(srv interface{}, ctx context.Context, dec return interceptor(ctx, in, info, handler) } +func _Distribution_StartSplitMigration_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(StartSplitMigrationRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DistributionServer).StartSplitMigration(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Distribution_StartSplitMigration_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DistributionServer).StartSplitMigration(ctx, req.(*StartSplitMigrationRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Distribution_GetRouteOwnership_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetRouteOwnershipRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DistributionServer).GetRouteOwnership(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Distribution_GetRouteOwnership_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DistributionServer).GetRouteOwnership(ctx, req.(*GetRouteOwnershipRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Distribution_GetIntersectingRoutes_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetIntersectingRoutesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DistributionServer).GetIntersectingRoutes(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Distribution_GetIntersectingRoutes_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DistributionServer).GetIntersectingRoutes(ctx, req.(*GetIntersectingRoutesRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Distribution_GetSplitJob_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetSplitJobRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DistributionServer).GetSplitJob(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Distribution_GetSplitJob_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DistributionServer).GetSplitJob(ctx, req.(*GetSplitJobRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Distribution_ListSplitJobs_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListSplitJobsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DistributionServer).ListSplitJobs(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Distribution_ListSplitJobs_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DistributionServer).ListSplitJobs(ctx, req.(*ListSplitJobsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Distribution_AbandonSplitJob_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(AbandonSplitJobRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DistributionServer).AbandonSplitJob(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Distribution_AbandonSplitJob_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DistributionServer).AbandonSplitJob(ctx, req.(*AbandonSplitJobRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Distribution_RetrySplitJob_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RetrySplitJobRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DistributionServer).RetrySplitJob(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Distribution_RetrySplitJob_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DistributionServer).RetrySplitJob(ctx, req.(*RetrySplitJobRequest)) + } + return interceptor(ctx, in, info, handler) +} + // Distribution_ServiceDesc is the grpc.ServiceDesc for Distribution service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -229,6 +467,34 @@ var Distribution_ServiceDesc = grpc.ServiceDesc{ MethodName: "SplitRange", Handler: _Distribution_SplitRange_Handler, }, + { + MethodName: "StartSplitMigration", + Handler: _Distribution_StartSplitMigration_Handler, + }, + { + MethodName: "GetRouteOwnership", + Handler: _Distribution_GetRouteOwnership_Handler, + }, + { + MethodName: "GetIntersectingRoutes", + Handler: _Distribution_GetIntersectingRoutes_Handler, + }, + { + MethodName: "GetSplitJob", + Handler: _Distribution_GetSplitJob_Handler, + }, + { + MethodName: "ListSplitJobs", + Handler: _Distribution_ListSplitJobs_Handler, + }, + { + MethodName: "AbandonSplitJob", + Handler: _Distribution_AbandonSplitJob_Handler, + }, + { + MethodName: "RetrySplitJob", + Handler: _Distribution_RetrySplitJob_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "distribution.proto", diff --git a/proto/internal.pb.go b/proto/internal.pb.go index f5ec91269..0e2bd4664 100644 --- a/proto/internal.pb.go +++ b/proto/internal.pb.go @@ -529,6 +529,378 @@ func (x *RelayPublishResponse) GetSubscribers() int64 { return 0 } +type ExportRangeVersionsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + RangeStart []byte `protobuf:"bytes,1,opt,name=range_start,json=rangeStart,proto3" json:"range_start,omitempty"` + RangeEnd []byte `protobuf:"bytes,2,opt,name=range_end,json=rangeEnd,proto3" json:"range_end,omitempty"` + MaxCommitTs uint64 `protobuf:"varint,3,opt,name=max_commit_ts,json=maxCommitTs,proto3" json:"max_commit_ts,omitempty"` + MinCommitTs uint64 `protobuf:"varint,4,opt,name=min_commit_ts,json=minCommitTs,proto3" json:"min_commit_ts,omitempty"` + Cursor []byte `protobuf:"bytes,5,opt,name=cursor,proto3" json:"cursor,omitempty"` + ChunkBytes uint32 `protobuf:"varint,6,opt,name=chunk_bytes,json=chunkBytes,proto3" json:"chunk_bytes,omitempty"` + RouteStart []byte `protobuf:"bytes,7,opt,name=route_start,json=routeStart,proto3" json:"route_start,omitempty"` + RouteEnd []byte `protobuf:"bytes,8,opt,name=route_end,json=routeEnd,proto3" json:"route_end,omitempty"` + MaxScannedBytes uint64 `protobuf:"varint,9,opt,name=max_scanned_bytes,json=maxScannedBytes,proto3" json:"max_scanned_bytes,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExportRangeVersionsRequest) Reset() { + *x = ExportRangeVersionsRequest{} + mi := &file_internal_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExportRangeVersionsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExportRangeVersionsRequest) ProtoMessage() {} + +func (x *ExportRangeVersionsRequest) ProtoReflect() protoreflect.Message { + mi := &file_internal_proto_msgTypes[7] + 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 ExportRangeVersionsRequest.ProtoReflect.Descriptor instead. +func (*ExportRangeVersionsRequest) Descriptor() ([]byte, []int) { + return file_internal_proto_rawDescGZIP(), []int{7} +} + +func (x *ExportRangeVersionsRequest) GetRangeStart() []byte { + if x != nil { + return x.RangeStart + } + return nil +} + +func (x *ExportRangeVersionsRequest) GetRangeEnd() []byte { + if x != nil { + return x.RangeEnd + } + return nil +} + +func (x *ExportRangeVersionsRequest) GetMaxCommitTs() uint64 { + if x != nil { + return x.MaxCommitTs + } + return 0 +} + +func (x *ExportRangeVersionsRequest) GetMinCommitTs() uint64 { + if x != nil { + return x.MinCommitTs + } + return 0 +} + +func (x *ExportRangeVersionsRequest) GetCursor() []byte { + if x != nil { + return x.Cursor + } + return nil +} + +func (x *ExportRangeVersionsRequest) GetChunkBytes() uint32 { + if x != nil { + return x.ChunkBytes + } + return 0 +} + +func (x *ExportRangeVersionsRequest) GetRouteStart() []byte { + if x != nil { + return x.RouteStart + } + return nil +} + +func (x *ExportRangeVersionsRequest) GetRouteEnd() []byte { + if x != nil { + return x.RouteEnd + } + return nil +} + +func (x *ExportRangeVersionsRequest) GetMaxScannedBytes() uint64 { + if x != nil { + return x.MaxScannedBytes + } + return 0 +} + +type ExportRangeVersionsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Versions []*MVCCVersion `protobuf:"bytes,1,rep,name=versions,proto3" json:"versions,omitempty"` + NextCursor []byte `protobuf:"bytes,2,opt,name=next_cursor,json=nextCursor,proto3" json:"next_cursor,omitempty"` + Done bool `protobuf:"varint,3,opt,name=done,proto3" json:"done,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExportRangeVersionsResponse) Reset() { + *x = ExportRangeVersionsResponse{} + mi := &file_internal_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExportRangeVersionsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExportRangeVersionsResponse) ProtoMessage() {} + +func (x *ExportRangeVersionsResponse) ProtoReflect() protoreflect.Message { + mi := &file_internal_proto_msgTypes[8] + 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 ExportRangeVersionsResponse.ProtoReflect.Descriptor instead. +func (*ExportRangeVersionsResponse) Descriptor() ([]byte, []int) { + return file_internal_proto_rawDescGZIP(), []int{8} +} + +func (x *ExportRangeVersionsResponse) GetVersions() []*MVCCVersion { + if x != nil { + return x.Versions + } + return nil +} + +func (x *ExportRangeVersionsResponse) GetNextCursor() []byte { + if x != nil { + return x.NextCursor + } + return nil +} + +func (x *ExportRangeVersionsResponse) GetDone() bool { + if x != nil { + return x.Done + } + return false +} + +type MVCCVersion struct { + state protoimpl.MessageState `protogen:"open.v1"` + Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + CommitTs uint64 `protobuf:"varint,2,opt,name=commit_ts,json=commitTs,proto3" json:"commit_ts,omitempty"` + Tombstone bool `protobuf:"varint,3,opt,name=tombstone,proto3" json:"tombstone,omitempty"` + Value []byte `protobuf:"bytes,4,opt,name=value,proto3" json:"value,omitempty"` + KeyFamily uint32 `protobuf:"varint,5,opt,name=key_family,json=keyFamily,proto3" json:"key_family,omitempty"` + ExpireAt uint64 `protobuf:"varint,6,opt,name=expire_at,json=expireAt,proto3" json:"expire_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MVCCVersion) Reset() { + *x = MVCCVersion{} + mi := &file_internal_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MVCCVersion) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MVCCVersion) ProtoMessage() {} + +func (x *MVCCVersion) ProtoReflect() protoreflect.Message { + mi := &file_internal_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 MVCCVersion.ProtoReflect.Descriptor instead. +func (*MVCCVersion) Descriptor() ([]byte, []int) { + return file_internal_proto_rawDescGZIP(), []int{9} +} + +func (x *MVCCVersion) GetKey() []byte { + if x != nil { + return x.Key + } + return nil +} + +func (x *MVCCVersion) GetCommitTs() uint64 { + if x != nil { + return x.CommitTs + } + return 0 +} + +func (x *MVCCVersion) GetTombstone() bool { + if x != nil { + return x.Tombstone + } + return false +} + +func (x *MVCCVersion) GetValue() []byte { + if x != nil { + return x.Value + } + return nil +} + +func (x *MVCCVersion) GetKeyFamily() uint32 { + if x != nil { + return x.KeyFamily + } + return 0 +} + +func (x *MVCCVersion) GetExpireAt() uint64 { + if x != nil { + return x.ExpireAt + } + return 0 +} + +type ImportRangeVersionsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + JobId uint64 `protobuf:"varint,1,opt,name=job_id,json=jobId,proto3" json:"job_id,omitempty"` + Versions []*MVCCVersion `protobuf:"bytes,2,rep,name=versions,proto3" json:"versions,omitempty"` + Cursor []byte `protobuf:"bytes,3,opt,name=cursor,proto3" json:"cursor,omitempty"` + BracketId uint64 `protobuf:"varint,4,opt,name=bracket_id,json=bracketId,proto3" json:"bracket_id,omitempty"` + BatchSeq uint64 `protobuf:"varint,5,opt,name=batch_seq,json=batchSeq,proto3" json:"batch_seq,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ImportRangeVersionsRequest) Reset() { + *x = ImportRangeVersionsRequest{} + mi := &file_internal_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ImportRangeVersionsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ImportRangeVersionsRequest) ProtoMessage() {} + +func (x *ImportRangeVersionsRequest) ProtoReflect() protoreflect.Message { + mi := &file_internal_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 ImportRangeVersionsRequest.ProtoReflect.Descriptor instead. +func (*ImportRangeVersionsRequest) Descriptor() ([]byte, []int) { + return file_internal_proto_rawDescGZIP(), []int{10} +} + +func (x *ImportRangeVersionsRequest) GetJobId() uint64 { + if x != nil { + return x.JobId + } + return 0 +} + +func (x *ImportRangeVersionsRequest) GetVersions() []*MVCCVersion { + if x != nil { + return x.Versions + } + return nil +} + +func (x *ImportRangeVersionsRequest) GetCursor() []byte { + if x != nil { + return x.Cursor + } + return nil +} + +func (x *ImportRangeVersionsRequest) GetBracketId() uint64 { + if x != nil { + return x.BracketId + } + return 0 +} + +func (x *ImportRangeVersionsRequest) GetBatchSeq() uint64 { + if x != nil { + return x.BatchSeq + } + return 0 +} + +type ImportRangeVersionsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + AckedCursor []byte `protobuf:"bytes,1,opt,name=acked_cursor,json=ackedCursor,proto3" json:"acked_cursor,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ImportRangeVersionsResponse) Reset() { + *x = ImportRangeVersionsResponse{} + mi := &file_internal_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ImportRangeVersionsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ImportRangeVersionsResponse) ProtoMessage() {} + +func (x *ImportRangeVersionsResponse) ProtoReflect() protoreflect.Message { + mi := &file_internal_proto_msgTypes[11] + 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 ImportRangeVersionsResponse.ProtoReflect.Descriptor instead. +func (*ImportRangeVersionsResponse) Descriptor() ([]byte, []int) { + return file_internal_proto_rawDescGZIP(), []int{11} +} + +func (x *ImportRangeVersionsResponse) GetAckedCursor() []byte { + if x != nil { + return x.AckedCursor + } + return nil +} + var File_internal_proto protoreflect.FileDescriptor const file_internal_proto_rawDesc = "" + @@ -557,7 +929,42 @@ const file_internal_proto_rawDesc = "" + "\achannel\x18\x01 \x01(\fR\achannel\x12\x18\n" + "\amessage\x18\x02 \x01(\fR\amessage\"8\n" + "\x14RelayPublishResponse\x12 \n" + - "\vsubscribers\x18\x01 \x01(\x03R\vsubscribers*&\n" + + "\vsubscribers\x18\x01 \x01(\x03R\vsubscribers\"\xc5\x02\n" + + "\x1aExportRangeVersionsRequest\x12\x1f\n" + + "\vrange_start\x18\x01 \x01(\fR\n" + + "rangeStart\x12\x1b\n" + + "\trange_end\x18\x02 \x01(\fR\brangeEnd\x12\"\n" + + "\rmax_commit_ts\x18\x03 \x01(\x04R\vmaxCommitTs\x12\"\n" + + "\rmin_commit_ts\x18\x04 \x01(\x04R\vminCommitTs\x12\x16\n" + + "\x06cursor\x18\x05 \x01(\fR\x06cursor\x12\x1f\n" + + "\vchunk_bytes\x18\x06 \x01(\rR\n" + + "chunkBytes\x12\x1f\n" + + "\vroute_start\x18\a \x01(\fR\n" + + "routeStart\x12\x1b\n" + + "\troute_end\x18\b \x01(\fR\brouteEnd\x12*\n" + + "\x11max_scanned_bytes\x18\t \x01(\x04R\x0fmaxScannedBytes\"|\n" + + "\x1bExportRangeVersionsResponse\x12(\n" + + "\bversions\x18\x01 \x03(\v2\f.MVCCVersionR\bversions\x12\x1f\n" + + "\vnext_cursor\x18\x02 \x01(\fR\n" + + "nextCursor\x12\x12\n" + + "\x04done\x18\x03 \x01(\bR\x04done\"\xac\x01\n" + + "\vMVCCVersion\x12\x10\n" + + "\x03key\x18\x01 \x01(\fR\x03key\x12\x1b\n" + + "\tcommit_ts\x18\x02 \x01(\x04R\bcommitTs\x12\x1c\n" + + "\ttombstone\x18\x03 \x01(\bR\ttombstone\x12\x14\n" + + "\x05value\x18\x04 \x01(\fR\x05value\x12\x1d\n" + + "\n" + + "key_family\x18\x05 \x01(\rR\tkeyFamily\x12\x1b\n" + + "\texpire_at\x18\x06 \x01(\x04R\bexpireAt\"\xb1\x01\n" + + "\x1aImportRangeVersionsRequest\x12\x15\n" + + "\x06job_id\x18\x01 \x01(\x04R\x05jobId\x12(\n" + + "\bversions\x18\x02 \x03(\v2\f.MVCCVersionR\bversions\x12\x16\n" + + "\x06cursor\x18\x03 \x01(\fR\x06cursor\x12\x1d\n" + + "\n" + + "bracket_id\x18\x04 \x01(\x04R\tbracketId\x12\x1b\n" + + "\tbatch_seq\x18\x05 \x01(\x04R\bbatchSeq\"@\n" + + "\x1bImportRangeVersionsResponse\x12!\n" + + "\facked_cursor\x18\x01 \x01(\fR\vackedCursor*&\n" + "\x02Op\x12\a\n" + "\x03PUT\x10\x00\x12\a\n" + "\x03DEL\x10\x01\x12\x0e\n" + @@ -568,10 +975,12 @@ const file_internal_proto_rawDesc = "" + "\aPREPARE\x10\x01\x12\n" + "\n" + "\x06COMMIT\x10\x02\x12\t\n" + - "\x05ABORT\x10\x032y\n" + + "\x05ABORT\x10\x032\xa3\x02\n" + "\bInternal\x12.\n" + "\aForward\x12\x0f.ForwardRequest\x1a\x10.ForwardResponse\"\x00\x12=\n" + - "\fRelayPublish\x12\x14.RelayPublishRequest\x1a\x15.RelayPublishResponse\"\x00B#Z!github.com/bootjp/elastickv/protob\x06proto3" + "\fRelayPublish\x12\x14.RelayPublishRequest\x1a\x15.RelayPublishResponse\"\x00\x12T\n" + + "\x13ExportRangeVersions\x12\x1b.ExportRangeVersionsRequest\x1a\x1c.ExportRangeVersionsResponse\"\x000\x01\x12R\n" + + "\x13ImportRangeVersions\x12\x1b.ImportRangeVersionsRequest\x1a\x1c.ImportRangeVersionsResponse\"\x00B#Z!github.com/bootjp/elastickv/protob\x06proto3" var ( file_internal_proto_rawDescOnce sync.Once @@ -586,33 +995,44 @@ func file_internal_proto_rawDescGZIP() []byte { } var file_internal_proto_enumTypes = make([]protoimpl.EnumInfo, 2) -var file_internal_proto_msgTypes = make([]protoimpl.MessageInfo, 7) +var file_internal_proto_msgTypes = make([]protoimpl.MessageInfo, 12) var file_internal_proto_goTypes = []any{ - (Op)(0), // 0: Op - (Phase)(0), // 1: Phase - (*Mutation)(nil), // 2: Mutation - (*Request)(nil), // 3: Request - (*RaftCommand)(nil), // 4: RaftCommand - (*ForwardRequest)(nil), // 5: ForwardRequest - (*ForwardResponse)(nil), // 6: ForwardResponse - (*RelayPublishRequest)(nil), // 7: RelayPublishRequest - (*RelayPublishResponse)(nil), // 8: RelayPublishResponse + (Op)(0), // 0: Op + (Phase)(0), // 1: Phase + (*Mutation)(nil), // 2: Mutation + (*Request)(nil), // 3: Request + (*RaftCommand)(nil), // 4: RaftCommand + (*ForwardRequest)(nil), // 5: ForwardRequest + (*ForwardResponse)(nil), // 6: ForwardResponse + (*RelayPublishRequest)(nil), // 7: RelayPublishRequest + (*RelayPublishResponse)(nil), // 8: RelayPublishResponse + (*ExportRangeVersionsRequest)(nil), // 9: ExportRangeVersionsRequest + (*ExportRangeVersionsResponse)(nil), // 10: ExportRangeVersionsResponse + (*MVCCVersion)(nil), // 11: MVCCVersion + (*ImportRangeVersionsRequest)(nil), // 12: ImportRangeVersionsRequest + (*ImportRangeVersionsResponse)(nil), // 13: ImportRangeVersionsResponse } var file_internal_proto_depIdxs = []int32{ - 0, // 0: Mutation.op:type_name -> Op - 1, // 1: Request.phase:type_name -> Phase - 2, // 2: Request.mutations:type_name -> Mutation - 3, // 3: RaftCommand.requests:type_name -> Request - 3, // 4: ForwardRequest.requests:type_name -> Request - 5, // 5: Internal.Forward:input_type -> ForwardRequest - 7, // 6: Internal.RelayPublish:input_type -> RelayPublishRequest - 6, // 7: Internal.Forward:output_type -> ForwardResponse - 8, // 8: Internal.RelayPublish:output_type -> RelayPublishResponse - 7, // [7:9] is the sub-list for method output_type - 5, // [5:7] is the sub-list for method input_type - 5, // [5:5] is the sub-list for extension type_name - 5, // [5:5] is the sub-list for extension extendee - 0, // [0:5] is the sub-list for field type_name + 0, // 0: Mutation.op:type_name -> Op + 1, // 1: Request.phase:type_name -> Phase + 2, // 2: Request.mutations:type_name -> Mutation + 3, // 3: RaftCommand.requests:type_name -> Request + 3, // 4: ForwardRequest.requests:type_name -> Request + 11, // 5: ExportRangeVersionsResponse.versions:type_name -> MVCCVersion + 11, // 6: ImportRangeVersionsRequest.versions:type_name -> MVCCVersion + 5, // 7: Internal.Forward:input_type -> ForwardRequest + 7, // 8: Internal.RelayPublish:input_type -> RelayPublishRequest + 9, // 9: Internal.ExportRangeVersions:input_type -> ExportRangeVersionsRequest + 12, // 10: Internal.ImportRangeVersions:input_type -> ImportRangeVersionsRequest + 6, // 11: Internal.Forward:output_type -> ForwardResponse + 8, // 12: Internal.RelayPublish:output_type -> RelayPublishResponse + 10, // 13: Internal.ExportRangeVersions:output_type -> ExportRangeVersionsResponse + 13, // 14: Internal.ImportRangeVersions:output_type -> ImportRangeVersionsResponse + 11, // [11:15] is the sub-list for method output_type + 7, // [7:11] is the sub-list for method input_type + 7, // [7:7] is the sub-list for extension type_name + 7, // [7:7] is the sub-list for extension extendee + 0, // [0:7] is the sub-list for field type_name } func init() { file_internal_proto_init() } @@ -626,7 +1046,7 @@ func file_internal_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_internal_proto_rawDesc), len(file_internal_proto_rawDesc)), NumEnums: 2, - NumMessages: 7, + NumMessages: 12, NumExtensions: 0, NumServices: 1, }, diff --git a/proto/internal.proto b/proto/internal.proto index 53215dc91..1f7c77712 100644 --- a/proto/internal.proto +++ b/proto/internal.proto @@ -7,6 +7,8 @@ service Internal { // for internal leader redirect only rpc Forward(ForwardRequest) returns (ForwardResponse) {} rpc RelayPublish(RelayPublishRequest) returns (RelayPublishResponse) {} + rpc ExportRangeVersions(ExportRangeVersionsRequest) returns (stream ExportRangeVersionsResponse) {} + rpc ImportRangeVersions(ImportRangeVersionsRequest) returns (ImportRangeVersionsResponse) {} } // internal.proto is node to node communication message in raft replication. @@ -78,3 +80,42 @@ message RelayPublishRequest { message RelayPublishResponse { int64 subscribers = 1; } + +message ExportRangeVersionsRequest { + bytes range_start = 1; + bytes range_end = 2; + uint64 max_commit_ts = 3; + uint64 min_commit_ts = 4; + bytes cursor = 5; + uint32 chunk_bytes = 6; + bytes route_start = 7; + bytes route_end = 8; + uint64 max_scanned_bytes = 9; +} + +message ExportRangeVersionsResponse { + repeated MVCCVersion versions = 1; + bytes next_cursor = 2; + bool done = 3; +} + +message MVCCVersion { + bytes key = 1; + uint64 commit_ts = 2; + bool tombstone = 3; + bytes value = 4; + uint32 key_family = 5; + uint64 expire_at = 6; +} + +message ImportRangeVersionsRequest { + uint64 job_id = 1; + repeated MVCCVersion versions = 2; + bytes cursor = 3; + uint64 bracket_id = 4; + uint64 batch_seq = 5; +} + +message ImportRangeVersionsResponse { + bytes acked_cursor = 1; +} diff --git a/proto/internal_grpc.pb.go b/proto/internal_grpc.pb.go index 3828db02a..6a21b9eba 100644 --- a/proto/internal_grpc.pb.go +++ b/proto/internal_grpc.pb.go @@ -19,8 +19,10 @@ import ( const _ = grpc.SupportPackageIsVersion9 const ( - Internal_Forward_FullMethodName = "/Internal/Forward" - Internal_RelayPublish_FullMethodName = "/Internal/RelayPublish" + Internal_Forward_FullMethodName = "/Internal/Forward" + Internal_RelayPublish_FullMethodName = "/Internal/RelayPublish" + Internal_ExportRangeVersions_FullMethodName = "/Internal/ExportRangeVersions" + Internal_ImportRangeVersions_FullMethodName = "/Internal/ImportRangeVersions" ) // InternalClient is the client API for Internal service. @@ -30,6 +32,8 @@ type InternalClient interface { // for internal leader redirect only Forward(ctx context.Context, in *ForwardRequest, opts ...grpc.CallOption) (*ForwardResponse, error) RelayPublish(ctx context.Context, in *RelayPublishRequest, opts ...grpc.CallOption) (*RelayPublishResponse, error) + ExportRangeVersions(ctx context.Context, in *ExportRangeVersionsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ExportRangeVersionsResponse], error) + ImportRangeVersions(ctx context.Context, in *ImportRangeVersionsRequest, opts ...grpc.CallOption) (*ImportRangeVersionsResponse, error) } type internalClient struct { @@ -60,6 +64,35 @@ func (c *internalClient) RelayPublish(ctx context.Context, in *RelayPublishReque return out, nil } +func (c *internalClient) ExportRangeVersions(ctx context.Context, in *ExportRangeVersionsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ExportRangeVersionsResponse], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &Internal_ServiceDesc.Streams[0], Internal_ExportRangeVersions_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[ExportRangeVersionsRequest, ExportRangeVersionsResponse]{ClientStream: stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type Internal_ExportRangeVersionsClient = grpc.ServerStreamingClient[ExportRangeVersionsResponse] + +func (c *internalClient) ImportRangeVersions(ctx context.Context, in *ImportRangeVersionsRequest, opts ...grpc.CallOption) (*ImportRangeVersionsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ImportRangeVersionsResponse) + err := c.cc.Invoke(ctx, Internal_ImportRangeVersions_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + // InternalServer is the server API for Internal service. // All implementations must embed UnimplementedInternalServer // for forward compatibility. @@ -67,6 +100,8 @@ type InternalServer interface { // for internal leader redirect only Forward(context.Context, *ForwardRequest) (*ForwardResponse, error) RelayPublish(context.Context, *RelayPublishRequest) (*RelayPublishResponse, error) + ExportRangeVersions(*ExportRangeVersionsRequest, grpc.ServerStreamingServer[ExportRangeVersionsResponse]) error + ImportRangeVersions(context.Context, *ImportRangeVersionsRequest) (*ImportRangeVersionsResponse, error) mustEmbedUnimplementedInternalServer() } @@ -83,6 +118,12 @@ func (UnimplementedInternalServer) Forward(context.Context, *ForwardRequest) (*F func (UnimplementedInternalServer) RelayPublish(context.Context, *RelayPublishRequest) (*RelayPublishResponse, error) { return nil, status.Error(codes.Unimplemented, "method RelayPublish not implemented") } +func (UnimplementedInternalServer) ExportRangeVersions(*ExportRangeVersionsRequest, grpc.ServerStreamingServer[ExportRangeVersionsResponse]) error { + return status.Error(codes.Unimplemented, "method ExportRangeVersions not implemented") +} +func (UnimplementedInternalServer) ImportRangeVersions(context.Context, *ImportRangeVersionsRequest) (*ImportRangeVersionsResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ImportRangeVersions not implemented") +} func (UnimplementedInternalServer) mustEmbedUnimplementedInternalServer() {} func (UnimplementedInternalServer) testEmbeddedByValue() {} @@ -140,6 +181,35 @@ func _Internal_RelayPublish_Handler(srv interface{}, ctx context.Context, dec fu return interceptor(ctx, in, info, handler) } +func _Internal_ExportRangeVersions_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(ExportRangeVersionsRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(InternalServer).ExportRangeVersions(m, &grpc.GenericServerStream[ExportRangeVersionsRequest, ExportRangeVersionsResponse]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type Internal_ExportRangeVersionsServer = grpc.ServerStreamingServer[ExportRangeVersionsResponse] + +func _Internal_ImportRangeVersions_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ImportRangeVersionsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(InternalServer).ImportRangeVersions(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Internal_ImportRangeVersions_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(InternalServer).ImportRangeVersions(ctx, req.(*ImportRangeVersionsRequest)) + } + return interceptor(ctx, in, info, handler) +} + // Internal_ServiceDesc is the grpc.ServiceDesc for Internal service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -155,7 +225,17 @@ var Internal_ServiceDesc = grpc.ServiceDesc{ MethodName: "RelayPublish", Handler: _Internal_RelayPublish_Handler, }, + { + MethodName: "ImportRangeVersions", + Handler: _Internal_ImportRangeVersions_Handler, + }, + }, + Streams: []grpc.StreamDesc{ + { + StreamName: "ExportRangeVersions", + Handler: _Internal_ExportRangeVersions_Handler, + ServerStreams: true, + }, }, - Streams: []grpc.StreamDesc{}, Metadata: "internal.proto", } diff --git a/proto/service.pb.go b/proto/service.pb.go index a869e0c1d..274372e83 100644 --- a/proto/service.pb.go +++ b/proto/service.pb.go @@ -181,12 +181,13 @@ func (x *RawPutResponse) GetSuccess() bool { } type RawGetRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` - Ts uint64 `protobuf:"varint,3,opt,name=ts,proto3" json:"ts,omitempty"` // optional read timestamp; if zero, server uses current HLC - GroupId uint64 `protobuf:"varint,4,opt,name=group_id,json=groupId,proto3" json:"group_id,omitempty"` // optional explicit Raft group for non-range-owned keyspaces - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + Ts uint64 `protobuf:"varint,3,opt,name=ts,proto3" json:"ts,omitempty"` // optional read timestamp; if zero, server uses current HLC + GroupId uint64 `protobuf:"varint,4,opt,name=group_id,json=groupId,proto3" json:"group_id,omitempty"` // optional explicit Raft group for non-range-owned keyspaces + ReadRouteVersion uint64 `protobuf:"varint,5,opt,name=read_route_version,json=readRouteVersion,proto3" json:"read_route_version,omitempty"` // stamped by server-side routing for migration read fences + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *RawGetRequest) Reset() { @@ -240,6 +241,13 @@ func (x *RawGetRequest) GetGroupId() uint64 { return 0 } +func (x *RawGetRequest) GetReadRouteVersion() uint64 { + if x != nil { + return x.ReadRouteVersion + } + return 0 +} + type RawGetResponse struct { state protoimpl.MessageState `protogen:"open.v1"` ReadAtIndex uint64 `protobuf:"varint,1,opt,name=read_at_index,json=readAtIndex,proto3" json:"read_at_index,omitempty"` @@ -397,10 +405,11 @@ func (x *RawDeleteResponse) GetSuccess() bool { } type RawLatestCommitTSRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + ReadRouteVersion uint64 `protobuf:"varint,2,opt,name=read_route_version,json=readRouteVersion,proto3" json:"read_route_version,omitempty"` // stamped by server-side routing for migration read fences + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *RawLatestCommitTSRequest) Reset() { @@ -440,6 +449,13 @@ func (x *RawLatestCommitTSRequest) GetKey() []byte { return nil } +func (x *RawLatestCommitTSRequest) GetReadRouteVersion() uint64 { + if x != nil { + return x.ReadRouteVersion + } + return 0 +} + type RawLatestCommitTSResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Ts uint64 `protobuf:"varint,1,opt,name=ts,proto3" json:"ts,omitempty"` @@ -493,15 +509,19 @@ func (x *RawLatestCommitTSResponse) GetExists() bool { } type RawScanAtRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - StartKey []byte `protobuf:"bytes,1,opt,name=start_key,json=startKey,proto3" json:"start_key,omitempty"` - EndKey []byte `protobuf:"bytes,2,opt,name=end_key,json=endKey,proto3" json:"end_key,omitempty"` - Limit int64 `protobuf:"varint,3,opt,name=limit,proto3" json:"limit,omitempty"` // validated against host int size; large values may be rejected - Ts uint64 `protobuf:"varint,4,opt,name=ts,proto3" json:"ts,omitempty"` // optional read timestamp; if zero, server uses current HLC - Reverse bool `protobuf:"varint,5,opt,name=reverse,proto3" json:"reverse,omitempty"` - GroupId uint64 `protobuf:"varint,6,opt,name=group_id,json=groupId,proto3" json:"group_id,omitempty"` // optional explicit Raft group for non-range-owned keyspaces - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + StartKey []byte `protobuf:"bytes,1,opt,name=start_key,json=startKey,proto3" json:"start_key,omitempty"` + EndKey []byte `protobuf:"bytes,2,opt,name=end_key,json=endKey,proto3" json:"end_key,omitempty"` + Limit int64 `protobuf:"varint,3,opt,name=limit,proto3" json:"limit,omitempty"` // validated against host int size; large values may be rejected + Ts uint64 `protobuf:"varint,4,opt,name=ts,proto3" json:"ts,omitempty"` // optional read timestamp; if zero, server uses current HLC + Reverse bool `protobuf:"varint,5,opt,name=reverse,proto3" json:"reverse,omitempty"` + GroupId uint64 `protobuf:"varint,6,opt,name=group_id,json=groupId,proto3" json:"group_id,omitempty"` // optional explicit Raft group for non-range-owned keyspaces + ReadRouteVersion uint64 `protobuf:"varint,7,opt,name=read_route_version,json=readRouteVersion,proto3" json:"read_route_version,omitempty"` // stamped by server-side routing for migration read fences + RouteStart []byte `protobuf:"bytes,8,opt,name=route_start,json=routeStart,proto3" json:"route_start,omitempty"` // route-key-normalized inclusive start, when already known + RouteEnd []byte `protobuf:"bytes,9,opt,name=route_end,json=routeEnd,proto3" json:"route_end,omitempty"` // route-key-normalized exclusive end; empty means +infinity + RouteBoundsPresent bool `protobuf:"varint,10,opt,name=route_bounds_present,json=routeBoundsPresent,proto3" json:"route_bounds_present,omitempty"` // true when route_start/route_end were supplied, including ["", +infinity) + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *RawScanAtRequest) Reset() { @@ -576,6 +596,34 @@ func (x *RawScanAtRequest) GetGroupId() uint64 { return 0 } +func (x *RawScanAtRequest) GetReadRouteVersion() uint64 { + if x != nil { + return x.ReadRouteVersion + } + return 0 +} + +func (x *RawScanAtRequest) GetRouteStart() []byte { + if x != nil { + return x.RouteStart + } + return nil +} + +func (x *RawScanAtRequest) GetRouteEnd() []byte { + if x != nil { + return x.RouteEnd + } + return nil +} + +func (x *RawScanAtRequest) GetRouteBoundsPresent() bool { + if x != nil { + return x.RouteBoundsPresent + } + return false +} + type RawKVPair struct { state protoimpl.MessageState `protogen:"open.v1"` Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` @@ -2261,11 +2309,12 @@ const file_service_proto_rawDesc = "" + "\x05value\x18\x02 \x01(\fR\x05value\"M\n" + "\x0eRawPutResponse\x12!\n" + "\fcommit_index\x18\x01 \x01(\x04R\vcommitIndex\x12\x18\n" + - "\asuccess\x18\x02 \x01(\bR\asuccess\"L\n" + + "\asuccess\x18\x02 \x01(\bR\asuccess\"z\n" + "\rRawGetRequest\x12\x10\n" + "\x03key\x18\x01 \x01(\fR\x03key\x12\x0e\n" + "\x02ts\x18\x03 \x01(\x04R\x02ts\x12\x19\n" + - "\bgroup_id\x18\x04 \x01(\x04R\agroupId\"b\n" + + "\bgroup_id\x18\x04 \x01(\x04R\agroupId\x12,\n" + + "\x12read_route_version\x18\x05 \x01(\x04R\x10readRouteVersion\"b\n" + "\x0eRawGetResponse\x12\"\n" + "\rread_at_index\x18\x01 \x01(\x04R\vreadAtIndex\x12\x14\n" + "\x05value\x18\x02 \x01(\fR\x05value\x12\x16\n" + @@ -2274,19 +2323,26 @@ const file_service_proto_rawDesc = "" + "\x03key\x18\x01 \x01(\fR\x03key\"P\n" + "\x11RawDeleteResponse\x12!\n" + "\fcommit_index\x18\x01 \x01(\x04R\vcommitIndex\x12\x18\n" + - "\asuccess\x18\x02 \x01(\bR\asuccess\",\n" + + "\asuccess\x18\x02 \x01(\bR\asuccess\"Z\n" + "\x18RawLatestCommitTSRequest\x12\x10\n" + - "\x03key\x18\x01 \x01(\fR\x03key\"C\n" + + "\x03key\x18\x01 \x01(\fR\x03key\x12,\n" + + "\x12read_route_version\x18\x02 \x01(\x04R\x10readRouteVersion\"C\n" + "\x19RawLatestCommitTSResponse\x12\x0e\n" + "\x02ts\x18\x01 \x01(\x04R\x02ts\x12\x16\n" + - "\x06exists\x18\x02 \x01(\bR\x06exists\"\xa3\x01\n" + + "\x06exists\x18\x02 \x01(\bR\x06exists\"\xc1\x02\n" + "\x10RawScanAtRequest\x12\x1b\n" + "\tstart_key\x18\x01 \x01(\fR\bstartKey\x12\x17\n" + "\aend_key\x18\x02 \x01(\fR\x06endKey\x12\x14\n" + "\x05limit\x18\x03 \x01(\x03R\x05limit\x12\x0e\n" + "\x02ts\x18\x04 \x01(\x04R\x02ts\x12\x18\n" + "\areverse\x18\x05 \x01(\bR\areverse\x12\x19\n" + - "\bgroup_id\x18\x06 \x01(\x04R\agroupId\"3\n" + + "\bgroup_id\x18\x06 \x01(\x04R\agroupId\x12,\n" + + "\x12read_route_version\x18\a \x01(\x04R\x10readRouteVersion\x12\x1f\n" + + "\vroute_start\x18\b \x01(\fR\n" + + "routeStart\x12\x1b\n" + + "\troute_end\x18\t \x01(\fR\brouteEnd\x120\n" + + "\x14route_bounds_present\x18\n" + + " \x01(\bR\x12routeBoundsPresent\"3\n" + "\tRawKVPair\x12\x10\n" + "\x03key\x18\x01 \x01(\fR\x03key\x12\x14\n" + "\x05value\x18\x02 \x01(\fR\x05value\"/\n" + diff --git a/proto/service.proto b/proto/service.proto index b1ac9105f..772fe2a69 100644 --- a/proto/service.proto +++ b/proto/service.proto @@ -45,6 +45,7 @@ message RawGetRequest { bytes key = 1; uint64 ts = 3; // optional read timestamp; if zero, server uses current HLC uint64 group_id = 4; // optional explicit Raft group for non-range-owned keyspaces + uint64 read_route_version = 5; // stamped by server-side routing for migration read fences } message RawGetResponse { @@ -64,6 +65,7 @@ message RawDeleteResponse { message RawLatestCommitTSRequest { bytes key = 1; + uint64 read_route_version = 2; // stamped by server-side routing for migration read fences } message RawLatestCommitTSResponse { @@ -78,6 +80,10 @@ message RawScanAtRequest { uint64 ts = 4; // optional read timestamp; if zero, server uses current HLC bool reverse = 5; uint64 group_id = 6; // optional explicit Raft group for non-range-owned keyspaces + uint64 read_route_version = 7; // stamped by server-side routing for migration read fences + bytes route_start = 8; // route-key-normalized inclusive start, when already known + bytes route_end = 9; // route-key-normalized exclusive end; empty means +infinity + bool route_bounds_present = 10; // true when route_start/route_end were supplied, including ["", +infinity) } message RawKVPair {