diff --git a/adapter/distribution_server.go b/adapter/distribution_server.go index 072294d78..7e4110a99 100644 --- a/adapter/distribution_server.go +++ b/adapter/distribution_server.go @@ -455,7 +455,7 @@ func (s *DistributionServer) saveSplitResultViaCoordinator( left.SplitAtHLC = commitTS right.SplitAtHLC = commitTS - 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) } @@ -522,6 +522,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. @@ -531,14 +532,10 @@ func buildCatalogSplitOps( Key: distribution.CatalogRouteKey(parentID), }) for _, route := range []distribution.RouteDescriptor{left, right} { - encoded, err := distribution.EncodeRouteDescriptor(route) + encoded, patchOffset, err := distribution.EncodeRouteDescriptorForCatalogWriteWithSplitAtHLCOffset(route, allowRouteDescriptorV2Writes) if err != nil { return nil, errors.WithStack(err) } - patchOffset, err := splitAtHLCPatchOffset(encoded) - if err != nil { - return nil, err - } ops = append(ops, &kv.Elem[kv.OP]{ Op: kv.Put, Key: distribution.CatalogRouteKey(route.RouteID), @@ -559,14 +556,6 @@ func buildCatalogSplitOps( return ops, nil } -func splitAtHLCPatchOffset(encoded []byte) (uint64, error) { - const splitAtHLCTailBytes = 8 - if len(encoded) < splitAtHLCTailBytes { - return 0, errors.WithStack(distribution.ErrCatalogInvalidRouteRecord) - } - return uint64(len(encoded) - splitAtHLCTailBytes), nil //nolint:gosec // len was checked to be at least splitAtHLCTailBytes. -} - func splitChildrenFromSnapshot(snapshot distribution.CatalogSnapshot, leftID uint64, rightID uint64) (distribution.RouteDescriptor, distribution.RouteDescriptor, error) { left, found := findRouteByID(snapshot.Routes, leftID) if !found { @@ -692,22 +681,28 @@ 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, - SplitAtHLC: splitAtHLC, + RouteID: leftID, + Start: parent.Start, + End: splitKey, + GroupID: parent.GroupID, + State: parent.State, + ParentRouteID: parent.RouteID, + SplitAtHLC: splitAtHLC, + 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, - SplitAtHLC: splitAtHLC, + RouteID: rightID, + Start: splitKey, + End: parent.End, + GroupID: parent.GroupID, + State: parent.State, + ParentRouteID: parent.RouteID, + SplitAtHLC: splitAtHLC, + StagedVisibilityActive: parent.StagedVisibilityActive, + MigrationJobID: parent.MigrationJobID, + MinWriteTSExclusive: parent.MinWriteTSExclusive, } return left, right } @@ -757,13 +752,16 @@ 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, - SplitAtHlc: route.SplitAtHLC, + RouteId: route.RouteID, + Start: distribution.CloneBytes(route.Start), + End: distribution.CloneBytes(route.End), + RaftGroupId: route.GroupID, + State: toProtoRouteState(route.State), + ParentRouteId: route.ParentRouteID, + SplitAtHlc: route.SplitAtHLC, + StagedVisibilityActive: route.StagedVisibilityActive, + MigrationJobId: route.MigrationJobID, + MinWriteTsExclusive: route.MinWriteTSExclusive, } } diff --git a/adapter/distribution_server_test.go b/adapter/distribution_server_test.go index ce268cddc..514a9608a 100644 --- a/adapter/distribution_server_test.go +++ b/adapter/distribution_server_test.go @@ -97,16 +97,19 @@ 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, - SplitAtHLC: 99, + RouteID: 2, + Start: []byte("m"), + End: nil, + GroupID: 2, + State: distribution.RouteStateWriteFenced, + ParentRouteID: 1, + SplitAtHLC: 77, + StagedVisibilityActive: true, + MigrationJobID: 42, + MinWriteTSExclusive: 99, }, { RouteID: 1, @@ -133,7 +136,10 @@ 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.Equal(t, uint64(99), resp.Routes[1].SplitAtHlc) + require.Equal(t, uint64(77), resp.Routes[1].SplitAtHlc) + 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) { @@ -151,15 +157,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, @@ -192,6 +199,7 @@ 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) @@ -199,6 +207,7 @@ func TestDistributionServerSplitRange_Success(t *testing.T) { require.Equal(t, uint64(1), resp.Right.ParentRouteId) require.NotZero(t, resp.Left.SplitAtHlc) require.Equal(t, resp.Left.SplitAtHlc, resp.Right.SplitAtHlc) + require.Equal(t, uint64(99), resp.Right.MinWriteTsExclusive) snapshot, err := catalog.Snapshot(ctx) require.NoError(t, err) @@ -206,7 +215,9 @@ 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.NotZero(t, snapshot.Routes[0].SplitAtHLC) require.Equal(t, snapshot.Routes[0].SplitAtHLC, snapshot.Routes[1].SplitAtHLC) @@ -216,9 +227,11 @@ func TestDistributionServerSplitRange_Success(t *testing.T) { 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_SnapsFilesystemChunkKeyToFileBoundary(t *testing.T) { @@ -754,7 +767,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 48e384200..3e400e40c 100644 --- a/adapter/grpc.go +++ b/adapter/grpc.go @@ -34,6 +34,30 @@ 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 rawGroupCommitTSReader interface { + LatestCommitTSGroupWithReadFence(ctx context.Context, key []byte, groupID uint64, 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 rawReadFenceKeyScanner interface { + ScanKeysAtWithReadFence(ctx context.Context, start []byte, end []byte, limit int, ts uint64, groupID uint64, readRouteVersion uint64) ([][]byte, error) +} + +type rawReadFenceVersioner interface { + ReadRouteVersion() uint64 +} + type GRPCServerOption func(*GRPCServer) type rawGroupGetter interface { @@ -106,7 +130,11 @@ 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 req.GetReadRouteVersion() != 0 { + return nil, errors.WithStack(status.Error(codes.FailedPrecondition, "raw get with read fence requires a read-fence-aware store")) + } 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")) @@ -145,7 +173,23 @@ 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 + readRouteVersion := r.readRouteVersion(req.GetReadRouteVersion()) + if groupID := req.GetGroupId(); groupID != 0 { + groupReader, ok := r.store.(rawGroupCommitTSReader) + if !ok { + return nil, errors.WithStack(status.Error(codes.FailedPrecondition, "latest commit timestamp for an explicit group requires a group-aware store")) + } + ts, exists, err = groupReader.LatestCommitTSGroupWithReadFence(ctx, key, groupID, readRouteVersion) + } else if fenceReader, ok := r.store.(rawReadFenceCommitTSReader); ok { + ts, exists, err = fenceReader.LatestCommitTSWithReadFence(ctx, key, readRouteVersion) + } else if req.GetReadRouteVersion() != 0 { + return nil, errors.WithStack(status.Error(codes.FailedPrecondition, "latest commit timestamp with read fence requires a read-fence-aware store")) + } else { + ts, exists, err = r.store.LatestCommitTS(ctx, key) + } if err != nil { return nil, errors.WithStack(err) } @@ -175,7 +219,7 @@ func (r *GRPCServer) RawScanAt(ctx context.Context, req *pb.RawScanAtRequest) (* return &pb.RawScanAtResponse{Kv: rawKeyPairs(keys)}, nil } - res, err := r.rawScanValuesAt(ctx, req, limit, readTS) + res, err := r.rawScanAt(ctx, req, limit, readTS) if err != nil { return rawScanErrorResponse(err) } @@ -183,37 +227,49 @@ func (r *GRPCServer) RawScanAt(ctx context.Context, req *pb.RawScanAtRequest) (* return &pb.RawScanAtResponse{Kv: rawKvPairs(res)}, nil } -func (r *GRPCServer) rawScanValuesAt(ctx context.Context, req *pb.RawScanAtRequest, limit int, readTS uint64) ([]*store.KVPair, error) { - var res []*store.KVPair - var err error - if groupID := req.GetGroupId(); groupID != 0 { - res, err = r.rawScanAtExplicitGroup(ctx, req, groupID, 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) +func (r *GRPCServer) rawScanKeysAt(ctx context.Context, req *pb.RawScanAtRequest, limit int, readTS uint64) ([][]byte, error) { + if rawScanCanUseExplicitGroupReverse(req) { + if _, ok := r.store.(rawGroupReverseScanner); ok { + return r.rawScanExplicitGroupKeysAt(ctx, req, req.GetGroupId(), limit, readTS) + } } - if err != nil { - return nil, errors.WithStack(err) + _, readFenceAware := r.store.(rawReadFenceScanner) + if readFenceAware || req.GetRouteBoundsPresent() || req.GetReadRouteVersion() != 0 { + return r.rawScanKeysAtWithReadFence(ctx, req, limit, readTS) } - return res, nil -} - -func (r *GRPCServer) rawScanKeysAt(ctx context.Context, req *pb.RawScanAtRequest, limit int, readTS uint64) ([][]byte, error) { if groupID := req.GetGroupId(); groupID != 0 { return r.rawScanExplicitGroupKeysAt(ctx, req, groupID, limit, readTS) } if req.GetReverse() { - kvs, err := r.store.ReverseScanAt(ctx, req.StartKey, req.EndKey, limit, readTS) - if err != nil { - return nil, errors.WithStack(err) - } - return storeKeysFromKVPairs(kvs), nil + return r.rawReverseScanKeysAt(ctx, req, limit, readTS) } keys, err := r.store.ScanKeysAt(ctx, req.StartKey, req.EndKey, limit, readTS) return keys, errors.WithStack(err) } +func (r *GRPCServer) rawScanKeysAtWithReadFence(ctx context.Context, req *pb.RawScanAtRequest, limit int, readTS uint64) ([][]byte, error) { + 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")) + } + readRouteVersion := r.readRouteVersion(req.GetReadRouteVersion()) + if !req.GetReverse() && !req.GetRouteBoundsPresent() { + if keyScanner, ok := r.store.(rawReadFenceKeyScanner); ok { + keys, err := keyScanner.ScanKeysAtWithReadFence(ctx, req.StartKey, req.EndKey, limit, readTS, req.GetGroupId(), readRouteVersion) + return keys, errors.WithStack(err) + } + } + fenceScanner, ok := r.store.(rawReadFenceScanner) + if !ok { + return nil, errors.WithStack(status.Error(codes.FailedPrecondition, "raw key scan with read fence requires a read-fence-aware store")) + } + routeStart, routeEnd := rawScanRouteBounds(req) + kvs, err := fenceScanner.ScanAtWithReadFence(ctx, req.StartKey, req.EndKey, limit, readTS, req.GetReverse(), req.GetGroupId(), readRouteVersion, routeStart, routeEnd) + if err != nil { + return nil, errors.WithStack(err) + } + return storeKeysFromKVPairs(kvs), nil +} + func (r *GRPCServer) rawScanExplicitGroupKeysAt( ctx context.Context, req *pb.RawScanAtRequest, @@ -243,6 +299,14 @@ func (r *GRPCServer) rawScanExplicitGroupKeysAt( return keys, nil } +func (r *GRPCServer) rawReverseScanKeysAt(ctx context.Context, req *pb.RawScanAtRequest, limit int, readTS uint64) ([][]byte, error) { + kvs, err := r.store.ReverseScanAt(ctx, req.StartKey, req.EndKey, limit, readTS) + if err != nil { + return nil, errors.WithStack(err) + } + return storeKeysFromKVPairs(kvs), nil +} + func rawScanErrorResponse(err error) (*pb.RawScanAtResponse, error) { if errors.Is(err, store.ErrReadTSCompacted) { return &pb.RawScanAtResponse{Kv: nil}, errors.WithStack(status.Error(codes.FailedPrecondition, store.ErrReadTSCompacted.Error())) @@ -250,6 +314,71 @@ func rawScanErrorResponse(err error) (*pb.RawScanAtResponse, error) { return &pb.RawScanAtResponse{Kv: nil}, errors.WithStack(err) } +func (r *GRPCServer) rawScanAt(ctx context.Context, req *pb.RawScanAtRequest, limit int, readTS uint64) ([]*store.KVPair, error) { + if rawScanCanUseExplicitGroupReverse(req) { + if _, ok := r.store.(rawGroupReverseScanner); ok { + return r.rawScanAtExplicitGroup(ctx, req, req.GetGroupId(), limit, readTS) + } + } + 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) + } + return r.rawScanAtWithoutReadFence(ctx, req, limit, readTS) +} + +func rawScanCanUseExplicitGroupReverse(req *pb.RawScanAtRequest) bool { + return req.GetGroupId() != 0 && + req.GetReverse() && + !req.GetRouteBoundsPresent() && + req.GetReadRouteVersion() == 0 +} + +func (r *GRPCServer) rawScanAtWithoutReadFence(ctx context.Context, req *pb.RawScanAtRequest, limit int, readTS uint64) ([]*store.KVPair, error) { + if req.GetRouteBoundsPresent() || req.GetReadRouteVersion() != 0 { + return nil, errors.WithStack(status.Error(codes.FailedPrecondition, "raw scan with read fence requires a read-fence-aware store")) + } + if groupID := req.GetGroupId(); groupID != 0 { + return r.rawScanAtExplicitGroup(ctx, req, groupID, limit, readTS) + } + 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 || !req.GetRouteBoundsPresent() { + return nil, nil + } + routeStart := req.GetRouteStart() + routeEnd := req.GetRouteEnd() + 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 (r *GRPCServer) rawScanAtExplicitGroup( ctx context.Context, req *pb.RawScanAtRequest, diff --git a/adapter/grpc_test.go b/adapter/grpc_test.go index 7d936c913..b25d6c3b4 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 TestRawKeyPairsPreservesNilAndEmptyKeys(t *testing.T) { @@ -251,6 +254,447 @@ 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 + latestGroupID uint64 + latestGroupReadVersion uint64 + scanReadRouteVersion uint64 + scanReadRouteStart []byte + scanReadRouteEnd []byte + scanReverse bool + scanGroupID uint64 + scanRouteBoundsPresent bool + keyScanCalled bool + keyScanReadRouteVersion uint64 + keyScanGroupID uint64 + callerSuppliedGetSeen uint64 + callerSuppliedScanSeen uint64 + callerSuppliedLatestSeen uint64 +} + +type recordingReadFenceGroupStore struct { + *recordingRawGroupStore + + readFenceScanCalled bool +} + +func (s *recordingReadFenceGroupStore) ReadRouteVersion() uint64 { + return 55 +} + +func (s *recordingReadFenceGroupStore) ScanAtWithReadFence(context.Context, []byte, []byte, int, uint64, bool, uint64, uint64, []byte, []byte) ([]*store.KVPair, error) { + s.readFenceScanCalled = true + return []*store.KVPair{}, nil +} + +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) LatestCommitTSGroupWithReadFence(_ context.Context, _ []byte, groupID uint64, readRouteVersion uint64) (uint64, bool, error) { + s.latestGroupID = groupID + s.latestGroupReadVersion = readRouteVersion + return 11, 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 (s *recordingRawReadFenceStore) ScanKeysAtWithReadFence(_ context.Context, start []byte, _ []byte, _ int, _ uint64, groupID uint64, readRouteVersion uint64) ([][]byte, error) { + s.keyScanCalled = true + s.keyScanReadRouteVersion = readRouteVersion + s.keyScanGroupID = groupID + return [][]byte{append([]byte(nil), start...)}, 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_RawLatestCommitTS_UsesExplicitGroup(t *testing.T) { + t.Parallel() + + ctx := context.Background() + st := &recordingRawReadFenceStore{MVCCStore: store.NewMVCCStore(), routeVersion: 55} + s := NewGRPCServer(st, nil) + + resp, err := s.RawLatestCommitTS(ctx, &pb.RawLatestCommitTSRequest{Key: []byte("k"), GroupId: 42, ReadRouteVersion: 98}) + require.NoError(t, err) + require.True(t, resp.GetExists()) + require.Equal(t, uint64(11), resp.GetTs()) + require.Equal(t, uint64(42), st.latestGroupID) + require.Equal(t, uint64(98), st.latestGroupReadVersion) + require.Zero(t, st.latestReadRouteVersion) +} + +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"), + RouteBoundsPresent: true, + }) + 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_ReadFenceVariants(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + req *pb.RawScanAtRequest + wireRoundTrip bool + wantRouteVersion uint64 + wantBoundsPresent bool + wantRouteStart []byte + wantRouteEnd []byte + wantKeysOnly bool + }{ + { + name: "preserves empty full-range bounds across proto", + req: &pb.RawScanAtRequest{ + StartKey: []byte("!redis|meta|"), + EndKey: []byte("!redis|meta}"), + Limit: 10, + Ts: 10, + ReadRouteVersion: 97, + RouteStart: []byte{}, + RouteEnd: []byte{}, + RouteBoundsPresent: true, + }, + wireRoundTrip: true, + wantRouteVersion: 97, + wantBoundsPresent: true, + wantRouteStart: []byte{}, + wantRouteEnd: []byte{}, + }, + { + name: "ignores bytes when bounds presence is false", + req: &pb.RawScanAtRequest{ + StartKey: []byte("!redis|meta|"), + EndKey: []byte("!redis|meta}"), + Limit: 10, + Ts: 10, + ReadRouteVersion: 97, + RouteStart: []byte("m"), + RouteEnd: []byte("z"), + }, + wantRouteVersion: 97, + }, + { + name: "keys-only stamps current version without caller fields", + req: &pb.RawScanAtRequest{ + StartKey: []byte("a"), + EndKey: []byte("z"), + Limit: 10, + Ts: 10, + KeysOnly: true, + }, + wantRouteVersion: 55, + wantKeysOnly: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + ctx := context.Background() + st := &recordingRawReadFenceStore{MVCCStore: store.NewMVCCStore(), routeVersion: 55} + s := NewGRPCServer(st, nil) + req := tc.req + if tc.wireRoundTrip { + wire, err := goproto.Marshal(req) + require.NoError(t, err) + decoded := new(pb.RawScanAtRequest) + require.NoError(t, goproto.Unmarshal(wire, decoded)) + require.True(t, decoded.GetRouteBoundsPresent()) + require.Nil(t, decoded.RouteStart) + require.Nil(t, decoded.RouteEnd) + req = decoded + } + + resp, err := s.RawScanAt(ctx, req) + require.NoError(t, err) + require.Len(t, resp.GetKv(), 1) + if tc.wantKeysOnly { + require.Empty(t, resp.GetKv()[0].GetValue()) + } + if tc.wantKeysOnly && !tc.wantBoundsPresent { + require.True(t, st.keyScanCalled) + require.Equal(t, tc.wantRouteVersion, st.keyScanReadRouteVersion) + require.Zero(t, st.scanReadRouteVersion) + } else { + require.Equal(t, tc.wantRouteVersion, st.scanReadRouteVersion) + } + require.Equal(t, tc.wantBoundsPresent, st.scanRouteBoundsPresent) + if tc.wantRouteStart == nil { + require.Nil(t, st.scanReadRouteStart) + } else { + require.NotNil(t, st.scanReadRouteStart) + require.Equal(t, tc.wantRouteStart, st.scanReadRouteStart) + } + if tc.wantRouteEnd == nil { + require.Nil(t, st.scanReadRouteEnd) + } else { + require.NotNil(t, st.scanReadRouteEnd) + require.Equal(t, tc.wantRouteEnd, st.scanReadRouteEnd) + } + }) + } +} + +func TestGRPCServer_RawScanAt_ValueReadFenceRequiresAwareStore(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + name string + req *pb.RawScanAtRequest + }{ + { + name: "route version", + req: &pb.RawScanAtRequest{ + StartKey: []byte("a"), + EndKey: []byte("z"), + Limit: 10, + Ts: 10, + ReadRouteVersion: 7, + }, + }, + { + name: "route bounds", + req: &pb.RawScanAtRequest{ + StartKey: []byte("a"), + EndKey: []byte("z"), + Limit: 10, + Ts: 10, + RouteStart: []byte("m"), + RouteEnd: []byte("z"), + RouteBoundsPresent: true, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + st := store.NewMVCCStore() + t.Cleanup(func() { _ = st.Close() }) + s := NewGRPCServer(st, nil) + _, err := s.RawScanAt(context.Background(), tc.req) + require.Equal(t, codes.FailedPrecondition, status.Code(err)) + }) + } +} + +func TestGRPCServer_RawPointReadsRequireReadFenceAwareStore(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + read func(*GRPCServer) error + }{ + { + name: "get", + read: func(s *GRPCServer) error { + _, err := s.RawGet(context.Background(), &pb.RawGetRequest{ + Key: []byte("k"), + Ts: 10, + ReadRouteVersion: 7, + }) + return err + }, + }, + { + name: "latest commit timestamp", + read: func(s *GRPCServer) error { + _, err := s.RawLatestCommitTS(context.Background(), &pb.RawLatestCommitTSRequest{ + Key: []byte("k"), + ReadRouteVersion: 7, + }) + return err + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + st := store.NewMVCCStore() + t.Cleanup(func() { _ = st.Close() }) + err := tc.read(NewGRPCServer(st, nil)) + require.Equal(t, codes.FailedPrecondition, status.Code(err)) + }) + } +} + +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_RawScanAt_KeysOnlyWithRouteBoundsUsesReadFence(t *testing.T) { + t.Parallel() + + ctx := context.Background() + st := &recordingRawReadFenceStore{MVCCStore: store.NewMVCCStore(), routeVersion: 55} + s := NewGRPCServer(st, nil) + + resp, err := s.RawScanAt(ctx, &pb.RawScanAtRequest{ + StartKey: []byte("!redis|meta|"), + EndKey: []byte("!redis|meta}"), + Limit: 10, + Ts: 10, + GroupId: 42, + KeysOnly: true, + RouteStart: []byte("m"), + RouteBoundsPresent: true, + }) + require.NoError(t, err) + require.Len(t, resp.GetKv(), 1) + require.Equal(t, []byte("!redis|meta|"), resp.GetKv()[0].GetKey()) + require.Empty(t, resp.GetKv()[0].GetValue()) + require.Equal(t, uint64(55), st.scanReadRouteVersion) + require.Equal(t, uint64(42), st.scanGroupID) + require.Equal(t, []byte("m"), st.scanReadRouteStart) + require.NotNil(t, st.scanReadRouteEnd) + require.Empty(t, st.scanReadRouteEnd) +} + +func TestGRPCServer_RawScanAt_KeysOnlyUsesReadFenceKeyScanner(t *testing.T) { + t.Parallel() + + ctx := context.Background() + st := &recordingRawReadFenceStore{MVCCStore: store.NewMVCCStore(), routeVersion: 55} + s := NewGRPCServer(st, nil) + + resp, err := s.RawScanAt(ctx, &pb.RawScanAtRequest{ + StartKey: []byte("a"), + EndKey: []byte("z"), + Limit: 10, + Ts: 10, + GroupId: 42, + KeysOnly: true, + }) + require.NoError(t, err) + require.Len(t, resp.GetKv(), 1) + require.Equal(t, []byte("a"), resp.GetKv()[0].GetKey()) + require.Empty(t, resp.GetKv()[0].GetValue()) + require.True(t, st.keyScanCalled) + require.Equal(t, uint64(55), st.keyScanReadRouteVersion) + require.Equal(t, uint64(42), st.keyScanGroupID) + require.Zero(t, st.scanReadRouteVersion) +} + func TestGRPCServer_RawScanAt_UsesExplicitGroupForReverse(t *testing.T) { t.Parallel() @@ -279,6 +723,49 @@ func TestGRPCServer_RawScanAt_UsesExplicitGroupForReverse(t *testing.T) { require.Equal(t, []byte("a"), resp.GetKv()[1].Key) } +func TestGRPCServer_RawScanAt_ReadFenceAwareStoreUsesExplicitGroupForNonFencedReverse(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + name string + keysOnly bool + }{ + {name: "values"}, + {name: "keys-only", keysOnly: true}, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctx := context.Background() + st := &recordingReadFenceGroupStore{ + recordingRawGroupStore: &recordingRawGroupStore{MVCCStore: store.NewMVCCStore()}, + } + t.Cleanup(func() { _ = st.Close() }) + require.NoError(t, st.PutAt(ctx, []byte("a"), []byte("va"), 9, 0)) + require.NoError(t, st.PutAt(ctx, []byte("b"), []byte("vb"), 10, 0)) + s := NewGRPCServer(st, nil) + + resp, err := s.RawScanAt(ctx, &pb.RawScanAtRequest{ + StartKey: []byte("a"), + EndKey: []byte("z"), + Limit: 10, + Ts: 10, + Reverse: true, + GroupId: 42, + KeysOnly: tc.keysOnly, + }) + require.NoError(t, err) + require.Len(t, resp.GetKv(), 2) + require.Equal(t, []byte("b"), resp.GetKv()[0].GetKey()) + require.Equal(t, []byte("a"), resp.GetKv()[1].GetKey()) + require.True(t, st.reverseScan) + require.False(t, st.fallbackScan) + require.False(t, st.readFenceScanCalled) + require.Equal(t, uint64(42), st.scanGroupID) + }) + } +} + func TestGRPCServer_RawScanAt_KeysOnlyUsesExplicitGroup(t *testing.T) { t.Parallel() diff --git a/distribution/catalog.go b/distribution/catalog.go index a27734c1f..9447a018a 100644 --- a/distribution/catalog.go +++ b/distribution/catalog.go @@ -22,8 +22,11 @@ const ( catalogVersionCodecVersion byte = 1 catalogRouteCodecVersionMin byte = 1 + catalogRouteCodecVersionV1 byte = 1 catalogRouteCodecVersionV2 byte = 2 - catalogRouteCodecVersion byte = catalogRouteCodecVersionV2 + catalogRouteCodecVersionV3 byte = 3 + catalogRouteCodecVersion byte = catalogRouteCodecVersionV3 + catalogRouteV3TailSize = catalogUint64Bytes + 1 + catalogUint64Bytes + catalogUint64Bytes catalogScanPageSize = 256 catalogSaveMetaMutationCount = 2 @@ -44,6 +47,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. @@ -71,13 +75,16 @@ 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 - SplitAtHLC uint64 + RouteID uint64 + Start []byte + End []byte + GroupID uint64 + State RouteState + ParentRouteID uint64 + StagedVisibilityActive bool + MigrationJobID uint64 + MinWriteTSExclusive uint64 + SplitAtHLC uint64 } // CatalogSnapshot is a point-in-time snapshot of the route catalog. @@ -89,12 +96,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. @@ -170,12 +196,24 @@ func DecodeCatalogNextRouteID(raw []byte) (uint64, error) { // EncodeRouteDescriptor serializes a route descriptor record. func EncodeRouteDescriptor(route RouteDescriptor) ([]byte, error) { + raw, _, err := encodeRouteDescriptorWithSplitAtHLCOffset(route) + return raw, err +} + +func encodeRouteDescriptorWithSplitAtHLCOffset(route RouteDescriptor) ([]byte, uint64, error) { if err := validateRouteDescriptor(route); err != nil { - return nil, err + return nil, 0, err } out := make([]byte, 0, routeDescriptorEncodedSize(route)) - out = append(out, catalogRouteCodecVersion) + version := catalogRouteCodecVersionV1 + if route.SplitAtHLC != 0 { + version = catalogRouteCodecVersionV2 + } + if routeDescriptorRequiresV3(route) { + version = catalogRouteCodecVersionV3 + } + out = append(out, version) out = appendU64(out, route.RouteID) out = appendU64(out, route.GroupID) out = append(out, byte(route.State)) @@ -185,16 +223,38 @@ func EncodeRouteDescriptor(route RouteDescriptor) ([]byte, error) { if route.End == nil { out = append(out, 0) + } else { + out = append(out, 1) + out = appendU64(out, uint64(len(route.End))) + out = append(out, route.End...) + } + + splitAtHLCOffset := uint64(0) + switch version { + case catalogRouteCodecVersionV3: + splitAtHLCOffset = uint64(len(out)) + out = appendRouteDescriptorV3Tail(out, route) + case catalogRouteCodecVersionV2: + splitAtHLCOffset = uint64(len(out)) out = appendU64(out, route.SplitAtHLC) - return out, nil } + return out, splitAtHLCOffset, nil +} - out = append(out, 1) - out = appendU64(out, uint64(len(route.End))) - out = append(out, route.End...) - out = appendU64(out, route.SplitAtHLC) +func EncodeRouteDescriptorForCatalogWrite(route RouteDescriptor, allowV2 bool) ([]byte, error) { + if routeDescriptorRequiresV2(route) && !allowV2 { + return nil, errors.WithStack(ErrCatalogRouteV2WriteDisabled) + } + return EncodeRouteDescriptor(route) +} - return out, nil +// EncodeRouteDescriptorForCatalogWriteWithSplitAtHLCOffset serializes a route +// descriptor and returns the byte offset of its SplitAtHLC field. +func EncodeRouteDescriptorForCatalogWriteWithSplitAtHLCOffset(route RouteDescriptor, allowV2 bool) ([]byte, uint64, error) { + if routeDescriptorRequiresV2(route) && !allowV2 { + return nil, 0, errors.WithStack(ErrCatalogRouteV2WriteDisabled) + } + return encodeRouteDescriptorWithSplitAtHLCOffset(route) } // DecodeRouteDescriptor deserializes a route descriptor record. @@ -203,7 +263,7 @@ func DecodeRouteDescriptor(raw []byte) (RouteDescriptor, error) { 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]) } @@ -216,14 +276,8 @@ func DecodeRouteDescriptor(raw []byte) (RouteDescriptor, error) { if err != nil { return RouteDescriptor{}, err } - if version >= catalogRouteCodecVersionV2 { - route.SplitAtHLC, err = decodeRouteDescriptorSplitAtHLC(r) - 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 @@ -320,7 +374,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 } @@ -381,19 +435,28 @@ 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, - SplitAtHLC: route.SplitAtHLC, + 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, + SplitAtHLC: route.SplitAtHLC, } } @@ -605,11 +668,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 @@ -626,7 +694,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 } @@ -652,6 +720,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 { @@ -697,7 +785,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 @@ -707,7 +795,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 } @@ -727,6 +815,9 @@ func routeDescriptorEqual(left, right RouteDescriptor) bool { left.GroupID == right.GroupID && left.State == right.State && left.ParentRouteID == right.ParentRouteID && + left.StagedVisibilityActive == right.StagedVisibilityActive && + left.MigrationJobID == right.MigrationJobID && + left.MinWriteTSExclusive == right.MinWriteTSExclusive && left.SplitAtHLC == right.SplitAtHLC } @@ -741,10 +832,88 @@ func routeDescriptorEncodedSize(route RouteDescriptor) int { if route.End != nil { size += catalogUint64Bytes + len(route.End) } - size += catalogUint64Bytes + if routeDescriptorRequiresV3(route) { + size += catalogRouteV3TailSize + } else if route.SplitAtHLC != 0 { + size += catalogUint64Bytes + } return size } +func routeDescriptorRequiresV2(route RouteDescriptor) bool { + return routeDescriptorRequiresV3(route) +} + +func routeDescriptorRequiresV3(route RouteDescriptor) bool { + return route.StagedVisibilityActive || route.MigrationJobID != 0 || route.MinWriteTSExclusive != 0 +} + +func appendRouteDescriptorV3Tail(out []byte, route RouteDescriptor) []byte { + out = appendU64(out, route.SplitAtHLC) + 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: + var err error + route.SplitAtHLC, err = decodeRouteDescriptorSplitAtHLC(r) + if err != nil { + return err + } + if r.Len() != 0 { + return errors.WithStack(ErrCatalogInvalidRouteRecord) + } + return nil + case catalogRouteCodecVersionV3: + return decodeRouteDescriptorV3Tail(r, route) + default: + return errors.Wrapf(ErrCatalogInvalidRouteRecord, "unsupported version %d", version) + } +} + +func decodeRouteDescriptorV3Tail(r *bytes.Reader, route *RouteDescriptor) error { + if r.Len() != catalogRouteV3TailSize { + return errors.WithStack(ErrCatalogInvalidRouteRecord) + } + var err error + route.SplitAtHLC, err = decodeRouteDescriptorSplitAtHLC(r) + if err != nil { + return err + } + 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_delta.go b/distribution/catalog_delta.go index c051ebd53..621888c28 100644 --- a/distribution/catalog_delta.go +++ b/distribution/catalog_delta.go @@ -251,6 +251,9 @@ func catalogDeltaRouteIsZero(route RouteDescriptor) bool { route.GroupID == 0 && route.State == 0 && route.ParentRouteID == 0 && + !route.StagedVisibilityActive && + route.MigrationJobID == 0 && + route.MinWriteTSExclusive == 0 && route.SplitAtHLC == 0 } diff --git a/distribution/catalog_test.go b/distribution/catalog_test.go index a3ffe852f..162a2edf2 100644 --- a/distribution/catalog_test.go +++ b/distribution/catalog_test.go @@ -3,6 +3,7 @@ package distribution import ( "bytes" "context" + "encoding/binary" "math" "testing" @@ -64,6 +65,9 @@ func TestRouteDescriptorCodecRoundTrip(t *testing.T) { if err != nil { t.Fatalf("encode route: %v", err) } + if raw[0] != catalogRouteCodecVersionV2 { + t.Fatalf("split route encoded version = %d, want v2", raw[0]) + } got, err := DecodeRouteDescriptor(raw) if err != nil { t.Fatalf("decode route: %v", err) @@ -85,6 +89,9 @@ func TestRouteDescriptorCodecRoundTripNilEnd(t *testing.T) { if err != nil { t.Fatalf("encode route: %v", err) } + if raw[0] != catalogRouteCodecVersionV2 { + t.Fatalf("split nil-end route encoded version = %d, want v2", raw[0]) + } got, err := DecodeRouteDescriptor(raw) if err != nil { t.Fatalf("decode route: %v", err) @@ -149,15 +156,90 @@ func TestRouteDescriptorCodecRejectsTrailingBytes(t *testing.T) { } } -func TestRouteDescriptorCodecRejectsTruncatedSplitAtHLC(t *testing.T) { +func TestRouteDescriptorCodecV3RoundTrip(t *testing.T) { route := RouteDescriptor{ - RouteID: 1, - Start: []byte("a"), - End: nil, - GroupID: 1, - State: RouteStateActive, - ParentRouteID: 0, - SplitAtHLC: 42, + RouteID: 1, + Start: []byte("a"), + End: []byte("m"), + GroupID: 1, + State: RouteStateActive, + ParentRouteID: 0, + StagedVisibilityActive: true, + MigrationJobID: 42, + MinWriteTSExclusive: 99, + SplitAtHLC: 123, + } + raw, err := EncodeRouteDescriptor(route) + if err != nil { + t.Fatalf("encode route: %v", err) + } + if raw[0] != catalogRouteCodecVersionV3 { + t.Fatalf("M2 route encoded version = %d, want v3", raw[0]) + } + + got, err := DecodeRouteDescriptor(raw) + if err != nil { + t.Fatalf("decode v3 route: %v", err) + } + assertRouteEqual(t, route, got) +} + +func TestRouteDescriptorCodecV3RoundTripNilEnd(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] != catalogRouteCodecVersionV3 { + t.Fatalf("M2 nil-end route encoded version = %d, want v3", raw[0]) + } + + got, err := DecodeRouteDescriptor(raw) + if err != nil { + t.Fatalf("decode v3 nil-end route: %v", err) + } + assertRouteEqual(t, route, got) +} + +func TestRouteDescriptorCodecRejectsV3TrailingBytes(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 TestRouteDescriptorCodecRejectsTruncatedV3Tail(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 { @@ -171,34 +253,33 @@ func TestRouteDescriptorCodecRejectsTruncatedSplitAtHLC(t *testing.T) { } } -func TestRouteDescriptorCodecAcceptsForwardVersionTail(t *testing.T) { +func TestRouteDescriptorCodecRejectsTruncatedSplitAtHLC(t *testing.T) { route := RouteDescriptor{ RouteID: 1, Start: []byte("a"), - End: []byte("m"), + End: nil, GroupID: 1, State: RouteStateActive, ParentRouteID: 0, + SplitAtHLC: 42, } 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)...) + raw = raw[:len(raw)-1] - 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 TestRouteDescriptorCodecAcceptsForwardVersionTailWithNilEnd(t *testing.T) { +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, @@ -208,13 +289,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) { @@ -238,27 +317,98 @@ func TestRouteDescriptorCodecRejectsBelowMinimumVersion(t *testing.T) { } } -func TestRouteDescriptorCloneAndEqualIncludeSplitAtHLC(t *testing.T) { +func TestRouteDescriptorHelpersIncludeExtensionFields(t *testing.T) { route := RouteDescriptor{ - RouteID: 3, - Start: []byte("a"), - End: []byte("m"), - GroupID: 2, - State: RouteStateActive, - ParentRouteID: 1, - SplitAtHLC: 99, + RouteID: 1, + Start: []byte("a"), + End: []byte("m"), + GroupID: 1, + State: RouteStateActive, + StagedVisibilityActive: true, + MigrationJobID: 42, + MinWriteTSExclusive: 99, + SplitAtHLC: 101, + } + cloned := CloneRouteDescriptor(route) + assertRouteEqual(t, route, cloned) + + withoutFloor := cloned + withoutFloor.MinWriteTSExclusive = 0 + if routeDescriptorEqual(route, withoutFloor) { + t.Fatal("routeDescriptorEqual must compare MinWriteTSExclusive") } - clone := CloneRouteDescriptor(route) - assertRouteEqual(t, route, clone) - if !routeDescriptorEqual(route, clone) { - t.Fatal("expected clone to compare equal") + + withoutJob := cloned + withoutJob.MigrationJobID = 100 + if routeDescriptorEqual(route, withoutJob) { + t.Fatal("routeDescriptorEqual must compare MigrationJobID") } - clone.SplitAtHLC++ - if routeDescriptorEqual(route, clone) { + + withoutStaged := cloned + withoutStaged.StagedVisibilityActive = false + withoutStaged.MigrationJobID = 0 + if routeDescriptorEqual(route, withoutStaged) { + t.Fatal("routeDescriptorEqual must compare StagedVisibilityActive") + } + + withoutSplitAt := cloned + withoutSplitAt.SplitAtHLC++ + if routeDescriptorEqual(route, withoutSplitAt) { t.Fatal("expected SplitAtHLC difference to compare unequal") } } +func TestRouteDescriptorSplitAtHLCPatchOffsetPreservesV3Fields(t *testing.T) { + route := RouteDescriptor{ + RouteID: 1, + Start: []byte("a"), + End: []byte("m"), + GroupID: 1, + State: RouteStateActive, + StagedVisibilityActive: true, + MigrationJobID: 42, + MinWriteTSExclusive: 99, + } + raw, offset, err := EncodeRouteDescriptorForCatalogWriteWithSplitAtHLCOffset(route, true) + if err != nil { + t.Fatalf("encode route: %v", err) + } + binary.BigEndian.PutUint64(raw[offset:offset+catalogUint64Bytes], 123) + + got, err := DecodeRouteDescriptor(raw) + if err != nil { + t.Fatalf("decode route: %v", err) + } + if got.SplitAtHLC != 123 { + t.Fatalf("expected split HLC 123, got %d", got.SplitAtHLC) + } + if got.MigrationJobID != route.MigrationJobID || got.MinWriteTSExclusive != route.MinWriteTSExclusive || got.StagedVisibilityActive != route.StagedVisibilityActive { + t.Fatalf("patch changed V3 migration fields: got %+v", got) + } +} + +func TestRouteDescriptorSplitAtHLCPatchOffsetSupportsV2(t *testing.T) { + raw, offset, err := EncodeRouteDescriptorForCatalogWriteWithSplitAtHLCOffset(RouteDescriptor{ + RouteID: 1, + Start: []byte("a"), + End: []byte("m"), + GroupID: 1, + State: RouteStateActive, + SplitAtHLC: 7, + }, true) + if err != nil { + t.Fatalf("encode route: %v", err) + } + binary.BigEndian.PutUint64(raw[offset:offset+catalogUint64Bytes], 123) + got, err := DecodeRouteDescriptor(raw) + if err != nil { + t.Fatalf("decode route: %v", err) + } + if got.SplitAtHLC != 123 { + t.Fatalf("expected split HLC 123, got %d", got.SplitAtHLC) + } +} + func TestCatalogRouteKeyHelpers(t *testing.T) { key := CatalogRouteKey(11) if !IsCatalogRouteKey(key) { @@ -636,6 +786,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() @@ -692,7 +889,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) } @@ -713,6 +910,13 @@ func TestCatalogStoreApplySaveMutations_UsesMonotonicCommitTS(t *testing.T) { } func assertRouteEqual(t *testing.T, want, got RouteDescriptor) { + t.Helper() + assertRouteIdentityEqual(t, want, got) + assertRouteMetadataEqual(t, want, got) + assertRouteBoundsEqual(t, want, got) +} + +func assertRouteIdentityEqual(t *testing.T, want, got RouteDescriptor) { t.Helper() if want.RouteID != got.RouteID { t.Fatalf("route id mismatch: want %d, got %d", want.RouteID, got.RouteID) @@ -723,12 +927,29 @@ 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.State != got.State { + t.Fatalf("state mismatch: want %d, got %d", want.State, got.State) + } +} + +func assertRouteMetadataEqual(t *testing.T, want, got RouteDescriptor) { + t.Helper() if want.SplitAtHLC != got.SplitAtHLC { t.Fatalf("split at HLC mismatch: want %d, got %d", want.SplitAtHLC, got.SplitAtHLC) } - if want.State != got.State { - t.Fatalf("state mismatch: want %d, got %d", want.State, got.State) + 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) } +} + +func assertRouteBoundsEqual(t *testing.T, want, got RouteDescriptor) { + t.Helper() if !bytes.Equal(want.Start, got.Start) { t.Fatalf("start mismatch: want %q, got %q", want.Start, got.Start) } @@ -744,6 +965,9 @@ func encodeRouteDescriptorV1ForTest(t *testing.T, route RouteDescriptor) []byte if err != nil { t.Fatalf("encode route: %v", err) } + if raw[0] == catalogRouteCodecVersionV1 { + return raw + } if len(raw) < catalogUint64Bytes+1 { t.Fatalf("encoded route too short: %d", len(raw)) } diff --git a/distribution/engine.go b/distribution/engine.go index 3af787314..726c83b16 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 } @@ -194,12 +200,15 @@ func routesAfterCatalogDelta(current []Route, delta CatalogDelta) ([]Route, erro load = current.Load } byID[mutation.RouteID] = Route{ - RouteID: mutation.Route.RouteID, - Start: CloneBytes(mutation.Route.Start), - End: CloneBytes(mutation.Route.End), - GroupID: mutation.Route.GroupID, - State: mutation.Route.State, - Load: load, + RouteID: mutation.Route.RouteID, + Start: CloneBytes(mutation.Route.Start), + End: CloneBytes(mutation.Route.End), + GroupID: mutation.Route.GroupID, + State: mutation.Route.State, + StagedVisibilityActive: mutation.Route.StagedVisibilityActive, + MigrationJobID: mutation.Route.MigrationJobID, + MinWriteTSExclusive: mutation.Route.MinWriteTSExclusive, + Load: load, } } } @@ -407,13 +416,23 @@ func (e *Engine) UpdateRoute(start, end []byte, group uint64) { // GetRoute finds a route for the given key using right half-open intervals. func (e *Engine) GetRoute(key []byte) (Route, bool) { + route, _, ok := e.GetRouteWithVersion(key) + return route, ok +} + +// GetRouteWithVersion finds a route and returns the catalog version from the +// same locked snapshot. Callers can use the version as a read-routing fence. +func (e *Engine) GetRouteWithVersion(key []byte) (Route, uint64, bool) { e.mu.RLock() defer e.mu.RUnlock() idx := e.routeIndex(key) if idx < 0 { - return Route{}, false + return Route{}, e.catalogVersion, false } - return e.routes[idx], true + route := e.routes[idx] + route.Start = CloneBytes(route.Start) + route.End = CloneBytes(route.End) + return route, e.catalogVersion, true } // NextTimestamp returns a monotonic increasing timestamp. @@ -428,12 +447,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 @@ -444,6 +466,13 @@ func (e *Engine) Stats() []Route { // - rStart < end (or end is nil, meaning unbounded scan) // - start < rEnd (or rEnd is nil, meaning unbounded route) func (e *Engine) GetIntersectingRoutes(start, end []byte) []Route { + routes, _ := e.GetIntersectingRoutesWithVersion(start, end) + return routes +} + +// GetIntersectingRoutesWithVersion returns intersecting routes and the catalog +// version from the same locked snapshot. +func (e *Engine) GetIntersectingRoutesWithVersion(start, end []byte) ([]Route, uint64) { e.mu.RLock() defer e.mu.RUnlock() @@ -461,15 +490,18 @@ 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 + return result, e.catalogVersion } func (e *Engine) routeIndex(key []byte) int { @@ -505,12 +537,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..a54ef5863 100644 --- a/distribution/engine_test.go +++ b/distribution/engine_test.go @@ -86,6 +86,124 @@ 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 TestEngineApplyDelta_PreservesMigrationRouteFields(t *testing.T) { + t.Parallel() + + e := NewEngine() + if err := e.ApplySnapshot(CatalogSnapshot{ + Version: 1, + Routes: []RouteDescriptor{ + { + RouteID: 1, + Start: []byte(""), + End: nil, + GroupID: 1, + State: RouteStateActive, + }, + }, + }); err != nil { + t.Fatalf("ApplySnapshot: %v", err) + } + + err := e.ApplyDelta(CatalogDelta{ + PreviousVersion: 1, + Version: 2, + Mutations: []CatalogRouteMutation{ + {Op: CatalogMutationDelete, RouteID: 1}, + { + Op: CatalogMutationUpsert, + RouteID: 7, + Route: RouteDescriptor{ + RouteID: 7, + Start: []byte("a"), + End: []byte("z"), + GroupID: 2, + State: RouteStateMigratingTarget, + StagedVisibilityActive: true, + MigrationJobID: 42, + MinWriteTSExclusive: 99, + }, + }, + }, + }) + if err != nil { + t.Fatalf("ApplyDelta: %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) @@ -192,6 +310,31 @@ func TestEngineApplySnapshot_ReplacesRoutesAndVersion(t *testing.T) { } } +func TestEngineRouteLookupsReturnMatchingCatalogVersion(t *testing.T) { + t.Parallel() + + e := NewEngine() + if err := e.ApplySnapshot(CatalogSnapshot{ + Version: 12, + Routes: []RouteDescriptor{ + {RouteID: 10, Start: []byte(""), End: []byte("m"), GroupID: 1, State: RouteStateActive}, + {RouteID: 11, Start: []byte("m"), GroupID: 2, State: RouteStateActive}, + }, + }); err != nil { + t.Fatalf("apply snapshot: %v", err) + } + + route, version, ok := e.GetRouteWithVersion([]byte("z")) + if !ok || route.GroupID != 2 || version != 12 { + t.Fatalf("unexpected atomic route lookup: route=%+v version=%d ok=%v", route, version, ok) + } + + routes, version := e.GetIntersectingRoutesWithVersion([]byte("a"), []byte("z")) + if len(routes) != 2 || version != 12 { + t.Fatalf("unexpected atomic range lookup: routes=%+v version=%d", routes, version) + } +} + func TestEngineApplySnapshot_RejectsOldVersion(t *testing.T) { e := NewEngine() diff --git a/distribution/grpc_watcher.go b/distribution/grpc_watcher.go index 96ce2faf5..0b442dd21 100644 --- a/distribution/grpc_watcher.go +++ b/distribution/grpc_watcher.go @@ -322,12 +322,16 @@ func routeDescriptorFromProto(raw *pb.RouteDescriptor) (RouteDescriptor, error) return RouteDescriptor{}, errors.WithStack(ErrCatalogWatchEventInvalid) } route := RouteDescriptor{ - RouteID: raw.GetRouteId(), - Start: CloneBytes(raw.GetStart()), - End: CloneBytes(raw.GetEnd()), - GroupID: raw.GetRaftGroupId(), - State: state, - ParentRouteID: raw.GetParentRouteId(), + RouteID: raw.GetRouteId(), + Start: CloneBytes(raw.GetStart()), + End: CloneBytes(raw.GetEnd()), + GroupID: raw.GetRaftGroupId(), + State: state, + ParentRouteID: raw.GetParentRouteId(), + SplitAtHLC: raw.GetSplitAtHlc(), + StagedVisibilityActive: raw.GetStagedVisibilityActive(), + MigrationJobID: raw.GetMigrationJobId(), + MinWriteTSExclusive: raw.GetMinWriteTsExclusive(), } if err := validateRouteDescriptor(route); err != nil { return RouteDescriptor{}, err diff --git a/distribution/grpc_watcher_test.go b/distribution/grpc_watcher_test.go new file mode 100644 index 000000000..18e97e074 --- /dev/null +++ b/distribution/grpc_watcher_test.go @@ -0,0 +1,36 @@ +package distribution + +import ( + "testing" + + pb "github.com/bootjp/elastickv/proto" + "github.com/stretchr/testify/require" +) + +func TestRouteDescriptorFromProtoPreservesMigrationFields(t *testing.T) { + t.Parallel() + + route, err := routeDescriptorFromProto(&pb.RouteDescriptor{ + RouteId: 7, + Start: []byte("a"), + End: []byte("z"), + RaftGroupId: 3, + State: pb.RouteState_ROUTE_STATE_MIGRATING_TARGET, + ParentRouteId: 6, + SplitAtHlc: 101, + StagedVisibilityActive: true, + MigrationJobId: 42, + MinWriteTsExclusive: 99, + }) + require.NoError(t, err) + require.Equal(t, uint64(7), route.RouteID) + require.Equal(t, []byte("a"), route.Start) + require.Equal(t, []byte("z"), route.End) + require.Equal(t, uint64(3), route.GroupID) + require.Equal(t, RouteStateMigratingTarget, route.State) + require.Equal(t, uint64(6), route.ParentRouteID) + require.Equal(t, uint64(101), route.SplitAtHLC) + require.True(t, route.StagedVisibilityActive) + require.Equal(t, uint64(42), route.MigrationJobID) + require.Equal(t, uint64(99), route.MinWriteTSExclusive) +} diff --git a/kv/backup_scan.go b/kv/backup_scan.go index 10352718b..76b2a3b4a 100644 --- a/kv/backup_scan.go +++ b/kv/backup_scan.go @@ -110,7 +110,7 @@ func (s *backupScanner) loadNextPage(ctx context.Context) error { if !ok { continue } - val, err := s.store.getRouteAt(ctx, route, item.key, s.ts) + val, err := s.store.getRouteAt(ctx, route, item.key, s.ts, 0) if errors.Is(err, store.ErrKeyNotFound) { continue } diff --git a/kv/leader_routed_store.go b/kv/leader_routed_store.go index 47e4bc6b1..a0b06c6be 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,9 @@ 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}) + ctx, cancel := context.WithTimeout(ctx, proxyForwardTimeout) + defer cancel() + resp, err := cli.RawGet(ctx, &pb.RawGetRequest{Key: key, Ts: ts, ReadRouteVersion: readRouteVersion}) if err != nil { return nil, errors.WithStack(err) } @@ -94,7 +100,29 @@ 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 { + if readRouteVersion != 0 { + return nil, errors.WithStack(store.ErrNotSupported) + } + 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 +134,29 @@ func (s *LeaderRoutedStore) proxyRawLatestCommitTS(ctx context.Context, key []by } cli := pb.NewRawKVClient(conn) - resp, err := cli.RawLatestCommitTS(ctx, &pb.RawLatestCommitTSRequest{Key: key}) + ctx, cancel := context.WithTimeout(ctx, proxyForwardTimeout) + defer cancel() + 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) { + if readRouteVersion != 0 { + return 0, false, errors.WithStack(store.ErrNotSupported) + } + 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 +164,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 == "" { @@ -132,12 +190,18 @@ func (s *LeaderRoutedStore) proxyRawScanAt( } cli := pb.NewRawKVClient(conn) + ctx, cancel := context.WithTimeout(ctx, proxyForwardTimeout) + defer cancel() 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,12 +217,13 @@ func (s *LeaderRoutedStore) proxyRawScanAt( return out, nil } -func (s *LeaderRoutedStore) proxyRawScanKeysAt( +func (s *LeaderRoutedStore) proxyRawScanKeysAtWithReadFence( ctx context.Context, start []byte, end []byte, limit int, ts uint64, + readRouteVersion uint64, ) ([][]byte, error) { addr := s.leaderAddrForKey(start) if addr == "" { @@ -171,12 +236,15 @@ func (s *LeaderRoutedStore) proxyRawScanKeysAt( } cli := pb.NewRawKVClient(conn) + ctx, cancel := context.WithTimeout(ctx, proxyForwardTimeout) + defer cancel() resp, err := cli.RawScanAt(ctx, &pb.RawScanAtRequest{ - StartKey: start, - EndKey: end, - Limit: int64(limit), - Ts: ts, - KeysOnly: true, + StartKey: start, + EndKey: end, + Limit: int64(limit), + Ts: ts, + ReadRouteVersion: readRouteVersion, + KeysOnly: true, }) if err != nil { return nil, errors.WithStack(err) @@ -192,6 +260,66 @@ func (s *LeaderRoutedStore) proxyRawScanKeysAt( 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) + } + if readRouteVersion != 0 { + return nil, errors.WithStack(store.ErrNotSupported) + } + 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 @@ -300,18 +428,28 @@ func (s *LeaderRoutedStore) ScanAt(ctx context.Context, start []byte, end []byte } func (s *LeaderRoutedStore) ScanKeysAt(ctx context.Context, start []byte, end []byte, limit int, ts uint64) ([][]byte, error) { + return s.ScanKeysAtWithReadFence(ctx, start, end, limit, ts, 0, 0) +} + +func (s *LeaderRoutedStore) ScanKeysAtWithReadFence(ctx context.Context, start []byte, end []byte, limit int, ts uint64, groupID uint64, readRouteVersion uint64) ([][]byte, error) { if s == nil || s.local == nil { return [][]byte{}, nil } if limit <= 0 { return [][]byte{}, nil } + if groupID != 0 { + return nil, store.ErrNotSupported + } ok, fenceTS := s.leaderFenceTS(ctx, start) if ok { + if readRouteVersion != 0 { + return nil, errors.WithStack(store.ErrNotSupported) + } keys, err := s.local.ScanKeysAt(ctx, start, end, limit, max(ts, fenceTS)) return keys, errors.WithStack(err) } - return s.proxyRawScanKeysAt(ctx, start, end, limit, ts) + return s.proxyRawScanKeysAtWithReadFence(ctx, start, end, limit, ts, readRouteVersion) } func (s *LeaderRoutedStore) ScanAtPhysicalLimit(ctx context.Context, start []byte, end []byte, visibleLimit, physicalLimit int, ts uint64) ([]*store.KVPair, bool, error) { diff --git a/kv/leader_routed_store_test.go b/kv/leader_routed_store_test.go index 22ddc9535..b2b9f8304 100644 --- a/kv/leader_routed_store_test.go +++ b/kv/leader_routed_store_test.go @@ -91,12 +91,17 @@ 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 } @@ -107,6 +112,7 @@ func (f *fakeRawKVServer) RawScanAt(_ context.Context, req *pb.RawScanAtRequest) f.mu.Lock() defer f.mu.Unlock() f.scanCalls++ + f.lastScanReq = req f.lastScanGroupID = req.GetGroupId() f.lastScanKeysOnly = req.GetKeysOnly() if f.scanResp != nil { @@ -115,10 +121,11 @@ func (f *fakeRawKVServer) RawScanAt(_ context.Context, req *pb.RawScanAtRequest) 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 } @@ -174,6 +181,65 @@ 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, 0, []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, 0, []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_RejectsLocalReadRouteVersion(t *testing.T) { + t.Parallel() + + ctx := context.Background() + local := store.NewMVCCStore() + require.NoError(t, local.PutAt(ctx, []byte("k"), []byte("v"), 10, 0)) + require.NoError(t, local.PutAt(ctx, []byte("a"), []byte("va"), 10, 0)) + + coord := &stubLeaderCoordinator{ + isLeader: true, + clock: NewHLC(), + } + s := NewLeaderRoutedStore(local, coord) + t.Cleanup(func() { _ = s.Close() }) + + _, err := s.GetAtWithReadFence(ctx, []byte("k"), 10, 0, 7) + require.ErrorIs(t, err, store.ErrNotSupported) + + _, _, err = s.LatestCommitTSWithReadFence(ctx, []byte("k"), 7) + require.ErrorIs(t, err, store.ErrNotSupported) + + _, err = s.ScanAtWithReadFence(ctx, []byte("a"), []byte("z"), 10, 10, false, 0, 7, nil, nil) + require.ErrorIs(t, err, store.ErrNotSupported) + + _, err = s.ScanKeysAtWithReadFence(ctx, []byte("a"), []byte("z"), 10, 10, 0, 7) + require.ErrorIs(t, err, store.ErrNotSupported) +} + func TestLeaderRoutedStore_PrefersLinearizableReadFence(t *testing.T) { t.Parallel() @@ -251,6 +317,75 @@ 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_ForwardsKeyScanReadFenceWithoutValues(t *testing.T) { + t.Parallel() + + fake := &fakeRawKVServer{ + scanResp: &pb.RawScanAtResponse{Kv: []*pb.RawKVPair{{Key: []byte("k")}}}, + } + addr, stop := startRawKVServer(t, fake) + t.Cleanup(stop) + + coord := &stubLeaderCoordinator{isLeader: false, leader: addr, clock: NewHLC()} + mvcc := store.NewMVCCStore() + s := NewLeaderRoutedStore(mvcc, coord) + t.Cleanup(func() { + require.NoError(t, s.Close()) + require.NoError(t, mvcc.Close()) + }) + + keys, err := s.ScanKeysAtWithReadFence(context.Background(), []byte("a"), []byte("z"), 10, 11, 0, 83) + require.NoError(t, err) + require.Equal(t, [][]byte{[]byte("k")}, keys) + + fake.mu.Lock() + defer fake.mu.Unlock() + require.True(t, fake.lastScanKeysOnly) + require.Equal(t, uint64(83), fake.lastScanReq.GetReadRouteVersion()) +} + func TestLeaderRoutedStore_ReturnsLeaderNotFoundWhenNoLeaderAddr(t *testing.T) { t.Parallel() diff --git a/kv/shard_key.go b/kv/shard_key.go index 21afe185d..d4132c8c1 100644 --- a/kv/shard_key.go +++ b/kv/shard_key.go @@ -2,6 +2,7 @@ package kv import ( "bytes" + "encoding/binary" "github.com/bootjp/elastickv/internal/fskeys" "github.com/bootjp/elastickv/internal/s3keys" @@ -12,6 +13,8 @@ const redisInternalRoutePrefix = "!redis|" var redisInternalRoutePrefixBytes = []byte(redisInternalRoutePrefix) +const wideColumnEncodedKeyLengthSize = 4 + const ( dynamoRoutePrefix = "!ddb|route|table|" @@ -47,6 +50,22 @@ var ( dynamoGSIPrefixBytes = []byte(DynamoGSIPrefix) sqsRoutePrefixBytes = []byte(sqsRoutePrefix) sqsInternalPrefixBytes = []byte(sqsInternalPrefix) + redisWideColumnScanPrefixes = [][]byte{ + []byte(store.HashMetaDeltaPrefix), + []byte(store.HashMetaPrefix), + []byte(store.HashFieldPrefix), + []byte(store.SetMetaDeltaPrefix), + []byte(store.SetMetaPrefix), + []byte(store.SetMemberPrefix), + []byte(store.ZSetMetaDeltaPrefix), + []byte(store.ZSetMetaPrefix), + []byte(store.ZSetMemberPrefix), + []byte(store.ZSetScorePrefix), + } + redisListAuxiliaryScanPrefixes = [][]byte{ + []byte(store.ListMetaDeltaPrefix), + []byte(store.ListClaimPrefix), + } ) // RouteKey normalizes internal keys (e.g., list metadata/items) to the logical @@ -65,10 +84,23 @@ func routeKey(key []byte) []byte { return normalizeRouteKey(key) } +func routeFilterKey(key []byte) []byte { + if key == nil { + return nil + } + if embedded, ok := txnRouteKey(key); ok { + return normalizeRouteFilterKey(embedded) + } + return normalizeRouteFilterKey(key) +} + 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 } @@ -87,6 +119,177 @@ func normalizeRouteKey(key []byte) []byte { return key } +func normalizeRouteFilterKey(key []byte) []byte { + if user := redisListAuxiliaryRouteKey(key); user != nil { + return user + } + if user := redisStreamRouteKey(key); user != nil { + return user + } + return normalizeRouteKey(key) +} + +func redisWideColumnLegacyPointRouteKey(key []byte) []byte { + if embedded, ok := txnRouteKey(key); ok { + key = embedded + } + if redisWideColumnRouteKey(key) == nil { + return nil + } + 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 redisWideColumnScanRouteParts(key []byte) (prefix []byte, userKey []byte, userPrefix []byte, owned bool, parsed bool) { + for _, prefix := range redisWideColumnScanPrefixes { + if !bytes.HasPrefix(key, prefix) { + continue + } + user := wideColumnScanUserKey(key, prefix) + if user == nil { + return prefix, nil, nil, true, false + } + prefixLen := len(prefix) + wideColumnEncodedKeyLengthSize + len(user) + return prefix, user, key[:prefixLen], true, true + } + return nil, nil, nil, false, false +} + +func redisWideColumnLegacyScanRouteRange(start []byte, end []byte) ([]byte, []byte, bool) { + _, _, _, owned, parsed := redisWideColumnScanRouteParts(start) + if !owned || !parsed { + return nil, nil, false + } + return start, end, true +} + +func redisWideColumnScanRouteRange(start []byte, end []byte) (routeStart []byte, routeEnd []byte, exact bool, ok bool) { + prefix, userKey, userPrefix, owned, parsed := redisWideColumnScanRouteParts(start) + if !owned { + return nil, nil, false, false + } + if !parsed { + return nil, nil, false, true + } + if exactEnd := prefixScanEnd(userPrefix); end != nil && bytes.Compare(end, exactEnd) <= 0 { + return userKey, nil, true, true + } + if bytes.Equal(start, userPrefix) && end != nil && bytes.Compare(end, prefixScanEnd(prefix)) <= 0 { + return userKey, prefixScanEnd(userKey), false, true + } + // Physical wide-column cursors include a field/member suffix. Their raw + // ordering cannot be projected to a logical user-key lower bound, so the + // remaining namespace must fan out to every logical route. + return nil, nil, false, true +} + +func listAuxiliaryScanRouteRange(start []byte, end []byte) (routeStart []byte, exact bool, ok bool) { + for _, prefix := range redisListAuxiliaryScanPrefixes { + if !bytes.HasPrefix(start, prefix) { + continue + } + user := wideColumnScanUserKey(start, prefix) + if user == nil { + return nil, false, true + } + userPrefixLen := len(prefix) + wideColumnEncodedKeyLengthSize + len(user) + userPrefix := start[:userPrefixLen] + if exactEnd := prefixScanEnd(userPrefix); end != nil && bytes.Compare(end, exactEnd) <= 0 { + return user, true, true + } + return nil, false, true + } + return nil, false, false +} + +func wideColumnScanUserKey(key []byte, prefix []byte) []byte { + if !bytes.HasPrefix(key, prefix) { + return nil + } + rest := key[len(prefix):] + if len(rest) < wideColumnEncodedKeyLengthSize { + return nil + } + keyLen := binary.BigEndian.Uint32(rest[:wideColumnEncodedKeyLengthSize]) + rest = rest[wideColumnEncodedKeyLengthSize:] + if uint64(keyLen) > uint64(len(rest)) { + return nil + } + return rest[:keyLen] +} + +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 redisListAuxiliaryRouteKey(key []byte) []byte { + switch { + case store.IsListMetaDeltaKey(key): + return store.ExtractListUserKeyFromDelta(key) + case store.IsListClaimKey(key): + return store.ExtractListUserKeyFromClaim(key) + default: + return nil + } +} + +func redisStreamRouteKey(key []byte) []byte { + switch { + case store.IsStreamMetaKey(key): + return store.ExtractStreamUserKeyFromMeta(key) + case store.IsStreamEntryKey(key): + return store.ExtractStreamUserKeyFromEntry(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 30f27fd94..de560d250 100644 --- a/kv/shard_key_test.go +++ b/kv/shard_key_test.go @@ -6,6 +6,7 @@ import ( "github.com/bootjp/elastickv/internal/fskeys" "github.com/bootjp/elastickv/internal/s3keys" + "github.com/bootjp/elastickv/store" "github.com/stretchr/testify/require" ) @@ -54,6 +55,102 @@ 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 TestRouteFilterKey_NormalizesRedisAuxiliaryKeys(t *testing.T) { + t.Parallel() + + userKey := []byte("user:key") + for _, raw := range [][]byte{ + store.ListMetaDeltaKey(userKey, 10, 0), + store.ListClaimKey(userKey, 1), + store.StreamMetaKey(userKey), + store.StreamEntryKey(userKey, 123, 4), + } { + require.Equal(t, userKey, routeFilterKey(raw)) + require.Equal(t, userKey, routeFilterKey(txnLockKey(raw))) + } +} + +func TestRedisWideColumnScanRouteRangeFansOutBareFamilyAndCursor(t *testing.T) { + t.Parallel() + + prefix := []byte(store.HashFieldPrefix) + familyEnd := prefixScanEnd(prefix) + start := store.HashFieldScanPrefix([]byte("alice")) + cursor := append(append([]byte(nil), start...), []byte("field\x00")...) + + for _, tc := range []struct { + name string + start []byte + }{ + {name: "bare family", start: prefix}, + {name: "physical cursor", start: cursor}, + } { + t.Run(tc.name, func(t *testing.T) { + routeStart, routeEnd, exact, ok := redisWideColumnScanRouteRange(tc.start, familyEnd) + require.True(t, ok) + require.False(t, exact) + require.Nil(t, routeStart) + require.Nil(t, routeEnd) + }) + } + + routeStart, routeEnd, exact, ok := redisWideColumnScanRouteRange(start, prefixScanEnd(start)) + require.True(t, ok) + require.True(t, exact) + require.Equal(t, []byte("alice"), routeStart) + require.Nil(t, routeEnd) +} + +func TestListAuxiliaryScanRouteRangeFansOutBareFamilyAndCursor(t *testing.T) { + t.Parallel() + + prefix := []byte(store.ListMetaDeltaPrefix) + familyEnd := prefixScanEnd(prefix) + userPrefix := store.ListMetaDeltaScanPrefix([]byte("alice")) + cursor := store.ListMetaDeltaKey([]byte("alice"), 10, 0) + + for _, tc := range []struct { + name string + start []byte + }{ + {name: "bare family", start: prefix}, + {name: "physical cursor", start: cursor}, + } { + t.Run(tc.name, func(t *testing.T) { + routeStart, exact, ok := listAuxiliaryScanRouteRange(tc.start, familyEnd) + require.True(t, ok) + require.False(t, exact) + require.Nil(t, routeStart) + }) + } + + routeStart, exact, ok := listAuxiliaryScanRouteRange(userPrefix, prefixScanEnd(userPrefix)) + require.True(t, ok) + require.True(t, exact) + require.Equal(t, []byte("alice"), routeStart) +} + func TestRouteKey_NormalizesDynamoKeysToTable(t *testing.T) { t.Parallel() diff --git a/kv/shard_store.go b/kv/shard_store.go index fc3276de9..c64412639 100644 --- a/kv/shard_store.go +++ b/kv/shard_store.go @@ -19,7 +19,11 @@ import ( "github.com/cockroachdb/errors" ) -const proxyForwardTimeout = 5 * time.Second +const ( + proxyForwardTimeout = 5 * time.Second + readRouteVersionWaitTimeout = 200 * time.Millisecond + readRouteVersionPollInterval = 2 * time.Millisecond +) // ShardStore routes MVCC reads to shard-specific stores and proxies to leaders when needed. type ShardStore struct { @@ -31,6 +35,7 @@ type ShardStore struct { var ( ErrCrossShardMutationBatchNotSupported = errors.New("cross-shard mutation batches are not supported") + ErrReadRouteVersionUnavailable = errors.New("read route version is not locally available") ErrFilesystemPlacementTargetNotFound = errors.New("filesystem placement target group has no routable home slot") ) @@ -42,6 +47,35 @@ 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) awaitReadRouteVersion(ctx context.Context, requested uint64) error { + if requested == 0 || s.ReadRouteVersion() >= requested { + return nil + } + + waitCtx, cancel := context.WithTimeout(ctx, readRouteVersionWaitTimeout) + defer cancel() + ticker := time.NewTicker(readRouteVersionPollInterval) + defer ticker.Stop() + + for { + select { + case <-waitCtx.Done(): + return errors.Wrapf(ErrReadRouteVersionUnavailable, "requested=%d current=%d: %v", requested, s.ReadRouteVersion(), waitCtx.Err()) + case <-ticker.C: + if s.ReadRouteVersion() >= requested { + return nil + } + } + } +} + // FilesystemGroupForHome resolves the group that owns one file-home route. func (s *ShardStore) FilesystemGroupForHome(homeSlot uint64, inode uint64) (uint64, bool) { if s == nil || s.engine == nil { @@ -139,51 +173,126 @@ func filesystemHomeCandidateFromBound(bound []byte, upper bool) (uint64, bool) { } func (s *ShardStore) GetAt(ctx context.Context, key []byte, ts uint64) ([]byte, error) { - g, ok := s.groupForKey(key) - if !ok || g.Store == nil { - return nil, store.ErrKeyNotFound - } + return s.GetAtWithReadFence(ctx, key, ts, 0, 0) +} - // Some tests use ShardStore without raft; in that case serve reads locally. - if engineForGroup(g) == nil { - return s.localGetAt(ctx, g, key, ts) +func (s *ShardStore) GetAtWithReadFence(ctx context.Context, key []byte, ts uint64, groupID uint64, readRouteVersion uint64) ([]byte, error) { + if err := s.awaitReadRouteVersion(ctx, readRouteVersion); err != nil { + return nil, err } - - // Wait for a leader read fence before serving from local state. - if isLinearizableRaftLeader(ctx, engineForGroup(g)) { - return s.leaderGetAt(ctx, g, key, ts) + if groupID != 0 { + return s.getGroupAtWithReadFence(ctx, groupID, key, ts, readRouteVersion) + } + routes, routeVersion := s.pointReadRoutesWithVersion(key) + readRouteVersion = max(readRouteVersion, routeVersion) + if len(routes) == 0 { + return nil, store.ErrKeyNotFound + } + for i, route := range routes { + value, err := s.getRouteAt(ctx, route, key, ts, readRouteVersion) + if err == nil { + return value, nil + } + if !errors.Is(err, store.ErrKeyNotFound) { + return nil, err + } + stop, err := s.routeMissStopsPointFallback(ctx, routes, i, route, key, ts, readRouteVersion) + if err != nil { + return nil, err + } + if stop { + return nil, store.ErrKeyNotFound + } } - return s.proxyRawGet(ctx, g, key, ts, 0) + return nil, store.ErrKeyNotFound } // 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 } - return s.getGroupAt(ctx, g, key, ts, groupID) + return s.getGroupAt(ctx, g, key, ts, groupID, readRouteVersion) } -func (s *ShardStore) getRouteAt(ctx context.Context, route distribution.Route, key []byte, ts uint64) ([]byte, error) { +func (s *ShardStore) getRouteAt(ctx context.Context, route distribution.Route, key []byte, ts uint64, readRouteVersion uint64) ([]byte, error) { g, ok := s.groupForID(route.GroupID) if !ok || g.Store == nil { return nil, store.ErrKeyNotFound } - return s.getGroupAt(ctx, g, key, ts, route.GroupID) + return s.getGroupAt(ctx, g, key, ts, route.GroupID, readRouteVersion) +} + +func (s *ShardStore) routeHasLatestVersionVisibleAt(ctx context.Context, route distribution.Route, key []byte, ts uint64, readRouteVersion uint64) (bool, error) { + if exists, ok, err := s.routeHasVersionAtOrBefore(ctx, route, key, ts); ok || err != nil { + return exists, err + } + latest, exists, err := s.latestCommitTSForRoute(ctx, route, key, readRouteVersion) + if err != nil { + return false, err + } + return exists && latest <= ts, nil +} + +func (s *ShardStore) routeHasVersionAtOrBefore(ctx context.Context, route distribution.Route, key []byte, ts uint64) (bool, bool, error) { + g, ok := s.groupForID(route.GroupID) + if !ok || g.Store == nil { + return false, true, nil + } + if engine := engineForGroup(g); engine != nil && !isLinearizableRaftLeader(ctx, engine) { + return false, false, nil + } + checker, ok := g.Store.(store.VersionPresenceReader) + if !ok { + return false, false, nil + } + exists, err := checker.VersionExistsAtOrBefore(ctx, key, ts) + return exists, true, errors.WithStack(err) +} + +func (s *ShardStore) routeMissStopsPointFallback(ctx context.Context, routes []distribution.Route, routeIndex int, route distribution.Route, key []byte, ts uint64, readRouteVersion uint64) (bool, error) { + if routeIndex != 0 || len(routes) < 2 { + return false, nil + } + return s.routeHasLatestVersionVisibleAt(ctx, route, key, ts, readRouteVersion) } -func (s *ShardStore) getGroupAt(ctx context.Context, g *ShardGroup, key []byte, ts uint64, groupID uint64) ([]byte, error) { +func (s *ShardStore) getGroupAt(ctx context.Context, g *ShardGroup, key []byte, ts uint64, groupID uint64, readRouteVersion uint64) ([]byte, error) { if engineForGroup(g) == nil { return s.localGetAt(ctx, g, key, ts) } 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 (s *ShardStore) pointReadRoutesWithVersion(key []byte) ([]distribution.Route, uint64) { + primaryKey := routeKey(key) + primary, version, ok := s.engine.GetRouteWithVersion(primaryKey) + if !ok { + return nil, version + } + routes := []distribution.Route{primary} + + legacyKey := redisWideColumnLegacyPointRouteKey(key) + if legacyKey == nil || bytes.Equal(legacyKey, primaryKey) { + return routes, version + } + legacy, legacyVersion, ok := s.engine.GetRouteWithVersion(legacyKey) + version = max(version, legacyVersion) + if !ok || legacy.GroupID == primary.GroupID { + return routes, version + } + return append(routes, legacy), version } func isLinearizableRaftLeader(ctx context.Context, engine raftengine.LeaderView) bool { @@ -300,21 +409,75 @@ func tryEngineLinearizableFence(ctx context.Context, engine raftengine.LeaderVie // multiple shards are best-effort because each shard may have a different Raft // apply position. 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 err := s.awaitReadRouteVersion(ctx, readRouteVersion); err != nil { + return nil, err + } + if reverse { + if groupID != 0 { + if routeScanBoundsPresent(routeStart, routeEnd) { + return s.scanRouteAtDirectionWithReadFence(ctx, distribution.Route{GroupID: groupID}, start, end, limit, ts, 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.routesForForwardScan(start, end) - return s.scanRoutesAtSorted(ctx, routes, start, end, limit, ts, clampToRoutes) + if groupID != 0 { + return s.scanRouteAtDirectionWithReadFence(ctx, distribution.Route{GroupID: groupID}, start, end, limit, ts, false, readRouteVersion, routeStart, routeEnd) + } + + routes, clampToRoutes, routeVersion := s.routesForFencedScanWithVersion(start, end, routeStart, routeEnd) + readRouteVersion = max(readRouteVersion, routeVersion) + out, err := s.scanRoutesAtWithReadFence(ctx, routes, start, end, limit, ts, clampToRoutes, readRouteVersion, routeStart, routeEnd) + if err != nil { + return nil, err + } + sort.SliceStable(out, func(i, j int) bool { + return bytes.Compare(out[i].Key, out[j].Key) < 0 + }) + out = dedupeSortedScanResults(out) + out, err = s.canonicalizeRedisWideColumnScanResults(ctx, out, start, end, ts, readRouteVersion) + if err != nil { + return nil, err + } + if len(out) > limit { + out = out[:limit] + } + return out, nil } func (s *ShardStore) ScanKeysAt(ctx context.Context, start []byte, end []byte, limit int, ts uint64) ([][]byte, error) { + return s.ScanKeysAtWithReadFence(ctx, start, end, limit, ts, 0, 0) +} + +func (s *ShardStore) ScanKeysAtWithReadFence(ctx context.Context, start []byte, end []byte, limit int, ts uint64, groupID uint64, readRouteVersion uint64) ([][]byte, error) { if limit <= 0 { return [][]byte{}, nil } + if err := s.awaitReadRouteVersion(ctx, readRouteVersion); err != nil { + return nil, err + } + if groupID != 0 { + return s.scanKeyRouteAtWithReadFence(ctx, distribution.Route{GroupID: groupID}, start, end, limit, ts, readRouteVersion) + } - routes, clampToRoutes := s.routesForForwardScan(start, end) - out, err := s.scanKeyRoutesAt(ctx, routes, start, end, limit, ts, clampToRoutes) + routes, clampToRoutes, routeVersion := s.routesForScanWithVersion(start, end) + readRouteVersion = max(readRouteVersion, routeVersion) + out, err := s.scanKeyRoutesAtWithReadFence(ctx, routes, start, end, limit, ts, clampToRoutes, readRouteVersion) if err != nil { return nil, err } @@ -348,7 +511,7 @@ func (s *ShardStore) ScanGroupAt(ctx context.Context, groupID uint64, start []by if limit <= 0 { return []*store.KVPair{}, nil } - return s.scanRouteAtDirection(ctx, distribution.Route{GroupID: groupID}, start, end, limit, ts, false, true) + return s.scanRouteAtDirection(ctx, distribution.Route{GroupID: groupID}, start, end, limit, ts, false) } // ReverseScanGroupAt reverse-scans a range on the explicitly selected Raft group. @@ -356,7 +519,7 @@ func (s *ShardStore) ReverseScanGroupAt(ctx context.Context, groupID uint64, sta if limit <= 0 { return []*store.KVPair{}, nil } - return s.scanRouteAtDirection(ctx, distribution.Route{GroupID: groupID}, start, end, limit, ts, true, true) + return s.scanRouteAtDirection(ctx, distribution.Route{GroupID: groupID}, start, end, limit, ts, true) } // ScanGroupKeysAt scans keys on the explicitly selected Raft group without @@ -369,12 +532,21 @@ func (s *ShardStore) ScanGroupKeysAt(ctx context.Context, groupID uint64, start } 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.routesForReverseScan(start, end) - out, err := s.reverseScanRoutesAt(ctx, routes, start, end, limit, ts, clampToRoutes) + routes, clampToRoutes, routeVersion := s.routesForFencedScanWithVersion(start, end, routeStart, routeEnd) + readRouteVersion = max(readRouteVersion, routeVersion) + out, err := s.reverseScanRoutesAtWithReadFence(ctx, routes, start, end, limit, ts, clampToRoutes, readRouteVersion, routeStart, routeEnd) + if err != nil { + return nil, err + } + out, err = s.canonicalizeRedisWideColumnScanResults(ctx, out, start, end, ts, readRouteVersion) if err != nil { return nil, err } @@ -384,6 +556,55 @@ func (s *ShardStore) ReverseScanAt(ctx context.Context, start []byte, end []byte return out, nil } +func (s *ShardStore) canonicalizeRedisWideColumnScanResults(ctx context.Context, kvs []*store.KVPair, start []byte, end []byte, ts uint64, readRouteVersion uint64) ([]*store.KVPair, error) { + if len(kvs) == 0 { + return kvs, nil + } + if _, _, _, ok := redisWideColumnScanRouteRange(start, end); !ok { + return kvs, nil + } + out := make([]*store.KVPair, 0, len(kvs)) + for _, kvp := range kvs { + if kvp == nil || redisWideColumnLegacyPointRouteKey(kvp.Key) == nil { + out = append(out, kvp) + continue + } + value, err := s.GetAtWithReadFence(ctx, kvp.Key, ts, 0, readRouteVersion) + if err != nil { + if errors.Is(err, store.ErrKeyNotFound) { + continue + } + return nil, err + } + out = append(out, &store.KVPair{Key: bytes.Clone(kvp.Key), Value: value}) + } + return out, nil +} + +func (s *ShardStore) canonicalizeRedisWideColumnScanKeys(ctx context.Context, keys [][]byte, start []byte, end []byte, ts uint64, readRouteVersion uint64) ([][]byte, error) { + if len(keys) == 0 { + return keys, nil + } + if _, _, _, ok := redisWideColumnScanRouteRange(start, end); !ok { + return keys, nil + } + out := make([][]byte, 0, len(keys)) + for _, key := range keys { + if key == nil || redisWideColumnLegacyPointRouteKey(key) == nil { + out = append(out, key) + continue + } + if _, err := s.GetAtWithReadFence(ctx, key, ts, 0, readRouteVersion); err != nil { + if errors.Is(err, store.ErrKeyNotFound) { + continue + } + return nil, err + } + out = append(out, bytes.Clone(key)) + } + return out, nil +} + func (s *ShardStore) ReverseScanAtPhysicalLimit(ctx context.Context, start []byte, end []byte, visibleLimit, physicalLimit int, ts uint64) ([]*store.KVPair, bool, error) { if visibleLimit <= 0 || physicalLimit <= 0 { return []*store.KVPair{}, false, nil @@ -397,73 +618,252 @@ func (s *ShardStore) ReverseScanAtPhysicalLimit(ctx context.Context, start []byt } func (s *ShardStore) routesForForwardScan(start []byte, end []byte) ([]distribution.Route, bool) { - return s.routesForScan(start, end, true) + return s.routesForScan(start, end) } func (s *ShardStore) routesForReverseScan(start []byte, end []byte) ([]distribution.Route, bool) { - return s.routesForScan(start, end, true) + return s.routesForScan(start, end) +} + +func (s *ShardStore) routesForScan(start []byte, end []byte) ([]distribution.Route, bool) { + routes, clampToRoutes, _ := s.routesForScanWithVersion(start, end) + return routes, clampToRoutes } -func (s *ShardStore) routesForScan(start []byte, end []byte, useFilesystemChunkRoutes bool) ([]distribution.Route, bool) { +func (s *ShardStore) routesForScanWithVersion(start []byte, end []byte) ([]distribution.Route, bool, uint64) { if routeStart, routeEnd, ok := s3keys.ManifestScanRouteBounds(start, end); ok { - return s.engine.GetIntersectingRoutes(routeStart, routeEnd), false - } - if routes, ok := s.routesForFilesystemUsageScan(start, end); ok { - return routes, false + routes, version := s.engine.GetIntersectingRoutesWithVersion(routeStart, routeEnd) + return routes, false, version } - if useFilesystemChunkRoutes { - if routes, ok := s.routesForFilesystemChunkScan(start, end); ok { - return routes, false - } + if routes, version, ok := s.routesForEncodedScanWithVersion(start, end); ok { + return routes, false, version } - // For internal list keys, shard routing is based on the logical user key - // rather than the raw key prefix. - if userKey := store.ExtractListUserKey(start); userKey != nil { - route, ok := s.engine.GetRoute(userKey) - if !ok { - return []distribution.Route{}, false - } - return []distribution.Route{route}, false + if routes, version, ok := s.routesForRedisWideColumnScanWithVersion(start, end); ok { + return routes, false, version } - routes := s.engine.GetIntersectingRoutes(start, end) + routes, version := s.engine.GetIntersectingRoutesWithVersion(start, end) // If the scan can include internal list keys (which use a fixed prefix), // avoid clamping to shard range bounds because those keys may be ordered // before the shard range start in raw keyspace. if len(start) == 0 { - return routes, false + return routes, false, version + } + + return routes, true, version +} + +func (s *ShardStore) routesForRedisWideColumnScanWithVersion(start []byte, end []byte) ([]distribution.Route, uint64, bool) { + routeStart, routeEnd, exact, ok := redisWideColumnScanRouteRange(start, end) + if !ok { + return nil, 0, false + } + if !exact { + routes, version := s.engine.GetIntersectingRoutesWithVersion(routeStart, routeEnd) + routes, version = s.appendRedisWideColumnLegacyScanRoutesWithVersion(routes, version, start, end) + return routes, version, true + } + route, version, ok := s.engine.GetRouteWithVersion(routeStart) + if !ok { + return []distribution.Route{}, version, true + } + routes := []distribution.Route{route} + routes, version = s.appendRedisWideColumnLegacyScanRoutesWithVersion(routes, version, start, end) + return routes, version, true +} + +func (s *ShardStore) appendRedisWideColumnLegacyScanRoutesWithVersion(routes []distribution.Route, version uint64, start []byte, end []byte) ([]distribution.Route, uint64) { + legacyStart, legacyEnd, ok := redisWideColumnLegacyScanRouteRange(start, end) + if !ok { + return routes, version + } + legacyRoutes, legacyVersion := s.engine.GetIntersectingRoutesWithVersion(legacyStart, legacyEnd) + version = max(version, legacyVersion) + return appendDistinctRoutesByGroup(routes, legacyRoutes), version +} + +func appendDistinctRoutesByGroup(routes []distribution.Route, candidates []distribution.Route) []distribution.Route { + seen := make(map[uint64]struct{}, len(routes)) + for _, route := range routes { + seen[route.GroupID] = struct{}{} + } + for _, route := range candidates { + if _, ok := seen[route.GroupID]; ok { + continue + } + seen[route.GroupID] = struct{}{} + routes = append(routes, route) + } + return routes +} + +func (s *ShardStore) routesForEncodedScanWithVersion(start []byte, end []byte) ([]distribution.Route, uint64, bool) { + if routes, version, ok := s.routesForFilesystemUsageScanWithVersion(start, end); ok { + return routes, version, true + } + if routes, version, ok := s.routesForFilesystemChunkScanWithVersion(start, end); ok { + return routes, version, true + } + if routeStart, exact, ok := listAuxiliaryScanRouteRange(start, end); ok { + if !exact { + routes, version := s.engine.GetIntersectingRoutesWithVersion(routeStart, nil) + return routes, version, true + } + route, version, ok := s.engine.GetRouteWithVersion(routeStart) + if !ok { + return []distribution.Route{}, version, true + } + return []distribution.Route{route}, version, true } + userKey := listScanUserKey(start) + if userKey == nil { + return nil, 0, false + } + route, version, ok := s.engine.GetRouteWithVersion(userKey) + if !ok { + return []distribution.Route{}, version, true + } + return []distribution.Route{route}, version, true +} + +func listScanUserKey(start []byte) []byte { + if userKey := store.ExtractListUserKeyFromDeltaScanKey(start); userKey != nil { + return userKey + } + if userKey := store.ExtractListUserKeyFromClaimScanKey(start); userKey != nil { + return userKey + } + // Internal list keys route by their logical user key rather than their raw + // storage prefix. + return store.ExtractListUserKey(start) +} - return routes, true +func (s *ShardStore) routesForFencedScanWithVersion(start []byte, end []byte, routeStart []byte, routeEnd []byte) ([]distribution.Route, bool, uint64) { + if routeScanBoundsPresent(routeStart, routeEnd) { + routes, version := s.engine.GetIntersectingRoutesWithVersion(routeStart, normalizedRouteScanEnd(routeEnd)) + return routes, false, version + } + return s.routesForScanWithVersion(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) routesForFilesystemUsageScan(start []byte, end []byte) ([]distribution.Route, bool) { +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) + filterUsageOwners := !clampToRoutes && !routeFilterPresent && filesystemUsageScanOverlap(start, end) + 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.scanRouteAtWithOptionalFilesystemUsageOwnerFilter( + ctx, route, scanStart, scanEnd, limit, ts, false, + readRouteVersion, routeStart, routeEnd, filterUsageOwners, + ) + if err != nil { + return nil, err + } + if clampToRoutes { + out = append(out, kvs...) + if len(out) >= limit { + out = out[:limit] + break + } + continue + } + out = mergeAndTrimScanResults(out, kvs, limit) + } + return out, nil +} + +func (s *ShardStore) scanRouteAtWithOptionalFilesystemUsageOwnerFilter( + ctx context.Context, + route distribution.Route, + start []byte, + end []byte, + limit int, + ts uint64, + reverse bool, + readRouteVersion uint64, + routeStart []byte, + routeEnd []byte, + filterUsageOwners bool, +) ([]*store.KVPair, error) { + if filterUsageOwners { + return s.scanRouteAtWithFilesystemUsageOwnerFilter( + ctx, route, start, end, limit, ts, reverse, + readRouteVersion, routeStart, routeEnd, + ) + } + return s.scanRouteAtDirectionWithReadFence( + ctx, route, start, end, limit, ts, reverse, + readRouteVersion, routeStart, routeEnd, + ) +} + +func (s *ShardStore) routesForFilesystemUsageScanWithVersion(start []byte, end []byte) ([]distribution.Route, uint64, bool) { if !filesystemUsageScanOverlap(start, end) { - return nil, false + return nil, 0, false } // Keep every captured range so backup materialization remains pinned to the // catalog snapshot. Unclamped scan dispatch de-duplicates these by group. - return s.engine.GetIntersectingRoutes(nil, nil), true + routes, version := s.engine.GetIntersectingRoutesWithVersion(nil, nil) + return routes, version, true } -func (s *ShardStore) routesForFilesystemChunkScan(start []byte, end []byte) ([]distribution.Route, bool) { +func (s *ShardStore) routesForFilesystemChunkScanWithVersion(start []byte, end []byte) ([]distribution.Route, uint64, bool) { if routeStart, routeEnd, ok := fskeys.ChunkScanRouteBounds(start, end); ok { - return s.engine.GetIntersectingRoutes(routeStart, routeEnd), true + allRoutes, version := s.engine.GetIntersectingRoutesWithVersion(nil, nil) + return intersectingRoutes(allRoutes, routeStart, routeEnd), version, true } chunkStart, chunkEnd, ok := filesystemChunkScanOverlap(start, end) if !ok { - return nil, false + return nil, 0, false } routeStart, routeEnd, ok := fskeys.ChunkScanRouteBounds(chunkStart, chunkEnd) if !ok { - return nil, false + return nil, 0, false } + allRoutes, version := s.engine.GetIntersectingRoutesWithVersion(nil, nil) // Raw scans can continue from the chunk keyspace into later filesystem // families, so include both raw and virtual chunk route groups rather than // narrowing the scan to chunks only. - routes := s.engine.GetIntersectingRoutes(start, end) - routes = append(routes, s.engine.GetIntersectingRoutes(routeStart, routeEnd)...) - return routes, true + routes := intersectingRoutes(allRoutes, start, end) + routes = append(routes, intersectingRoutes(allRoutes, routeStart, routeEnd)...) + return routes, version, true +} + +func intersectingRoutes(routes []distribution.Route, start []byte, end []byte) []distribution.Route { + result := make([]distribution.Route, 0, len(routes)) + for _, route := range routes { + if route.End != nil && bytes.Compare(route.End, start) <= 0 { + continue + } + if end != nil && bytes.Compare(route.Start, end) >= 0 { + continue + } + result = append(result, route) + } + return result } func filesystemUsageScanOverlap(start []byte, end []byte) bool { @@ -501,65 +901,7 @@ func filesystemChunkScanOverlap(start []byte, end []byte) ([]byte, []byte, bool) return overlapStart, overlapEnd, true } -func (s *ShardStore) scanRoutesAt(ctx context.Context, routes []distribution.Route, start []byte, end []byte, limit int, ts uint64, clampToRoutes bool) ([]*store.KVPair, error) { - out := make([]*store.KVPair, 0) - seenGroups := make(map[uint64]struct{}) - filterUsageOwners := !clampToRoutes && filesystemUsageScanOverlap(start, end) - for _, route := range routes { - if !clampToRoutes { - if _, seen := seenGroups[route.GroupID]; seen { - continue - } - seenGroups[route.GroupID] = struct{}{} - } - scanStart := start - scanEnd := end - explicitGroup := !clampToRoutes - if clampToRoutes { - scanStart = clampScanStart(start, route.Start) - scanEnd = clampScanEnd(end, route.End) - } - - var kvs []*store.KVPair - var err error - if filterUsageOwners { - kvs, err = s.scanRouteAtWithFilesystemUsageOwnerFilter( - ctx, route, scanStart, scanEnd, limit, ts, false, explicitGroup, - ) - } else { - kvs, err = s.scanRouteAtDirection(ctx, route, scanStart, scanEnd, limit, ts, false, explicitGroup) - } - if err != nil { - return nil, err - } - if clampToRoutes { - out = append(out, kvs...) - if len(out) >= limit { - out = out[:limit] - break - } - continue - } - out = mergeAndTrimScanResults(out, kvs, limit) - } - return out, nil -} - -func (s *ShardStore) scanRoutesAtSorted(ctx context.Context, routes []distribution.Route, start []byte, end []byte, limit int, ts uint64, clampToRoutes bool) ([]*store.KVPair, error) { - out, err := s.scanRoutesAt(ctx, routes, start, end, limit, ts, clampToRoutes) - if err != nil { - return nil, err - } - sort.Slice(out, func(i, j int) bool { - return bytes.Compare(out[i].Key, out[j].Key) < 0 - }) - if len(out) > limit { - out = out[:limit] - } - return out, nil -} - -func (s *ShardStore) scanKeyRoutesAt(ctx context.Context, routes []distribution.Route, start []byte, end []byte, limit int, ts uint64, clampToRoutes bool) ([][]byte, error) { +func (s *ShardStore) scanKeyRoutesAtWithReadFence(ctx context.Context, routes []distribution.Route, start []byte, end []byte, limit int, ts uint64, clampToRoutes bool, readRouteVersion uint64) ([][]byte, error) { out := make([][]byte, 0) seenGroups := make(map[uint64]struct{}) filterUsageOwners := !clampToRoutes && filesystemUsageScanOverlap(start, end) @@ -580,10 +922,10 @@ func (s *ShardStore) scanKeyRoutesAt(ctx context.Context, routes []distribution. var err error if filterUsageOwners { keys, err = s.scanKeyRouteAtWithFilesystemUsageOwnerFilter( - ctx, route, scanStart, scanEnd, limit, ts, + ctx, route, scanStart, scanEnd, limit, ts, readRouteVersion, ) } else { - keys, err = s.scanKeyRouteAt(ctx, route, scanStart, scanEnd, limit, ts) + keys, err = s.scanKeyRouteAtWithReadFence(ctx, route, scanStart, scanEnd, limit, ts, readRouteVersion) } if err != nil { return nil, err @@ -638,7 +980,9 @@ func (s *ShardStore) scanRouteAtWithFilesystemUsageOwnerFilter( limit int, ts uint64, reverse bool, - explicitGroup bool, + readRouteVersion uint64, + routeStart []byte, + routeEnd []byte, ) ([]*store.KVPair, error) { if limit <= 0 { return []*store.KVPair{}, nil @@ -648,8 +992,9 @@ func (s *ShardStore) scanRouteAtWithFilesystemUsageOwnerFilter( cursorStart := start cursorEnd := end for len(out) < limit { - page, err := s.scanRouteAtDirection( - ctx, route, cursorStart, cursorEnd, limit, ts, reverse, explicitGroup, + page, err := s.scanRouteAtDirectionWithReadFence( + ctx, route, cursorStart, cursorEnd, limit, ts, reverse, + readRouteVersion, routeStart, routeEnd, ) if err != nil { return nil, err @@ -687,6 +1032,7 @@ func (s *ShardStore) scanKeyRouteAtWithFilesystemUsageOwnerFilter( end []byte, limit int, ts uint64, + readRouteVersion uint64, ) ([][]byte, error) { if limit <= 0 { return [][]byte{}, nil @@ -695,7 +1041,7 @@ func (s *ShardStore) scanKeyRouteAtWithFilesystemUsageOwnerFilter( out := make([][]byte, 0, limit) cursor := start for len(out) < limit { - page, err := s.scanKeyRouteAt(ctx, route, cursor, end, limit, ts) + page, err := s.scanKeyRouteAtWithReadFence(ctx, route, cursor, end, limit, ts, readRouteVersion) if err != nil { return nil, err } @@ -714,7 +1060,7 @@ func (s *ShardStore) scanKeyRouteAtWithFilesystemUsageOwnerFilter( return out, nil } -func (s *ShardStore) reverseScanRoutesAt( +func (s *ShardStore) reverseScanRoutesAtWithReadFence( ctx context.Context, routes []distribution.Route, start []byte, @@ -722,14 +1068,19 @@ 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{}) - filterUsageOwners := !clampToRoutes && filesystemUsageScanOverlap(start, end) - for i := len(routes) - 1; i >= 0; i-- { + routeFilterPresent := routeScanBoundsPresent(routeStart, routeEnd) + filterUsageOwners := !clampToRoutes && !routeFilterPresent && filesystemUsageScanOverlap(start, end) + for i := range routes { route := routes[i] if clampToRoutes { - kvs, done, err := s.clampedReverseScanRouteAt(ctx, route, start, end, limit, len(out), ts) + route = routes[len(routes)-1-i] + kvs, done, err := s.clampedReverseScanRouteAtWithReadFence(ctx, route, start, end, limit, len(out), ts, readRouteVersion, routeStart, routeEnd) if err != nil { return nil, err } @@ -745,20 +1096,18 @@ 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 - } - seenGroups[route.GroupID] = struct{}{} - var kvs []*store.KVPair - var err error - if filterUsageOwners { - kvs, err = s.scanRouteAtWithFilesystemUsageOwnerFilter( - ctx, route, start, end, limit, ts, true, true, - ) - } else { - kvs, err = s.scanRouteAtDirection(ctx, route, start, end, limit, ts, true, true) + // 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{}{} } + kvs, err := s.scanRouteAtWithOptionalFilesystemUsageOwnerFilter( + ctx, route, start, end, limit, ts, true, + readRouteVersion, routeStart, routeEnd, filterUsageOwners, + ) if err != nil { return nil, err } @@ -774,6 +1123,18 @@ func (s *ShardStore) scanKeyRouteAt( end []byte, limit int, ts uint64, +) ([][]byte, error) { + return s.scanKeyRouteAtWithReadFence(ctx, route, start, end, limit, ts, 0) +} + +func (s *ShardStore) scanKeyRouteAtWithReadFence( + ctx context.Context, + route distribution.Route, + start []byte, + end []byte, + limit int, + ts uint64, + readRouteVersion uint64, ) ([][]byte, error) { g, ok := s.groupForID(route.GroupID) if !ok || g == nil || g.Store == nil { @@ -781,14 +1142,14 @@ func (s *ShardStore) scanKeyRouteAt( } if engineForGroup(g) == nil { - return s.scanKeysRouteLocal(ctx, g, start, end, limit, ts) + return s.scanKeysRouteLocal(ctx, g, start, end, limit, ts, readRouteVersion) } if isLinearizableRaftLeader(ctx, engineForGroup(g)) { - return s.scanKeysRouteAtLeader(ctx, g, start, end, limit, ts) + return s.scanKeysRouteAtLeader(ctx, g, start, end, limit, ts, readRouteVersion) } - return s.proxyScanKeysAt(ctx, g, start, end, limit, ts, route.GroupID) + return s.proxyScanKeysAt(ctx, g, start, end, limit, ts, route.GroupID, readRouteVersion) } func (s *ShardStore) scanKeysRouteLocal( @@ -798,6 +1159,7 @@ func (s *ShardStore) scanKeysRouteLocal( end []byte, limit int, ts uint64, + readRouteVersion uint64, ) ([][]byte, error) { return scanKeysWithRefill(start, end, limit, func(cursor []byte, pageLimit int) ([][]byte, error) { keys, err := g.Store.ScanKeysAt(ctx, cursor, end, pageLimit, ts) @@ -805,6 +1167,8 @@ func (s *ShardStore) scanKeysRouteLocal( return nil, errors.WithStack(err) } return keys, nil + }, func(keys [][]byte) ([][]byte, error) { + return s.canonicalizeRedisWideColumnScanKeys(ctx, keys, start, end, ts, readRouteVersion) }) } @@ -815,6 +1179,7 @@ func (s *ShardStore) scanKeysRouteAtLeader( end []byte, limit int, ts uint64, + readRouteVersion uint64, ) ([][]byte, error) { if limit <= 0 { return [][]byte{}, nil @@ -828,7 +1193,7 @@ func (s *ShardStore) scanKeysRouteAtLeader( return nil, errors.WithStack(err) } if len(keys) == 0 { - keys, err := s.scanLockOnlyKeysAtLeader(ctx, g, cursor, end, ts, limit) + keys, err := s.scanLockOnlyVisibleKeysAtLeader(ctx, g, cursor, end, start, ts, limit, readRouteVersion) if err != nil { return nil, err } @@ -846,7 +1211,11 @@ func (s *ShardStore) scanKeysRouteAtLeader( if err != nil { return nil, err } - out = mergeAndTrimScanKeys(out, filterTxnInternalKeys(keysFromKVs(kvs)), limit) + visibleKeys, err := s.visibleScanKeysForReadFence(ctx, keysFromKVs(kvs), start, end, ts, readRouteVersion) + if err != nil { + return nil, err + } + out = mergeAndTrimScanKeys(out, visibleKeys, limit) nextCursor, ok := nextKeyScanCursor(keys, end, limit) if !ok { @@ -857,6 +1226,27 @@ func (s *ShardStore) scanKeysRouteAtLeader( return out, nil } +func (s *ShardStore) scanLockOnlyVisibleKeysAtLeader( + ctx context.Context, + g *ShardGroup, + cursor []byte, + end []byte, + start []byte, + ts uint64, + limit int, + readRouteVersion uint64, +) ([][]byte, error) { + keys, err := s.scanLockOnlyKeysAtLeader(ctx, g, cursor, end, ts, limit) + if err != nil { + return nil, err + } + return s.visibleScanKeysForReadFence(ctx, keys, start, end, ts, readRouteVersion) +} + +func (s *ShardStore) visibleScanKeysForReadFence(ctx context.Context, keys [][]byte, start []byte, end []byte, ts uint64, readRouteVersion uint64) ([][]byte, error) { + return s.canonicalizeRedisWideColumnScanKeys(ctx, filterTxnInternalKeys(keys), start, end, ts, readRouteVersion) +} + func (s *ShardStore) scanLockOnlyKeysAtLeader( ctx context.Context, g *ShardGroup, @@ -887,10 +1277,11 @@ func (s *ShardStore) proxyScanKeysAt( limit int, ts uint64, groupID uint64, + readRouteVersion uint64, ) ([][]byte, error) { return scanKeysWithRefill(start, end, limit, func(cursor []byte, pageLimit int) ([][]byte, error) { - return s.proxyRawScanKeysAt(ctx, g, cursor, end, pageLimit, ts, groupID) - }) + return s.proxyRawScanKeysAt(ctx, g, cursor, end, pageLimit, ts, groupID, readRouteVersion) + }, nil) } func scanKeysWithRefill( @@ -898,6 +1289,7 @@ func scanKeysWithRefill( end []byte, limit int, scan func(cursor []byte, pageLimit int) ([][]byte, error), + filter func(keys [][]byte) ([][]byte, error), ) ([][]byte, error) { if limit <= 0 { return [][]byte{}, nil @@ -914,7 +1306,14 @@ func scanKeysWithRefill( break } - out = mergeAndTrimScanKeys(out, filterTxnInternalKeys(keys), limit) + visibleKeys := filterTxnInternalKeys(keys) + if filter != nil { + visibleKeys, err = filter(visibleKeys) + if err != nil { + return nil, err + } + } + out = mergeAndTrimScanKeys(out, visibleKeys, limit) nextCursor, ok := nextKeyScanCursor(keys, end, limit) if !ok { @@ -946,7 +1345,7 @@ func lastScanKey(keys [][]byte) []byte { return nil } -func (s *ShardStore) clampedReverseScanRouteAt( +func (s *ShardStore) clampedReverseScanRouteAtWithReadFence( ctx context.Context, route distribution.Route, start []byte, @@ -954,6 +1353,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 @@ -961,7 +1363,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, readRouteVersion, routeStart, routeEnd) if err != nil { return nil, false, err } @@ -976,7 +1378,188 @@ func (s *ShardStore) scanRouteAtDirection( limit int, ts uint64, reverse bool, - explicitGroup bool, +) ([]*store.KVPair, error) { + return s.scanRouteAtDirectionWithReadFence(ctx, route, start, end, limit, ts, reverse, 0, nil, nil) +} + +func (s *ShardStore) scanRouteAtDirectionWithReadFence( + ctx context.Context, + route distribution.Route, + start []byte, + end []byte, + limit int, + ts uint64, + reverse bool, + readRouteVersion uint64, + routeStart []byte, + routeEnd []byte, +) ([]*store.KVPair, error) { + if routeScanBoundsPresent(routeStart, routeEnd) { + return s.scanRouteAtDirectionWithReadFenceRouteFilter(ctx, route, start, end, limit, ts, reverse, readRouteVersion, routeStart, routeEnd) + } + return s.scanRouteAtDirectionWithReadFenceOnce(ctx, route, start, end, limit, ts, reverse, readRouteVersion, routeStart, routeEnd) +} + +func (s *ShardStore) scanRouteAtDirectionWithReadFenceRouteFilter( + ctx context.Context, + route distribution.Route, + start []byte, + end []byte, + limit int, + ts uint64, + reverse 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, 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, + 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 { + return s.scanRouteAtDirectionWithReadFenceRouteFilterLocalPage(ctx, g, start, end, limit, ts, reverse, readRouteVersion) + } + + if isLinearizableRaftLeader(ctx, engineForGroup(g)) { + return s.scanRouteAtDirectionWithReadFenceRouteFilterLeaderPage(ctx, g, start, end, limit, visibleLimit, ts, reverse, readRouteVersion, routeStart, routeEnd) + } + + return s.scanRouteAtDirectionWithReadFenceRouteFilterProxyPage(ctx, route, g, start, end, limit, ts, reverse, readRouteVersion, routeStart, routeEnd) +} + +func (s *ShardStore) scanRouteAtDirectionWithReadFenceRouteFilterLocalPage( + ctx context.Context, + g *ShardGroup, + start []byte, + end []byte, + limit int, + ts uint64, + reverse bool, + readRouteVersion uint64, +) ([]*store.KVPair, []*store.KVPair, error) { + kvs, err := s.scanRouteLocal(ctx, g, start, end, limit, ts, reverse) + if err != nil { + return nil, nil, errors.WithStack(err) + } + filtered, err := s.canonicalizeRedisWideColumnScanResults(ctx, filterTxnInternalKVs(kvs), start, end, ts, readRouteVersion) + if err != nil { + return nil, nil, err + } + return filtered, kvs, nil +} + +func (s *ShardStore) scanRouteAtDirectionWithReadFenceRouteFilterLeaderPage( + ctx context.Context, + g *ShardGroup, + start []byte, + end []byte, + limit int, + visibleLimit int, + ts uint64, + reverse bool, + readRouteVersion uint64, + routeStart []byte, + routeEnd []byte, +) ([]*store.KVPair, []*store.KVPair, error) { + kvs, cursorKVs, err := s.scanRouteAtLeaderRouteFilter(ctx, g, start, end, limit, visibleLimit, ts, reverse, routeStart, routeEnd) + if err != nil { + return nil, nil, err + } + kvs, err = s.canonicalizeRedisWideColumnScanResults(ctx, kvs, start, end, ts, readRouteVersion) + return kvs, cursorKVs, err +} + +func (s *ShardStore) scanRouteAtDirectionWithReadFenceRouteFilterProxyPage( + ctx context.Context, + route distribution.Route, + g *ShardGroup, + start []byte, + end []byte, + limit int, + ts uint64, + reverse bool, + readRouteVersion uint64, + routeStart []byte, + routeEnd []byte, +) ([]*store.KVPair, []*store.KVPair, error) { + kvs, err := s.proxyRawScanAt(ctx, g, start, end, limit, ts, reverse, route.GroupID, readRouteVersion, routeStart, routeEnd) + if err != nil { + return nil, nil, err + } + filtered, err := s.canonicalizeRedisWideColumnScanResults(ctx, filterTxnInternalKVs(kvs), start, end, ts, readRouteVersion) + if err != nil { + return nil, nil, err + } + return filtered, kvs, nil +} + +func (s *ShardStore) scanRouteAtDirectionWithReadFenceOnce( + ctx context.Context, + route distribution.Route, + start []byte, + end []byte, + limit int, + ts uint64, + reverse bool, + readRouteVersion uint64, + routeStart []byte, + routeEnd []byte, ) ([]*store.KVPair, error) { g, ok := s.groupForID(route.GroupID) if !ok || g == nil || g.Store == nil { @@ -984,7 +1567,7 @@ func (s *ShardStore) scanRouteAtDirection( } if !reverse { - return s.scanRouteAtForward(ctx, route, g, start, end, limit, ts) + return s.scanRouteAtForward(ctx, route, g, start, end, limit, ts, readRouteVersion, routeStart, routeEnd) } if engineForGroup(g) == nil { @@ -999,11 +1582,7 @@ 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) + kvs, err := s.proxyRawScanAt(ctx, g, start, end, limit, ts, reverse, route.GroupID, readRouteVersion, routeStart, routeEnd) if err != nil { return nil, err } @@ -1012,6 +1591,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 = routeFilterKey(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 scanRoutePage struct { kvs []*store.KVPair advanceKey []byte @@ -1026,6 +1697,9 @@ func (s *ShardStore) scanRouteAtForward( end []byte, limit int, ts uint64, + readRouteVersion uint64, + routeStart []byte, + routeEnd []byte, ) ([]*store.KVPair, error) { if limit <= 0 { return []*store.KVPair{}, nil @@ -1034,7 +1708,7 @@ func (s *ShardStore) scanRouteAtForward( out := make([]*store.KVPair, 0, limit) cursor := start for len(out) < limit { - page, err := s.scanRouteAtForwardPage(ctx, route, g, cursor, end, limit, ts) + page, err := s.scanRouteAtForwardPage(ctx, route, g, cursor, end, limit, ts, readRouteVersion, routeStart, routeEnd) if err != nil { return nil, err } @@ -1058,58 +1732,112 @@ func (s *ShardStore) scanRouteAtForward( if len(out) > limit { out = out[:limit] } - return out, nil + return out, nil +} + +func (s *ShardStore) scanRouteAtForwardPage( + ctx context.Context, + route distribution.Route, + g *ShardGroup, + start []byte, + end []byte, + limit int, + ts uint64, + readRouteVersion uint64, + routeStart []byte, + routeEnd []byte, +) (scanRoutePage, error) { + engine := engineForGroup(g) + if engine == nil { + return s.scanRouteAtForwardLocalPage(ctx, g, start, end, limit, ts, readRouteVersion) + } + + if isLinearizableRaftLeader(ctx, engine) { + return s.scanRouteAtForwardLeaderPage(ctx, g, start, end, limit, ts, readRouteVersion) + } + + return s.scanRouteAtForwardProxyPage(ctx, route, g, start, end, limit, ts, readRouteVersion, routeStart, routeEnd) } -func (s *ShardStore) scanRouteAtForwardPage( +func (s *ShardStore) scanRouteAtForwardLocalPage( ctx context.Context, - route distribution.Route, g *ShardGroup, start []byte, end []byte, limit int, ts uint64, + readRouteVersion uint64, ) (scanRoutePage, error) { - engine := engineForGroup(g) - if engine == nil { - raw, err := s.scanRouteLocal(ctx, g, start, end, limit, ts, false) - if err != nil { - return scanRoutePage{}, errors.WithStack(err) - } - return scanRoutePage{ - kvs: filterTxnInternalKVs(raw), - advanceKey: lastKVKey(raw), - full: len(raw) >= limit, - }, nil + raw, err := s.scanRouteLocal(ctx, g, start, end, limit, ts, false) + if err != nil { + return scanRoutePage{}, errors.WithStack(err) + } + kvs, err := s.canonicalizeRedisWideColumnScanResults(ctx, filterTxnInternalKVs(raw), start, end, ts, readRouteVersion) + if err != nil { + return scanRoutePage{}, err } + return scanRoutePage{ + kvs: kvs, + advanceKey: lastKVKey(raw), + full: len(raw) >= limit, + }, nil +} - if isLinearizableRaftLeader(ctx, engine) { - raw, err := g.Store.ScanAt(ctx, start, end, limit, ts) - if err != nil { - return scanRoutePage{}, errors.WithStack(err) - } - lockStart, lockEnd := scanLockBoundsForKVs(raw, start, end, limit) - lockKVs, err := scanTxnLockRangeAt(ctx, g, lockStart, lockEnd, ts, limit) - if err != nil { - return scanRoutePage{}, err - } - kvs, err := s.resolveScanLocks(ctx, g, raw, lockKVs, ts) - if err != nil { - return scanRoutePage{}, err - } - return scanRoutePage{ - kvs: filterTxnInternalKVs(kvs), - advanceKey: lastKVKey(raw), - full: len(raw) >= limit, - }, nil +func (s *ShardStore) scanRouteAtForwardLeaderPage( + ctx context.Context, + g *ShardGroup, + start []byte, + end []byte, + limit int, + ts uint64, + readRouteVersion uint64, +) (scanRoutePage, error) { + raw, err := g.Store.ScanAt(ctx, start, end, limit, ts) + if err != nil { + return scanRoutePage{}, errors.WithStack(err) + } + lockStart, lockEnd := scanLockBoundsForKVs(raw, start, end, limit) + lockKVs, err := scanTxnLockRangeAt(ctx, g, lockStart, lockEnd, ts, limit) + if err != nil { + return scanRoutePage{}, err + } + kvs, err := s.resolveScanLocks(ctx, g, raw, lockKVs, ts) + if err != nil { + return scanRoutePage{}, err + } + kvs, err = s.canonicalizeRedisWideColumnScanResults(ctx, filterTxnInternalKVs(kvs), start, end, ts, readRouteVersion) + if err != nil { + return scanRoutePage{}, err } + return scanRoutePage{ + kvs: kvs, + advanceKey: lastKVKey(raw), + full: len(raw) >= limit, + }, nil +} - raw, err := s.proxyRawScanAt(ctx, g, start, end, limit, ts, false, route.GroupID) +func (s *ShardStore) scanRouteAtForwardProxyPage( + ctx context.Context, + route distribution.Route, + g *ShardGroup, + start []byte, + end []byte, + limit int, + ts uint64, + readRouteVersion uint64, + routeStart []byte, + routeEnd []byte, +) (scanRoutePage, error) { + raw, err := s.proxyRawScanAt(ctx, g, start, end, limit, ts, false, route.GroupID, readRouteVersion, routeStart, routeEnd) + if err != nil { + return scanRoutePage{}, err + } + kvs, err := s.canonicalizeRedisWideColumnScanResults(ctx, filterTxnInternalKVs(raw), start, end, ts, readRouteVersion) if err != nil { return scanRoutePage{}, err } return scanRoutePage{ - kvs: filterTxnInternalKVs(raw), + kvs: kvs, advanceKey: lastKVKey(raw), full: len(raw) >= limit, }, nil @@ -1261,6 +1989,62 @@ 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) + if err == nil { + sort.Slice(resolved, func(i, j int) bool { + if reverse { + return bytes.Compare(resolved[i].Key, resolved[j].Key) > 0 + } + return bytes.Compare(resolved[i].Key, resolved[j].Key) < 0 + }) + } + 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) } @@ -1269,8 +2053,14 @@ func scanLockBoundsForKVsDirection(kvs []*store.KVPair, scanStart []byte, scanEn if len(kvs) < limit { return scanStart, scanEnd } - _, lastUserKey, ok := observedScanUserBounds(kvs) + firstUserKey, lastUserKey, ok := observedScanUserBounds(kvs) if ok { + if reverse { + if len(scanStart) == 0 || bytes.Compare(firstUserKey, scanStart) > 0 { + scanStart = firstUserKey + } + return scanStart, scanEnd + } return scanStart, boundScanEnd(scanEnd, nextScanCursor(lastUserKey)) } if reverse { @@ -1327,16 +2117,33 @@ func mergeAndTrimScanResults(out []*store.KVPair, kvs []*store.KVPair, limit int return out } out = append(out, kvs...) + sort.SliceStable(out, func(i, j int) bool { + return bytes.Compare(out[i].Key, out[j].Key) < 0 + }) + out = dedupeSortedScanResults(out) if len(out) <= limit { return out } - sort.Slice(out, func(i, j int) bool { - return bytes.Compare(out[i].Key, out[j].Key) < 0 - }) clear(out[limit:]) return out[:limit] } +func dedupeSortedScanResults(kvs []*store.KVPair) []*store.KVPair { + write := 0 + for _, kvp := range kvs { + if kvp == nil { + continue + } + if write > 0 && bytes.Equal(kvs[write-1].Key, kvp.Key) { + continue + } + kvs[write] = kvp + write++ + } + clear(kvs[write:]) + return kvs[:write] +} + func mergeAndTrimScanKeys(out [][]byte, keys [][]byte, limit int) [][]byte { if len(keys) == 0 { return out @@ -1370,9 +2177,10 @@ func mergeAndTrimReverseScanResults(out []*store.KVPair, kvs []*store.KVPair, li return out } out = append(out, kvs...) - sort.Slice(out, func(i, j int) bool { + sort.SliceStable(out, func(i, j int) bool { return bytes.Compare(out[i].Key, out[j].Key) > 0 }) + out = dedupeSortedScanResults(out) if len(out) <= limit { return out } @@ -1455,39 +2263,74 @@ func clampScanEnd(end []byte, routeEnd []byte) []byte { } func (s *ShardStore) PutAt(ctx context.Context, key []byte, value []byte, commitTS uint64, expireAt uint64) error { - g, ok := s.groupForKey(key) - if !ok || g.Store == nil { - return store.ErrNotSupported + g, err := s.writeGroupForKey(key, commitTS) + if err != nil { + return err } return errors.WithStack(g.Store.PutAt(ctx, key, value, commitTS, expireAt)) } func (s *ShardStore) DeleteAt(ctx context.Context, key []byte, commitTS uint64) error { - g, ok := s.groupForKey(key) - if !ok || g.Store == nil { - return store.ErrNotSupported + g, err := s.writeGroupForKey(key, commitTS) + if err != nil { + return err } return errors.WithStack(g.Store.DeleteAt(ctx, key, commitTS)) } func (s *ShardStore) PutWithTTLAt(ctx context.Context, key []byte, value []byte, commitTS uint64, expireAt uint64) error { - g, ok := s.groupForKey(key) - if !ok || g.Store == nil { - return store.ErrNotSupported + g, err := s.writeGroupForKey(key, commitTS) + if err != nil { + return err } return errors.WithStack(g.Store.PutWithTTLAt(ctx, key, value, commitTS, expireAt)) } func (s *ShardStore) ExpireAt(ctx context.Context, key []byte, expireAt uint64, commitTS uint64) error { - g, ok := s.groupForKey(key) - if !ok || g.Store == nil { - return store.ErrNotSupported + g, err := s.writeGroupForKey(key, commitTS) + if err != nil { + return err } return errors.WithStack(g.Store.ExpireAt(ctx, key, expireAt, commitTS)) } func (s *ShardStore) LatestCommitTS(ctx context.Context, key []byte) (uint64, bool, error) { - g, ok := s.groupForKey(key) + return s.LatestCommitTSWithReadFence(ctx, key, 0) +} + +func (s *ShardStore) LatestCommitTSGroupWithReadFence(ctx context.Context, key []byte, groupID uint64, readRouteVersion uint64) (uint64, bool, error) { + if err := s.awaitReadRouteVersion(ctx, readRouteVersion); err != nil { + return 0, false, err + } + return s.latestCommitTSForRoute(ctx, distribution.Route{GroupID: groupID}, key, readRouteVersion) +} + +func (s *ShardStore) LatestCommitTSWithReadFence(ctx context.Context, key []byte, readRouteVersion uint64) (uint64, bool, error) { + if err := s.awaitReadRouteVersion(ctx, readRouteVersion); err != nil { + return 0, false, err + } + routes, routeVersion := s.pointReadRoutesWithVersion(key) + readRouteVersion = max(readRouteVersion, routeVersion) + if len(routes) == 0 { + return 0, false, nil + } + var latest uint64 + found := false + for _, route := range routes { + ts, exists, err := s.latestCommitTSForRoute(ctx, route, key, readRouteVersion) + if err != nil { + return 0, false, err + } + if exists && (!found || ts > latest) { + latest = ts + found = true + } + } + return latest, found, nil +} + +func (s *ShardStore) latestCommitTSForRoute(ctx context.Context, route distribution.Route, key []byte, readRouteVersion uint64) (uint64, bool, error) { + g, ok := s.groupForID(route.GroupID) if !ok || g.Store == nil { return 0, false, nil } @@ -1513,10 +2356,10 @@ func (s *ShardStore) LatestCommitTS(ctx context.Context, key []byte) (uint64, bo } } - return s.proxyLatestCommitTS(ctx, g, key) + return s.proxyLatestCommitTS(ctx, g, route.GroupID, 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, groupID uint64, key []byte, readRouteVersion uint64) (uint64, bool, error) { engine := engineForGroup(g) if engine == nil { return 0, false, nil @@ -1534,7 +2377,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, GroupId: groupID}) if err != nil { return 0, false, errors.WithStack(err) } @@ -1817,6 +2660,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 @@ -1839,6 +2694,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 scanned >= limit && !done { + return nil, errors.Wrapf(ErrTxnLocked, "scan lock budget exceeded for range [%q,%q)", string(start), string(end)) + } + if done { + return out, nil + } + cursor = nextCursor + } +} + const lockPageLimit = 256 const maxTxnLockScanResults = 1024 @@ -2128,7 +3012,7 @@ func cleanupTSWithNow(startTS, now uint64) uint64 { // All mutations must belong to the same shard. Cross-shard mutation batches are // not supported. func (s *ShardStore) ApplyMutations(ctx context.Context, mutations []*store.KVPairMutation, readKeys [][]byte, startTS, commitTS uint64) error { - group, err := s.resolveSingleShardGroup(mutations) + group, err := s.resolveSingleShardGroup(mutations, commitTS) if err != nil || group == nil { return err } @@ -2138,7 +3022,7 @@ func (s *ShardStore) ApplyMutations(ctx context.Context, mutations []*store.KVPa // ApplyMutationsRaft is the raft-apply variant; see store.MVCCStore for the // durability contract. Only the FSM may call this method. func (s *ShardStore) ApplyMutationsRaft(ctx context.Context, mutations []*store.KVPairMutation, readKeys [][]byte, startTS, commitTS uint64) error { - group, err := s.resolveSingleShardGroup(mutations) + group, err := s.resolveSingleShardGroup(mutations, commitTS) if err != nil || group == nil { return err } @@ -2149,29 +3033,141 @@ func (s *ShardStore) ApplyMutationsRaft(ctx context.Context, mutations []*store. // appliedIndex through to the single owning shard so the leaf can // bundle metaAppliedIndex with the mutation. See PR #910 design §2. func (s *ShardStore) ApplyMutationsRaftAt(ctx context.Context, mutations []*store.KVPairMutation, readKeys [][]byte, startTS, commitTS, appliedIndex uint64) error { - group, err := s.resolveSingleShardGroup(mutations) + group, err := s.resolveSingleShardGroup(mutations, commitTS) if err != nil || group == nil { return err } return errors.WithStack(group.Store.ApplyMutationsRaftAt(ctx, mutations, readKeys, startTS, commitTS, appliedIndex)) } -// resolveSingleShardGroup returns the shard group that owns every -// mutation in the batch, or an error if the batch is cross-shard or -// references an unknown group. A nil group with nil error means "empty -// batch — caller should no-op". -func (s *ShardStore) resolveSingleShardGroup(mutations []*store.KVPairMutation) (*ShardGroup, error) { - if len(mutations) == 0 { - return nil, nil +func (s *ShardStore) writeGroupForKey(key []byte, commitTS uint64) (*ShardGroup, error) { + route, ok := s.engine.GetRoute(routeKey(key)) + if !ok { + return nil, store.ErrNotSupported + } + if err := ensureRouteWriteAllowed(route, key, commitTS); err != nil { + return nil, err } - firstGroup, ok := s.groupForKey(mutations[0].Key) - if !ok || firstGroup == nil || firstGroup.Store == nil { + g, ok := s.groupForID(route.GroupID) + if !ok || g.Store == nil { return nil, store.ErrNotSupported } - for i := 1; i < len(mutations); i++ { - g, ok := s.groupForKey(mutations[i].Key) - if !ok || g == nil || g.Store == nil { - return nil, store.ErrNotSupported + return g, nil +} + +func ensureRouteWriteAllowed(route distribution.Route, key []byte, commitTS uint64) error { + if route.MinWriteTSExclusive == 0 || commitTS > route.MinWriteTSExclusive { + return nil + } + return errors.Wrapf( + store.NewWriteConflictError(key), + "route min_write_ts_exclusive=%d rejects commit_ts=%d", + route.MinWriteTSExclusive, + commitTS, + ) +} + +func (s *ShardStore) ensurePrefixWriteAllowed(prefix []byte, commitTS uint64) error { + routes := s.routesForPrefixWrite(prefix) + for _, route := range routes { + if err := ensureRouteWriteAllowed(route, prefix, commitTS); err != nil { + return err + } + } + return nil +} + +func (s *ShardStore) routesForPrefixWrite(prefix []byte) []distribution.Route { + if routes, ok := s.routesForRedisWideColumnPrefixWrite(prefix); ok { + return routes + } + if routes, ok := s.routesForRedisInternalPrefixWrite(prefix); ok { + return routes + } + if routes, ok := s.routesForRedisListPrefixWrite(prefix); ok { + return routes + } + return s.engine.GetIntersectingRoutes(prefix, prefixScanEnd(prefix)) +} + +func (s *ShardStore) routesForRedisWideColumnPrefixWrite(prefix []byte) ([]distribution.Route, bool) { + routeStart, routeEnd, exact, ok := redisWideColumnScanRouteRange(prefix, prefixScanEnd(prefix)) + if !ok { + return nil, false + } + return s.routesForLogicalPrefixWriteRange(routeStart, routeEnd, exact), true +} + +func (s *ShardStore) routesForRedisInternalPrefixWrite(prefix []byte) ([]distribution.Route, bool) { + if !bytes.HasPrefix(prefix, redisInternalRoutePrefixBytes) && !bytes.HasPrefix(redisInternalRoutePrefixBytes, prefix) { + return nil, false + } + if !bytes.HasPrefix(prefix, redisInternalRoutePrefixBytes) { + return s.engine.GetIntersectingRoutes(nil, nil), true + } + rest := prefix[len(redisInternalRoutePrefix):] + sep := bytes.IndexByte(rest, '|') + if sep < 0 || sep+1 >= len(rest) { + return s.engine.GetIntersectingRoutes(nil, nil), true + } + routeStart := rest[sep+1:] + return s.engine.GetIntersectingRoutes(routeStart, prefixScanEnd(routeStart)), true +} + +func (s *ShardStore) routesForRedisListPrefixWrite(prefix []byte) ([]distribution.Route, bool) { + if routeStart, exact, ok := listAuxiliaryScanRouteRange(prefix, prefixScanEnd(prefix)); ok { + return s.routesForLogicalPrefixWriteRange(routeStart, nil, exact), true + } + for _, family := range [][]byte{[]byte(store.ListMetaPrefix), []byte(store.ListItemPrefix)} { + if bytes.HasPrefix(family, prefix) { + return s.engine.GetIntersectingRoutes(nil, nil), true + } + if !bytes.HasPrefix(prefix, family) { + continue + } + if bytes.Equal(family, []byte(store.ListItemPrefix)) { + return s.engine.GetIntersectingRoutes(nil, nil), true + } + routeStart := prefix[len(family):] + if len(routeStart) == 0 { + return s.engine.GetIntersectingRoutes(nil, nil), true + } + return s.engine.GetIntersectingRoutes(routeStart, prefixScanEnd(routeStart)), true + } + return nil, false +} + +func (s *ShardStore) routesForLogicalPrefixWriteRange(routeStart []byte, routeEnd []byte, exact bool) []distribution.Route { + if exact { + route, ok := s.engine.GetRoute(routeStart) + if !ok { + return nil + } + return []distribution.Route{route} + } + return s.engine.GetIntersectingRoutes(routeStart, routeEnd) +} + +// resolveSingleShardGroup returns the shard group that owns every mutation in +// the batch, or an error if the batch is cross-shard, below a route write +// timestamp floor, or references an unknown group. A nil group with nil error +// means "empty batch; caller should no-op". +func (s *ShardStore) resolveSingleShardGroup(mutations []*store.KVPairMutation, commitTS uint64) (*ShardGroup, error) { + if len(mutations) == 0 { + return nil, nil + } + var firstGroup *ShardGroup + for _, mutation := range mutations { + g, err := s.writeGroupForMutation(mutation, commitTS) + if err != nil { + return nil, err + } + if g == nil { + continue + } + if firstGroup == nil { + firstGroup = g + continue } if g != firstGroup { return nil, errors.WithStack(ErrCrossShardMutationBatchNotSupported) @@ -2180,8 +3176,18 @@ func (s *ShardStore) resolveSingleShardGroup(mutations []*store.KVPairMutation) return firstGroup, nil } +func (s *ShardStore) writeGroupForMutation(mutation *store.KVPairMutation, commitTS uint64) (*ShardGroup, error) { + if mutation == nil { + return nil, nil + } + return s.writeGroupForKey(mutation.Key, commitTS) +} + // DeletePrefixAt applies a prefix delete to every shard in the store. func (s *ShardStore) DeletePrefixAt(ctx context.Context, prefix []byte, excludePrefix []byte, commitTS uint64) error { + if err := s.ensurePrefixWriteAllowed(prefix, commitTS); err != nil { + return err + } for _, g := range s.groups { if g == nil || g.Store == nil { continue @@ -2195,6 +3201,9 @@ func (s *ShardStore) DeletePrefixAt(ctx context.Context, prefix []byte, excludeP // DeletePrefixAtRaft is the raft-apply variant of DeletePrefixAt. func (s *ShardStore) DeletePrefixAtRaft(ctx context.Context, prefix []byte, excludePrefix []byte, commitTS uint64) error { + if err := s.ensurePrefixWriteAllowed(prefix, commitTS); err != nil { + return err + } for _, g := range s.groups { if g == nil || g.Store == nil { continue @@ -2221,6 +3230,9 @@ func (s *ShardStore) DeletePrefixAtRaft(ctx context.Context, prefix []byte, excl // is the receiver only when an aggregate (admin / coordinator) path // is replaying a global FLUSHALL, which is not raft-applied. func (s *ShardStore) DeletePrefixAtRaftAt(ctx context.Context, prefix []byte, excludePrefix []byte, commitTS, appliedIndex uint64) error { + if err := s.ensurePrefixWriteAllowed(prefix, commitTS); err != nil { + return err + } for _, g := range s.groups { if g == nil || g.Store == nil { continue @@ -2446,7 +3458,7 @@ func (s *ShardStore) LocalStores() []store.MVCCStore { return stores } -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 @@ -2464,7 +3476,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) } @@ -2485,6 +3497,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 { @@ -2504,12 +3519,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) @@ -2534,6 +3553,7 @@ func (s *ShardStore) proxyRawScanKeysAt( limit int, ts uint64, groupID uint64, + readRouteVersion uint64, ) ([][]byte, error) { engine := engineForGroup(g) if engine == nil { @@ -2553,12 +3573,13 @@ func (s *ShardStore) proxyRawScanKeysAt( defer cancel() cli := pb.NewRawKVClient(conn) resp, err := cli.RawScanAt(ctx, &pb.RawScanAtRequest{ - StartKey: start, - EndKey: end, - Limit: int64(limit), - Ts: ts, - GroupId: groupID, - KeysOnly: true, + StartKey: start, + EndKey: end, + Limit: int64(limit), + Ts: ts, + GroupId: groupID, + ReadRouteVersion: readRouteVersion, + KeysOnly: true, }) if err != nil { return nil, errors.WithStack(err) diff --git a/kv/shard_store_test.go b/kv/shard_store_test.go index 83f6ceb63..5364aad5c 100644 --- a/kv/shard_store_test.go +++ b/kv/shard_store_test.go @@ -4,6 +4,7 @@ import ( "context" "sync" "testing" + "time" "github.com/bootjp/elastickv/distribution" "github.com/bootjp/elastickv/internal/fskeys" @@ -118,18 +119,74 @@ func TestShardStoreScanAt_RoutesListItemScansByUserKey(t *testing.T) { require.Equal(t, k2, kvs[2].Key) } -func TestShardStoreLocalStoresUsesStableGroupOrder(t *testing.T) { +func TestShardStoreScanAtWithReadFence_RoutesListAuxiliaryScansByUserKey(t *testing.T) { t.Parallel() - first := store.NewMVCCStore() - second := store.NewMVCCStore() - shards := NewShardStore(distribution.NewEngine(), map[uint64]*ShardGroup{ - 20: {Store: second}, - 10: {Store: first}, - 30: nil, + 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()}, + } + t.Cleanup(func() { + _ = groups[1].Store.Close() + _ = groups[2].Store.Close() }) + shardStore := NewShardStore(engine, groups) + userKey := []byte("x") + deltaKey := store.ListMetaDeltaKey(userKey, 10, 0) + claimKey := store.ListClaimKey(userKey, 1) + require.NoError(t, groups[2].Store.PutAt(ctx, deltaKey, []byte("delta"), 10, 0)) + require.NoError(t, groups[2].Store.PutAt(ctx, claimKey, []byte("claim"), 11, 0)) + + for _, tc := range []struct { + name string + prefix []byte + key []byte + }{ + {name: "delta", prefix: store.ListMetaDeltaScanPrefix(userKey), key: deltaKey}, + {name: "claim", prefix: store.ListClaimScanPrefix(userKey), key: claimKey}, + } { + t.Run(tc.name, func(t *testing.T) { + kvs, err := shardStore.ScanAtWithReadFence( + ctx, tc.prefix, prefixScanEnd(tc.prefix), 10, ^uint64(0), false, 0, engine.Version(), nil, nil, + ) + require.NoError(t, err) + require.Len(t, kvs, 1) + require.Equal(t, tc.key, kvs[0].Key) + }) + } +} + +func TestShardStoreScanAt_RoutesBareListAuxiliaryScansAcrossShards(t *testing.T) { + t.Parallel() - require.Equal(t, []store.MVCCStore{first, second}, shards.LocalStores()) + 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()}, + } + t.Cleanup(func() { + _ = groups[1].Store.Close() + _ = groups[2].Store.Close() + }) + st := NewShardStore(engine, groups) + + left := store.ListMetaDeltaKey([]byte("anna"), 10, 0) + right := store.ListMetaDeltaKey([]byte("zoey"), 11, 0) + require.NoError(t, groups[1].Store.PutAt(ctx, left, []byte("left"), 10, 0)) + require.NoError(t, groups[2].Store.PutAt(ctx, right, []byte("right"), 11, 0)) + + prefix := []byte(store.ListMetaDeltaPrefix) + kvs, err := st.ScanAt(ctx, prefix, prefixScanEnd(prefix), 10, ^uint64(0)) + require.NoError(t, err) + require.Len(t, kvs, 2) + require.Equal(t, [][]byte{left, right}, [][]byte{kvs[0].Key, kvs[1].Key}) } func TestShardStoreScanGroupAt_UsesExplicitGroup(t *testing.T) { @@ -177,6 +234,631 @@ func TestShardStoreGetGroupAt_UsesExplicitGroup(t *testing.T) { require.ErrorIs(t, err, store.ErrKeyNotFound) } +func TestShardStoreWritePathsRejectRouteWriteTimestampFloor(t *testing.T) { + t.Parallel() + + ctx := context.Background() + engine := distribution.NewEngine() + require.NoError(t, engine.ApplySnapshot(distribution.CatalogSnapshot{ + Version: 1, + Routes: []distribution.RouteDescriptor{ + { + RouteID: 1, + Start: []byte(""), + End: nil, + GroupID: 1, + State: distribution.RouteStateActive, + MinWriteTSExclusive: 10, + }, + }, + })) + groups := map[uint64]*ShardGroup{ + 1: {Store: store.NewMVCCStore()}, + } + t.Cleanup(func() { + _ = groups[1].Store.Close() + }) + st := NewShardStore(engine, groups) + + require.ErrorIs(t, st.PutAt(ctx, []byte("put-stale"), []byte("v"), 10, 0), store.ErrWriteConflict) + require.NoError(t, st.PutAt(ctx, []byte("put-fresh"), []byte("v"), 11, 0)) + + require.ErrorIs(t, st.DeleteAt(ctx, []byte("delete-stale"), 10), store.ErrWriteConflict) + require.NoError(t, st.DeleteAt(ctx, []byte("delete-fresh"), 11)) + + require.ErrorIs(t, st.PutWithTTLAt(ctx, []byte("ttl-stale"), []byte("v"), 10, 99), store.ErrWriteConflict) + require.NoError(t, st.PutWithTTLAt(ctx, []byte("ttl-fresh"), []byte("v"), 11, 99)) + + require.ErrorIs(t, st.ExpireAt(ctx, []byte("expire-stale"), 99, 10), store.ErrWriteConflict) + require.NoError(t, st.PutAt(ctx, []byte("expire-fresh"), []byte("v"), 11, 0)) + require.NoError(t, st.ExpireAt(ctx, []byte("expire-fresh"), 99, 12)) + + require.ErrorIs(t, st.ApplyMutations(ctx, []*store.KVPairMutation{ + {Op: store.OpTypePut, Key: []byte("apply-stale"), Value: []byte("v")}, + }, nil, 0, 10), store.ErrWriteConflict) + require.NoError(t, st.ApplyMutations(ctx, []*store.KVPairMutation{ + {Op: store.OpTypePut, Key: []byte("apply-fresh"), Value: []byte("v")}, + }, nil, 0, 11)) + + require.ErrorIs(t, st.ApplyMutationsRaft(ctx, []*store.KVPairMutation{ + {Op: store.OpTypePut, Key: []byte("raft-stale"), Value: []byte("v")}, + }, nil, 0, 10), store.ErrWriteConflict) + require.NoError(t, st.ApplyMutationsRaft(ctx, []*store.KVPairMutation{ + {Op: store.OpTypePut, Key: []byte("raft-fresh"), Value: []byte("v")}, + }, nil, 0, 11)) + + require.ErrorIs(t, st.ApplyMutationsRaftAt(ctx, []*store.KVPairMutation{ + {Op: store.OpTypePut, Key: []byte("raft-at-stale"), Value: []byte("v")}, + }, nil, 0, 10, 1), store.ErrWriteConflict) + require.NoError(t, st.ApplyMutationsRaftAt(ctx, []*store.KVPairMutation{ + {Op: store.OpTypePut, Key: []byte("raft-at-fresh"), Value: []byte("v")}, + }, nil, 0, 11, 2)) + + require.ErrorIs(t, st.DeletePrefixAt(ctx, []byte("prefix-stale"), nil, 10), store.ErrWriteConflict) + require.NoError(t, st.DeletePrefixAt(ctx, []byte("prefix-fresh"), nil, 11)) + + require.ErrorIs(t, st.DeletePrefixAtRaft(ctx, []byte("raft-prefix-stale"), nil, 10), store.ErrWriteConflict) + require.NoError(t, st.DeletePrefixAtRaft(ctx, []byte("raft-prefix-fresh"), nil, 11)) + + require.ErrorIs(t, st.DeletePrefixAtRaftAt(ctx, []byte("raft-at-prefix-stale"), nil, 10, 3), store.ErrWriteConflict) + require.NoError(t, st.DeletePrefixAtRaftAt(ctx, []byte("raft-at-prefix-fresh"), nil, 11, 4)) +} + +func TestShardStoreDeletePrefixChecksRedisLogicalRouteFloors(t *testing.T) { + t.Parallel() + + ctx := context.Background() + engine := distribution.NewEngine() + require.NoError(t, engine.ApplySnapshot(distribution.CatalogSnapshot{ + Version: 1, + Routes: []distribution.RouteDescriptor{ + {RouteID: 1, Start: []byte(""), End: []byte("m"), GroupID: 1, State: distribution.RouteStateActive}, + {RouteID: 2, Start: []byte("m"), GroupID: 2, State: distribution.RouteStateActive, MinWriteTSExclusive: 100}, + }, + })) + groups := map[uint64]*ShardGroup{ + 1: {Store: store.NewMVCCStore()}, + 2: {Store: store.NewMVCCStore()}, + } + t.Cleanup(func() { + _ = groups[1].Store.Close() + _ = groups[2].Store.Close() + }) + st := NewShardStore(engine, groups) + + require.ErrorIs(t, st.DeletePrefixAt(ctx, []byte(store.HashFieldPrefix), nil, 100), store.ErrWriteConflict) + require.ErrorIs(t, st.DeletePrefixAt(ctx, []byte("!lst|"), nil, 100), store.ErrWriteConflict) + require.ErrorIs(t, st.DeletePrefixAt(ctx, []byte("!redis|hash|"), nil, 100), store.ErrWriteConflict) + require.NoError(t, st.DeletePrefixAt(ctx, store.HashFieldScanPrefix([]byte("alpha")), nil, 100)) + require.ErrorIs(t, st.DeletePrefixAt(ctx, store.HashFieldScanPrefix([]byte("zulu")), nil, 100), store.ErrWriteConflict) + require.NoError(t, st.DeletePrefixAt(ctx, store.HashFieldScanPrefix([]byte("zulu")), nil, 101)) +} + +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() + require.NoError(t, engine.ApplySnapshot(distribution.CatalogSnapshot{ + Version: 100, + Routes: []distribution.RouteDescriptor{ + {RouteID: 1, Start: []byte(""), GroupID: 1, State: distribution.RouteStateActive}, + }, + })) + st := NewShardStore(engine, map[uint64]*ShardGroup{ + 1: { + Store: store.NewMVCCStore(), + Engine: &stubFollowerEngine{leaderAddr: addr}, + }, + }) + t.Cleanup(func() { _ = st.Close() }) + + ctx := context.Background() + _, err := st.GetAt(ctx, []byte("k"), 10) + require.NoError(t, err) + _, _, err = st.LatestCommitTS(ctx, []byte("k")) + 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(100), fake.lastGetReq.GetReadRouteVersion()) + require.Equal(t, uint64(100), fake.lastLatestReq.GetReadRouteVersion()) + require.Equal(t, uint64(1), fake.lastLatestReq.GetGroupId()) + require.Equal(t, uint64(100), 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.ScanAt(ctx, []byte("a"), []byte("z"), 10, 11) + require.NoError(t, err) + + fake.mu.Lock() + require.Equal(t, uint64(1), fake.lastScanReq.GetGroupId()) + require.Equal(t, uint64(100), fake.lastScanReq.GetReadRouteVersion()) + fake.mu.Unlock() + + _, err = st.ScanKeysAtWithReadFence(ctx, []byte("a"), []byte("z"), 10, 11, 0, 82) + require.NoError(t, err) + + fake.mu.Lock() + require.Equal(t, uint64(1), fake.lastScanReq.GetGroupId()) + require.Equal(t, uint64(100), fake.lastScanReq.GetReadRouteVersion()) + require.True(t, fake.lastScanReq.GetKeysOnly()) + 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(100), 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() + require.Equal(t, uint64(1), fake.lastScanReq.GetGroupId()) + require.Equal(t, uint64(100), fake.lastScanReq.GetReadRouteVersion()) + require.False(t, fake.lastScanReq.GetRouteBoundsPresent()) + fake.mu.Unlock() + + _, err = st.ScanAt(ctx, []byte(""), nil, 10, 11) + require.NoError(t, err) + + fake.mu.Lock() + defer fake.mu.Unlock() + require.Equal(t, uint64(1), fake.lastScanReq.GetGroupId()) + require.Equal(t, uint64(100), fake.lastScanReq.GetReadRouteVersion()) +} + +func TestShardStoreRoutesForScanUsesWideColumnUserKey(t *testing.T) { + t.Parallel() + + engine := distribution.NewEngine() + engine.UpdateRoute([]byte(""), []byte("m"), 1) + engine.UpdateRoute([]byte("m"), nil, 2) + st := NewShardStore(engine, nil) + userKey := []byte("z-user") + + for _, tc := range []struct { + name string + prefix []byte + }{ + {name: "hash fields", prefix: store.HashFieldScanPrefix(userKey)}, + {name: "hash deltas", prefix: store.HashMetaDeltaScanPrefix(userKey)}, + {name: "set members", prefix: store.SetMemberScanPrefix(userKey)}, + {name: "set deltas", prefix: store.SetMetaDeltaScanPrefix(userKey)}, + {name: "zset members", prefix: store.ZSetMemberScanPrefix(userKey)}, + {name: "zset scores", prefix: store.ZSetScoreScanPrefix(userKey)}, + {name: "zset deltas", prefix: store.ZSetMetaDeltaScanPrefix(userKey)}, + } { + t.Run(tc.name, func(t *testing.T) { + routes, clamp := st.routesForScan(tc.prefix, prefixScanEnd(tc.prefix)) + require.False(t, clamp) + require.Len(t, routes, 2) + require.Equal(t, uint64(2), routes[0].GroupID) + require.Equal(t, uint64(1), routes[1].GroupID) + }) + } +} + +func TestShardStoreScanAtRoutesWideColumnPrefixesByUserKey(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) + userKey := []byte("z-user") + + for _, tc := range []struct { + name string + key []byte + prefix []byte + }{ + {name: "hash field", key: store.HashFieldKey(userKey, []byte("field")), prefix: store.HashFieldScanPrefix(userKey)}, + {name: "hash delta", key: store.HashMetaDeltaKey(userKey, 10, 1), prefix: store.HashMetaDeltaScanPrefix(userKey)}, + {name: "set member", key: store.SetMemberKey(userKey, []byte("member")), prefix: store.SetMemberScanPrefix(userKey)}, + {name: "set delta", key: store.SetMetaDeltaKey(userKey, 11, 1), prefix: store.SetMetaDeltaScanPrefix(userKey)}, + {name: "zset member", key: store.ZSetMemberKey(userKey, []byte("member")), prefix: store.ZSetMemberScanPrefix(userKey)}, + {name: "zset score", key: store.ZSetScoreKey(userKey, 1.5, []byte("member")), prefix: store.ZSetScoreScanPrefix(userKey)}, + {name: "zset delta", key: store.ZSetMetaDeltaKey(userKey, 12, 1), prefix: store.ZSetMetaDeltaScanPrefix(userKey)}, + } { + t.Run(tc.name, func(t *testing.T) { + require.NoError(t, st.PutAt(ctx, tc.key, []byte("value"), 20, 0)) + kvs, err := st.ScanAt(ctx, tc.prefix, prefixScanEnd(tc.prefix), 10, 20) + require.NoError(t, err) + require.Equal(t, []*store.KVPair{{Key: tc.key, Value: []byte("value")}}, kvs) + _, err = groups[1].Store.GetAt(ctx, tc.key, 20) + require.ErrorIs(t, err, store.ErrKeyNotFound) + }) + } +} + +func TestShardStoreReadFenceFailsClosedWhileCatalogVersionIsBehind(t *testing.T) { + t.Parallel() + + engine := distribution.NewEngine() + require.NoError(t, engine.ApplySnapshot(distribution.CatalogSnapshot{ + Version: 1, + Routes: []distribution.RouteDescriptor{ + {RouteID: 1, Start: []byte(""), GroupID: 1, State: distribution.RouteStateActive}, + }, + })) + groupStore := store.NewMVCCStore() + require.NoError(t, groupStore.PutAt(context.Background(), []byte("k"), []byte("stale"), 1, 0)) + st := NewShardStore(engine, map[uint64]*ShardGroup{1: {Store: groupStore}}) + + tests := []struct { + name string + read func(context.Context) error + }{ + { + name: "point read", + read: func(ctx context.Context) error { + _, err := st.GetAtWithReadFence(ctx, []byte("k"), 1, 0, 2) + return err + }, + }, + { + name: "latest commit timestamp", + read: func(ctx context.Context) error { + _, _, err := st.LatestCommitTSWithReadFence(ctx, []byte("k"), 2) + return err + }, + }, + { + name: "range scan", + read: func(ctx context.Context) error { + _, err := st.ScanAtWithReadFence(ctx, []byte("a"), []byte("z"), 1, 1, false, 0, 2, nil, nil) + return err + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond) + defer cancel() + err := tc.read(ctx) + require.ErrorIs(t, err, ErrReadRouteVersionUnavailable) + }) + } +} + +func TestShardStoreReadFenceWaitsForCatalogAndReroutesPointRead(t *testing.T) { + t.Parallel() + + engine := distribution.NewEngine() + require.NoError(t, engine.ApplySnapshot(distribution.CatalogSnapshot{ + Version: 1, + Routes: []distribution.RouteDescriptor{ + {RouteID: 1, Start: []byte(""), GroupID: 1, State: distribution.RouteStateActive}, + }, + })) + groups := map[uint64]*ShardGroup{ + 1: {Store: store.NewMVCCStore()}, + 2: {Store: store.NewMVCCStore()}, + } + require.NoError(t, groups[1].Store.PutAt(context.Background(), []byte("k"), []byte("old-owner"), 1, 0)) + require.NoError(t, groups[2].Store.PutAt(context.Background(), []byte("k"), []byte("new-owner"), 1, 0)) + st := NewShardStore(engine, groups) + + applyErr := make(chan error, 1) + go func() { + time.Sleep(20 * time.Millisecond) + applyErr <- engine.ApplySnapshot(distribution.CatalogSnapshot{ + Version: 2, + Routes: []distribution.RouteDescriptor{ + {RouteID: 1, Start: []byte(""), GroupID: 2, State: distribution.RouteStateActive}, + }, + }) + }() + + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + value, err := st.GetAtWithReadFence(ctx, []byte("k"), 1, 0, 2) + require.NoError(t, err) + require.Equal(t, []byte("new-owner"), value) + require.NoError(t, <-applyErr) +} + +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, st.ReadRouteVersion(), []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, st.ReadRouteVersion(), []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)) + + for _, tc := range []struct { + name string + reverse bool + routeStart []byte + routeEnd []byte + want [][]byte + }{ + { + name: "left interval only", + routeStart: []byte("a"), + routeEnd: []byte("z"), + want: [][]byte{first}, + }, + { + name: "forward across intervals", + routeStart: []byte("a"), + want: [][]byte{first, second}, + }, + { + name: "reverse across intervals", + reverse: true, + routeStart: []byte("a"), + want: [][]byte{second, first}, + }, + } { + t.Run(tc.name, func(t *testing.T) { + kvs, err := st.ScanAtWithReadFence(ctx, rawPrefix, prefixScanEnd(rawPrefix), 10, 2, tc.reverse, 0, st.ReadRouteVersion(), tc.routeStart, tc.routeEnd) + require.NoError(t, err) + require.Len(t, kvs, len(tc.want)) + for i, want := range tc.want { + require.Equal(t, want, kvs[i].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, st.ReadRouteVersion(), []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, st.ReadRouteVersion(), []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, st.ReadRouteVersion(), []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, st.ReadRouteVersion(), []byte{}, []byte("m")) + require.NoError(t, err) + require.Len(t, kvs, 1) + require.Equal(t, left, kvs[0].Key) +} + +func TestShardStoreScanAtWithReadFence_FiltersRedisAuxiliaryBoundsByRouteKey(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()}, + } + t.Cleanup(func() { _ = groups[1].Store.Close() }) + st := NewShardStore(engine, groups) + + for _, tc := range []struct { + name string + prefix []byte + left []byte + right []byte + }{ + { + name: "list delta", + prefix: []byte(store.ListMetaDeltaPrefix), + left: store.ListMetaDeltaKey([]byte("alpha"), 10, 0), + right: store.ListMetaDeltaKey([]byte("zulu"), 11, 0), + }, + { + name: "list claim", + prefix: []byte(store.ListClaimPrefix), + left: store.ListClaimKey([]byte("alpha"), 1), + right: store.ListClaimKey([]byte("zulu"), 1), + }, + { + name: "stream meta", + prefix: []byte(store.StreamMetaPrefix), + left: store.StreamMetaKey([]byte("alpha")), + right: store.StreamMetaKey([]byte("zulu")), + }, + { + name: "stream entry", + prefix: []byte(store.StreamEntryPrefix), + left: store.StreamEntryKey([]byte("alpha"), 1, 0), + right: store.StreamEntryKey([]byte("zulu"), 1, 0), + }, + } { + t.Run(tc.name, func(t *testing.T) { + require.NoError(t, groups[1].Store.PutAt(ctx, tc.left, []byte("left"), 1, 0)) + require.NoError(t, groups[1].Store.PutAt(ctx, tc.right, []byte("right"), 2, 0)) + + kvs, err := st.ScanAtWithReadFence(ctx, tc.prefix, prefixScanEnd(tc.prefix), 1, 2, false, 0, st.ReadRouteVersion(), []byte("m"), nil) + require.NoError(t, err) + require.Len(t, kvs, 1) + require.Equal(t, tc.right, 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, st.ReadRouteVersion(), []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, st.ReadRouteVersion(), nil, nil) + require.ErrorIs(t, err, store.ErrNotSupported) + + kvs, err := st.ScanAtWithReadFence(ctx, rawPrefix, prefixScanEnd(rawPrefix), -1, 2, true, 1, st.ReadRouteVersion(), []byte("m"), nil) + require.NoError(t, err) + require.Empty(t, kvs) + + kvs, err = st.ScanAtWithReadFence(ctx, rawPrefix, prefixScanEnd(rawPrefix), 1, 2, true, 1, st.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 TestShardStoreScanAt_IncludesS3ManifestKeysAcrossShards(t *testing.T) { t.Parallel() @@ -437,7 +1119,7 @@ func TestShardStoreScanKeysRouteAtLeaderRefillsAfterTxnInternalKeys(t *testing.T require.NoError(t, g.Store.PutAt(ctx, txnCommitKey([]byte("primary"), 10), []byte("commit"), 1, 0)) require.NoError(t, g.Store.PutAt(ctx, []byte("a"), []byte("va"), 2, 0)) - keys, err := st.scanKeysRouteAtLeader(ctx, g, []byte(""), nil, 1, ^uint64(0)) + keys, err := st.scanKeysRouteAtLeader(ctx, g, []byte(""), nil, 1, ^uint64(0), 0) require.NoError(t, err) require.Equal(t, [][]byte{[]byte("a")}, keys) } @@ -452,7 +1134,7 @@ func TestShardStoreScanKeysRouteAtLeaderPreservesEmptyKey(t *testing.T) { require.NoError(t, g.Store.PutAt(ctx, []byte(""), []byte("empty"), 1, 0)) require.NoError(t, g.Store.PutAt(ctx, []byte("a"), []byte("va"), 2, 0)) - keys, err := st.scanKeysRouteAtLeader(ctx, g, nil, nil, 2, ^uint64(0)) + keys, err := st.scanKeysRouteAtLeader(ctx, g, nil, nil, 2, ^uint64(0), 0) require.NoError(t, err) require.Equal(t, [][]byte{[]byte(""), []byte("a")}, keys) } @@ -541,7 +1223,7 @@ func TestShardStoreProxyForwardPageAdvancesFromRawPage(t *testing.T) { } st := NewShardStore(distribution.NewEngine(), map[uint64]*ShardGroup{42: g}) - page, err := st.scanRouteAtForwardPage(ctx, distribution.Route{GroupID: 42}, g, []byte(""), nil, 2, ^uint64(0)) + page, err := st.scanRouteAtForwardPage(ctx, distribution.Route{GroupID: 42}, g, []byte(""), nil, 2, ^uint64(0), 0, nil, nil) require.NoError(t, err) require.True(t, page.full) require.Equal(t, internalKey, page.advanceKey) @@ -892,6 +1574,299 @@ func TestShardStoreScanAt_RoutesS3ManifestScansByLogicalObjectKey(t *testing.T) require.Equal(t, k1, kvs[1].Key) } +func TestShardStoreScanAt_RoutesRedisWideColumnPrefixAcrossShards(t *testing.T) { + t.Parallel() + + ctx := context.Background() + engine := distribution.NewEngine() + engine.UpdateRoute([]byte(""), []byte("am"), 1) + engine.UpdateRoute([]byte("am"), nil, 2) + groups := map[uint64]*ShardGroup{ + 1: {Store: store.NewMVCCStore()}, + 2: {Store: store.NewMVCCStore()}, + } + t.Cleanup(func() { + _ = groups[1].Store.Close() + _ = groups[2].Store.Close() + }) + st := NewShardStore(engine, groups) + + left := store.HashFieldKey([]byte("alice"), []byte("field")) + right := store.HashFieldKey([]byte("amy"), []byte("field")) + require.NoError(t, st.PutAt(ctx, left, []byte("left"), 1, 0)) + require.NoError(t, st.PutAt(ctx, right, []byte("right"), 2, 0)) + + start := store.HashFieldScanPrefix([]byte("a")) + end := prefixScanEnd([]byte(store.HashFieldPrefix)) + kvs, err := st.ScanAt(ctx, start, end, 10, ^uint64(0)) + require.NoError(t, err) + require.Len(t, kvs, 2) + require.ElementsMatch(t, [][]byte{left, right}, [][]byte{kvs[0].Key, kvs[1].Key}) +} + +func TestShardStoreScanAt_RoutesBareRedisWideColumnFamilyAcrossShards(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()}, + } + t.Cleanup(func() { + _ = groups[1].Store.Close() + _ = groups[2].Store.Close() + }) + st := NewShardStore(engine, groups) + + left := store.HashFieldKey([]byte("anna"), []byte("field")) + right := store.HashFieldKey([]byte("zoey"), []byte("field")) + require.NoError(t, st.PutAt(ctx, left, []byte("left"), 1, 0)) + require.NoError(t, st.PutAt(ctx, right, []byte("right"), 2, 0)) + + prefix := []byte(store.HashFieldPrefix) + kvs, err := st.ScanAt(ctx, prefix, prefixScanEnd(prefix), 10, ^uint64(0)) + require.NoError(t, err) + require.Len(t, kvs, 2) + require.Equal(t, [][]byte{left, right}, [][]byte{kvs[0].Key, kvs[1].Key}) +} + +func TestShardStoreScanAt_RoutesRedisWideColumnCursorAcrossRemainingShards(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()}, + } + t.Cleanup(func() { + _ = groups[1].Store.Close() + _ = groups[2].Store.Close() + }) + st := NewShardStore(engine, groups) + + left := store.HashFieldKey([]byte("anna"), []byte("field")) + right := store.HashFieldKey([]byte("zoey"), []byte("field")) + require.NoError(t, st.PutAt(ctx, left, []byte("left"), 1, 0)) + require.NoError(t, st.PutAt(ctx, right, []byte("right"), 2, 0)) + + prefix := []byte(store.HashFieldPrefix) + kvs, err := st.ScanAt(ctx, nextScanCursor(left), prefixScanEnd(prefix), 10, ^uint64(0)) + require.NoError(t, err) + require.Len(t, kvs, 1) + require.Equal(t, [][]byte{right}, [][]byte{kvs[0].Key}) +} + +func TestShardStoreScanAt_RoutesExactRedisWideColumnScanToOneShard(t *testing.T) { + t.Parallel() + + engine := distribution.NewEngine() + engine.UpdateRoute([]byte(""), []byte("am"), 1) + engine.UpdateRoute([]byte("am"), nil, 2) + groups := map[uint64]*ShardGroup{ + 1: {Store: store.NewMVCCStore()}, + 2: {Store: store.NewMVCCStore()}, + } + t.Cleanup(func() { + _ = groups[1].Store.Close() + _ = groups[2].Store.Close() + }) + st := NewShardStore(engine, groups) + + start := store.HashFieldScanPrefix([]byte("alice")) + routes, clamp, _ := st.routesForScanWithVersion(start, prefixScanEnd(start)) + require.False(t, clamp) + require.Len(t, routes, 1) + require.Equal(t, uint64(1), routes[0].GroupID) +} + +func TestShardStoreRoutesForWideColumnBoundedPatternIncludesLegacyRawRoute(t *testing.T) { + t.Parallel() + + 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()}, + } + t.Cleanup(func() { + _ = groups[1].Store.Close() + _ = groups[2].Store.Close() + }) + st := NewShardStore(engine, groups) + + start := store.HashFieldScanPrefix([]byte("m")) + routes, clamp, _ := st.routesForScanWithVersion(start, prefixScanEnd([]byte(store.HashFieldPrefix))) + require.False(t, clamp) + require.Len(t, routes, 2) + require.Equal(t, uint64(2), routes[0].GroupID) + require.Equal(t, uint64(1), routes[1].GroupID) +} + +func TestShardStoreRedisWideColumnReadsLegacyRawRoute(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()}, + } + t.Cleanup(func() { + _ = groups[1].Store.Close() + _ = groups[2].Store.Close() + }) + st := NewShardStore(engine, groups) + + key := store.HashFieldKey([]byte("zulu"), []byte("field")) + require.NoError(t, groups[1].Store.PutAt(ctx, key, []byte("legacy"), 5, 0)) + + value, err := st.GetAt(ctx, key, 5) + require.NoError(t, err) + require.Equal(t, []byte("legacy"), value) + + ts, exists, err := st.LatestCommitTS(ctx, key) + require.NoError(t, err) + require.True(t, exists) + require.Equal(t, uint64(5), ts) + + prefix := store.HashFieldScanPrefix([]byte("zulu")) + kvs, err := st.ScanAt(ctx, prefix, prefixScanEnd(prefix), 10, 5) + require.NoError(t, err) + require.Len(t, kvs, 1) + require.Equal(t, []byte("legacy"), kvs[0].Value) + + require.NoError(t, st.PutAt(ctx, key, []byte("current"), 6, 0)) + value, err = st.GetAt(ctx, key, 6) + require.NoError(t, err) + require.Equal(t, []byte("current"), value) + + ts, exists, err = st.LatestCommitTS(ctx, key) + require.NoError(t, err) + require.True(t, exists) + require.Equal(t, uint64(6), ts) + + kvs, err = st.ScanAt(ctx, prefix, prefixScanEnd(prefix), 10, 6) + require.NoError(t, err) + require.Len(t, kvs, 1) + require.Equal(t, []byte("current"), kvs[0].Value) + + kvs, err = st.ReverseScanAt(ctx, prefix, prefixScanEnd(prefix), 10, 6) + require.NoError(t, err) + require.Len(t, kvs, 1) + require.Equal(t, []byte("current"), kvs[0].Value) + + require.NoError(t, st.DeleteAt(ctx, key, 7)) + _, err = st.GetAt(ctx, key, 7) + require.ErrorIs(t, err, store.ErrKeyNotFound) + + kvs, err = st.ScanAt(ctx, prefix, prefixScanEnd(prefix), 10, 7) + require.NoError(t, err) + require.Empty(t, kvs) + + kvs, err = st.ReverseScanAt(ctx, prefix, prefixScanEnd(prefix), 10, 7) + require.NoError(t, err) + require.Empty(t, kvs) + + require.NoError(t, st.PutAt(ctx, key, []byte("future"), 9, 0)) + _, err = st.GetAt(ctx, key, 8) + require.ErrorIs(t, err, store.ErrKeyNotFound) + + kvs, err = st.ScanAt(ctx, prefix, prefixScanEnd(prefix), 10, 8) + require.NoError(t, err) + require.Empty(t, kvs) + + value, err = st.GetAt(ctx, key, 9) + require.NoError(t, err) + require.Equal(t, []byte("future"), value) +} + +func TestShardStoreRedisWideColumnScanRefillsAfterLogicalTombstones(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()}, + } + t.Cleanup(func() { + _ = groups[1].Store.Close() + _ = groups[2].Store.Close() + }) + st := NewShardStore(engine, groups) + + userKey := []byte("zulu") + a := store.HashFieldKey(userKey, []byte("a")) + b := store.HashFieldKey(userKey, []byte("b")) + c := store.HashFieldKey(userKey, []byte("c")) + d := store.HashFieldKey(userKey, []byte("d")) + for _, item := range []struct { + key []byte + value []byte + }{ + {key: a, value: []byte("legacy-a")}, + {key: b, value: []byte("legacy-b")}, + {key: c, value: []byte("legacy-c")}, + {key: d, value: []byte("legacy-d")}, + } { + require.NoError(t, groups[1].Store.PutAt(ctx, item.key, item.value, 5, 0)) + } + require.NoError(t, st.DeleteAt(ctx, a, 7)) + require.NoError(t, st.DeleteAt(ctx, b, 7)) + + prefix := store.HashFieldScanPrefix(userKey) + kvs, err := st.ScanAt(ctx, prefix, prefixScanEnd(prefix), 2, 7) + require.NoError(t, err) + require.Len(t, kvs, 2) + require.Equal(t, c, kvs[0].Key) + require.Equal(t, []byte("legacy-c"), kvs[0].Value) + require.Equal(t, d, kvs[1].Key) + require.Equal(t, []byte("legacy-d"), kvs[1].Value) + + keys, err := st.ScanKeysAt(ctx, prefix, prefixScanEnd(prefix), 2, 7) + require.NoError(t, err) + require.Equal(t, [][]byte{c, d}, keys) +} + +func TestShardStoreReverseRedisWideColumnScanPrefersLogicalRoute(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()}, + } + t.Cleanup(func() { + _ = groups[1].Store.Close() + _ = groups[2].Store.Close() + }) + st := NewShardStore(engine, groups) + + key := store.HashFieldKey([]byte("zulu"), []byte("field")) + require.NoError(t, groups[1].Store.PutAt(ctx, key, []byte("legacy"), 5, 0)) + require.NoError(t, st.PutAt(ctx, key, []byte("current"), 6, 0)) + + prefix := store.HashFieldScanPrefix([]byte("zulu")) + kvs, err := st.ReverseScanAt(ctx, prefix, prefixScanEnd(prefix), 10, 6) + require.NoError(t, err) + require.Len(t, kvs, 1) + require.Equal(t, []byte("current"), kvs[0].Value) +} + func TestShardStoreScanAt_RoutesFilesystemChunkScansByChunkRouteKey(t *testing.T) { t.Parallel() @@ -1628,6 +2603,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 71a793afc..6158f60fd 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,173 @@ 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(ctx, []*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_BoundsForeignLockScan(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.ErrorIs(t, err, ErrTxnLocked) +} + +func TestScanTxnLockPagesAtWithRouteFilter_BoundsMatchingLocks(t *testing.T) { + t.Parallel() + + ctx := context.Background() + st := store.NewMVCCStore() + t.Cleanup(func() { require.NoError(t, st.Close()) }) + rawPrefix := []byte("!redis|meta|") + rawEnd := prefixScanEnd(rawPrefix) + for i := uint64(0); i <= lockPageLimit; i++ { + key := []byte(fmt.Sprintf("!redis|meta|z%04d", i)) + lock := encodeTxnLock(txnLock{ + StartTS: 10 + i, + TTLExpireAt: ^uint64(0), + PrimaryKey: key, + }) + require.NoError(t, st.PutAt(ctx, txnLockKey(key), lock, 10+i, 0)) + } + + _, err := scanTxnLockPagesAtWithRouteFilter(ctx, st, txnLockKey(rawPrefix), txnLockKey(rawEnd), ^uint64(0), lockPageLimit, []byte(""), nil) + require.ErrorIs(t, err, ErrTxnLocked) +} + +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(ctx, []*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 TestShardStoreScanKeysAt_ReturnsTxnLockedForPendingLockWithoutCommittedValue(t *testing.T) { t.Parallel() @@ -300,7 +468,7 @@ func TestShardStoreScanKeysAt_ReturnsTxnLockedForPendingLockWithoutCommittedValu key := []byte("k") startTS := uint64(1) - _, err := groups[1].Txn.Commit(context.Background(), []*pb.Request{makePrepareRequest(startTS, key, []byte("v"), key)}) + _, err := groups[1].Txn.Commit(ctx, []*pb.Request{makePrepareRequest(startTS, key, []byte("v"), key)}) require.NoError(t, err) _, err = shardStore.ScanKeysAt(ctx, []byte("k"), []byte("l"), 100, ^uint64(0)) @@ -321,9 +489,9 @@ func TestShardStoreScanKeysAt_ResolvesCommittedSecondaryLockWithoutCommittedValu primaryKey := []byte("b") secondaryKey := []byte("x") - _, err := groups[1].Txn.Commit(context.Background(), []*pb.Request{makePrepareRequest(startTS, primaryKey, []byte("v1"), primaryKey)}) + _, err := groups[1].Txn.Commit(ctx, []*pb.Request{makePrepareRequest(startTS, primaryKey, []byte("v1"), primaryKey)}) require.NoError(t, err) - _, err = groups[2].Txn.Commit(context.Background(), []*pb.Request{makePrepareRequest(startTS, secondaryKey, []byte("v2"), primaryKey)}) + _, err = groups[2].Txn.Commit(ctx, []*pb.Request{makePrepareRequest(startTS, secondaryKey, []byte("v2"), primaryKey)}) require.NoError(t, err) commitPrimary := &pb.Request{ @@ -335,7 +503,7 @@ func TestShardStoreScanKeysAt_ResolvesCommittedSecondaryLockWithoutCommittedValu {Op: pb.Op_PUT, Key: primaryKey}, }, } - _, err = groups[1].Txn.Commit(context.Background(), []*pb.Request{commitPrimary}) + _, err = groups[1].Txn.Commit(ctx, []*pb.Request{commitPrimary}) require.NoError(t, err) keys, err := shardStore.ScanKeysAt(ctx, []byte("x"), []byte("z"), 100, commitTS) diff --git a/proto/distribution.pb.go b/proto/distribution.pb.go index 14a3db51f..19c01fbaa 100644 --- a/proto/distribution.pb.go +++ b/proto/distribution.pb.go @@ -486,16 +486,19 @@ 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"` - SplitAtHlc uint64 `protobuf:"varint,7,opt,name=split_at_hlc,json=splitAtHlc,proto3" json:"split_at_hlc,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"` + SplitAtHlc uint64 `protobuf:"varint,7,opt,name=split_at_hlc,json=splitAtHlc,proto3" json:"split_at_hlc,omitempty"` + StagedVisibilityActive bool `protobuf:"varint,8,opt,name=staged_visibility_active,json=stagedVisibilityActive,proto3" json:"staged_visibility_active,omitempty"` + MigrationJobId uint64 `protobuf:"varint,9,opt,name=migration_job_id,json=migrationJobId,proto3" json:"migration_job_id,omitempty"` + MinWriteTsExclusive uint64 `protobuf:"varint,10,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() { @@ -577,6 +580,27 @@ func (x *RouteDescriptor) GetSplitAtHlc() 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"` @@ -1603,115 +1627,875 @@ func (*CatalogWatchEvent_Snapshot) isCatalogWatchEvent_Payload() {} func (*CatalogWatchEvent_Delta) isCatalogWatchEvent_Payload() {} -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\"\xe5\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\x12 \n" + - "\fsplit_at_hlc\x18\a \x01(\x04R\n" + - "splitAtHlc\"\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\"\x1c\n" + - "\x1aCatalogCapabilitiesRequest\"\xde\x01\n" + - "\x1bCatalogCapabilitiesResponse\x12>\n" + - "\x1bsupported_protocol_versions\x18\x01 \x03(\rR\x19supportedProtocolVersions\x12'\n" + - "\x0fcurrent_version\x18\x02 \x01(\x04R\x0ecurrentVersion\x120\n" + - "\x14oldest_delta_version\x18\x03 \x01(\x04R\x12oldestDeltaVersion\x12$\n" + - "\x0emax_batch_size\x18\x04 \x01(\rR\fmaxBatchSize\"\x8b\x01\n" + - "\x13CatalogWatchRequest\x12)\n" + - "\x10protocol_version\x18\x01 \x01(\rR\x0fprotocolVersion\x12#\n" + - "\rafter_version\x18\x02 \x01(\x04R\fafterVersion\x12$\n" + - "\x0emax_batch_size\x18\x03 \x01(\rR\fmaxBatchSize\"\x82\x01\n" + - "\x14CatalogDeltaMutation\x12'\n" + - "\x02op\x18\x01 \x01(\x0e2\x17.CatalogDeltaMutationOpR\x02op\x12\x19\n" + - "\broute_id\x18\x02 \x01(\x04R\arouteId\x12&\n" + - "\x05route\x18\x03 \x01(\v2\x10.RouteDescriptorR\x05route\"\x8e\x01\n" + - "\x12CatalogDeltaRecord\x12)\n" + - "\x10previous_version\x18\x01 \x01(\x04R\x0fpreviousVersion\x12\x18\n" + - "\aversion\x18\x02 \x01(\x04R\aversion\x123\n" + - "\tmutations\x18\x03 \x03(\v2\x15.CatalogDeltaMutationR\tmutations\"Z\n" + - "\x14CatalogSnapshotReset\x12\x18\n" + - "\aversion\x18\x01 \x01(\x04R\aversion\x12(\n" + - "\x06routes\x18\x02 \x03(\v2\x10.RouteDescriptorR\x06routes\"\x80\x01\n" + - "\x11CatalogWatchEvent\x123\n" + - "\bsnapshot\x18\x01 \x01(\v2\x15.CatalogSnapshotResetH\x00R\bsnapshot\x12+\n" + - "\x05delta\x18\x02 \x01(\v2\x13.CatalogDeltaRecordH\x00R\x05deltaB\t\n" + - "\apayload*\xa3\x01\n" + +func (x *StartSplitMigrationRequest) Reset() { + *x = StartSplitMigrationRequest{} + mi := &file_distribution_proto_msgTypes[18] + 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[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 StartSplitMigrationRequest.ProtoReflect.Descriptor instead. +func (*StartSplitMigrationRequest) Descriptor() ([]byte, []int) { + return file_distribution_proto_rawDescGZIP(), []int{18} +} + +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[19] + 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[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 StartSplitMigrationResponse.ProtoReflect.Descriptor instead. +func (*StartSplitMigrationResponse) Descriptor() ([]byte, []int) { + return file_distribution_proto_rawDescGZIP(), []int{19} +} + +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[20] + 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[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 GetRouteOwnershipRequest.ProtoReflect.Descriptor instead. +func (*GetRouteOwnershipRequest) Descriptor() ([]byte, []int) { + return file_distribution_proto_rawDescGZIP(), []int{20} +} + +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[21] + 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[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 GetRouteOwnershipResponse.ProtoReflect.Descriptor instead. +func (*GetRouteOwnershipResponse) Descriptor() ([]byte, []int) { + return file_distribution_proto_rawDescGZIP(), []int{21} +} + +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[22] + 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[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 GetIntersectingRoutesRequest.ProtoReflect.Descriptor instead. +func (*GetIntersectingRoutesRequest) Descriptor() ([]byte, []int) { + return file_distribution_proto_rawDescGZIP(), []int{22} +} + +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[23] + 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[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 GetIntersectingRoutesResponse.ProtoReflect.Descriptor instead. +func (*GetIntersectingRoutesResponse) Descriptor() ([]byte, []int) { + return file_distribution_proto_rawDescGZIP(), []int{23} +} + +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[24] + 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[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 GetSplitJobRequest.ProtoReflect.Descriptor instead. +func (*GetSplitJobRequest) Descriptor() ([]byte, []int) { + return file_distribution_proto_rawDescGZIP(), []int{24} +} + +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[25] + 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[25] + 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{25} +} + +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[26] + 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[26] + 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{26} +} + +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[27] + 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[27] + 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{27} +} + +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[28] + 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[28] + 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{28} +} + +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[29] + 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[29] + 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{29} +} + +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[30] + 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[30] + 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{30} +} + +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[31] + 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[31] + 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{31} +} + +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\"\xfe\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\x12 \n" + + "\fsplit_at_hlc\x18\a \x01(\x04R\n" + + "splitAtHlc\x128\n" + + "\x18staged_visibility_active\x18\b \x01(\bR\x16stagedVisibilityActive\x12(\n" + + "\x10migration_job_id\x18\t \x01(\x04R\x0emigrationJobId\x123\n" + + "\x16min_write_ts_exclusive\x18\n" + + " \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\"\x1c\n" + + "\x1aCatalogCapabilitiesRequest\"\xde\x01\n" + + "\x1bCatalogCapabilitiesResponse\x12>\n" + + "\x1bsupported_protocol_versions\x18\x01 \x03(\rR\x19supportedProtocolVersions\x12'\n" + + "\x0fcurrent_version\x18\x02 \x01(\x04R\x0ecurrentVersion\x120\n" + + "\x14oldest_delta_version\x18\x03 \x01(\x04R\x12oldestDeltaVersion\x12$\n" + + "\x0emax_batch_size\x18\x04 \x01(\rR\fmaxBatchSize\"\x8b\x01\n" + + "\x13CatalogWatchRequest\x12)\n" + + "\x10protocol_version\x18\x01 \x01(\rR\x0fprotocolVersion\x12#\n" + + "\rafter_version\x18\x02 \x01(\x04R\fafterVersion\x12$\n" + + "\x0emax_batch_size\x18\x03 \x01(\rR\fmaxBatchSize\"\x82\x01\n" + + "\x14CatalogDeltaMutation\x12'\n" + + "\x02op\x18\x01 \x01(\x0e2\x17.CatalogDeltaMutationOpR\x02op\x12\x19\n" + + "\broute_id\x18\x02 \x01(\x04R\arouteId\x12&\n" + + "\x05route\x18\x03 \x01(\v2\x10.RouteDescriptorR\x05route\"\x8e\x01\n" + + "\x12CatalogDeltaRecord\x12)\n" + + "\x10previous_version\x18\x01 \x01(\x04R\x0fpreviousVersion\x12\x18\n" + + "\aversion\x18\x02 \x01(\x04R\aversion\x123\n" + + "\tmutations\x18\x03 \x03(\v2\x15.CatalogDeltaMutationR\tmutations\"Z\n" + + "\x14CatalogSnapshotReset\x12\x18\n" + + "\aversion\x18\x01 \x01(\x04R\aversion\x12(\n" + + "\x06routes\x18\x02 \x03(\v2\x10.RouteDescriptorR\x06routes\"\x80\x01\n" + + "\x11CatalogWatchEvent\x123\n" + + "\bsnapshot\x18\x01 \x01(\v2\x15.CatalogSnapshotResetH\x00R\bsnapshot\x12+\n" + + "\x05delta\x18\x02 \x01(\v2\x13.CatalogDeltaRecordH\x00R\x05deltaB\t\n" + + "\apayload\"\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" + @@ -1744,7 +2528,7 @@ const file_distribution_proto_rawDesc = "" + "\x16CatalogDeltaMutationOp\x12)\n" + "%CATALOG_DELTA_MUTATION_OP_UNSPECIFIED\x10\x00\x12$\n" + " CATALOG_DELTA_MUTATION_OP_UPSERT\x10\x01\x12$\n" + - " CATALOG_DELTA_MUTATION_OP_DELETE\x10\x022\x87\x03\n" + + " CATALOG_DELTA_MUTATION_OP_DELETE\x10\x022\x8b\a\n" + "\fDistribution\x121\n" + "\bGetRoute\x12\x10.GetRouteRequest\x1a\x11.GetRouteResponse\"\x00\x12=\n" + "\fGetTimestamp\x12\x14.GetTimestampRequest\x1a\x15.GetTimestampResponse\"\x00\x127\n" + @@ -1753,7 +2537,14 @@ const file_distribution_proto_rawDesc = "" + "\n" + "SplitRange\x12\x12.SplitRangeRequest\x1a\x13.SplitRangeResponse\"\x00\x12U\n" + "\x16GetCatalogCapabilities\x12\x1b.CatalogCapabilitiesRequest\x1a\x1c.CatalogCapabilitiesResponse\"\x00\x12<\n" + - "\fWatchCatalog\x12\x14.CatalogWatchRequest\x1a\x12.CatalogWatchEvent\"\x000\x01B#Z!github.com/bootjp/elastickv/protob\x06proto3" + "\fWatchCatalog\x12\x14.CatalogWatchRequest\x1a\x12.CatalogWatchEvent\"\x000\x01\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 @@ -1768,31 +2559,46 @@ func file_distribution_proto_rawDescGZIP() []byte { } var file_distribution_proto_enumTypes = make([]protoimpl.EnumInfo, 5) -var file_distribution_proto_msgTypes = make([]protoimpl.MessageInfo, 18) +var file_distribution_proto_msgTypes = make([]protoimpl.MessageInfo, 33) var file_distribution_proto_goTypes = []any{ - (RouteState)(0), // 0: RouteState - (SplitJobPhase)(0), // 1: SplitJobPhase - (SplitJobBarrierState)(0), // 2: SplitJobBarrierState - (SplitJobExportPhase)(0), // 3: SplitJobExportPhase - (CatalogDeltaMutationOp)(0), // 4: CatalogDeltaMutationOp - (*GetRouteRequest)(nil), // 5: GetRouteRequest - (*GetRouteResponse)(nil), // 6: GetRouteResponse - (*GetTimestampRequest)(nil), // 7: GetTimestampRequest - (*GetTimestampResponse)(nil), // 8: GetTimestampResponse - (*RouteDescriptor)(nil), // 9: RouteDescriptor - (*SplitJobBracketProgress)(nil), // 10: SplitJobBracketProgress - (*SplitJob)(nil), // 11: SplitJob - (*ListRoutesRequest)(nil), // 12: ListRoutesRequest - (*ListRoutesResponse)(nil), // 13: ListRoutesResponse - (*SplitRangeRequest)(nil), // 14: SplitRangeRequest - (*SplitRangeResponse)(nil), // 15: SplitRangeResponse - (*CatalogCapabilitiesRequest)(nil), // 16: CatalogCapabilitiesRequest - (*CatalogCapabilitiesResponse)(nil), // 17: CatalogCapabilitiesResponse - (*CatalogWatchRequest)(nil), // 18: CatalogWatchRequest - (*CatalogDeltaMutation)(nil), // 19: CatalogDeltaMutation - (*CatalogDeltaRecord)(nil), // 20: CatalogDeltaRecord - (*CatalogSnapshotReset)(nil), // 21: CatalogSnapshotReset - (*CatalogWatchEvent)(nil), // 22: CatalogWatchEvent + (RouteState)(0), // 0: RouteState + (SplitJobPhase)(0), // 1: SplitJobPhase + (SplitJobBarrierState)(0), // 2: SplitJobBarrierState + (SplitJobExportPhase)(0), // 3: SplitJobExportPhase + (CatalogDeltaMutationOp)(0), // 4: CatalogDeltaMutationOp + (*GetRouteRequest)(nil), // 5: GetRouteRequest + (*GetRouteResponse)(nil), // 6: GetRouteResponse + (*GetTimestampRequest)(nil), // 7: GetTimestampRequest + (*GetTimestampResponse)(nil), // 8: GetTimestampResponse + (*RouteDescriptor)(nil), // 9: RouteDescriptor + (*SplitJobBracketProgress)(nil), // 10: SplitJobBracketProgress + (*SplitJob)(nil), // 11: SplitJob + (*ListRoutesRequest)(nil), // 12: ListRoutesRequest + (*ListRoutesResponse)(nil), // 13: ListRoutesResponse + (*SplitRangeRequest)(nil), // 14: SplitRangeRequest + (*SplitRangeResponse)(nil), // 15: SplitRangeResponse + (*CatalogCapabilitiesRequest)(nil), // 16: CatalogCapabilitiesRequest + (*CatalogCapabilitiesResponse)(nil), // 17: CatalogCapabilitiesResponse + (*CatalogWatchRequest)(nil), // 18: CatalogWatchRequest + (*CatalogDeltaMutation)(nil), // 19: CatalogDeltaMutation + (*CatalogDeltaRecord)(nil), // 20: CatalogDeltaRecord + (*CatalogSnapshotReset)(nil), // 21: CatalogSnapshotReset + (*CatalogWatchEvent)(nil), // 22: CatalogWatchEvent + (*StartSplitMigrationRequest)(nil), // 23: StartSplitMigrationRequest + (*StartSplitMigrationResponse)(nil), // 24: StartSplitMigrationResponse + (*GetRouteOwnershipRequest)(nil), // 25: GetRouteOwnershipRequest + (*GetRouteOwnershipResponse)(nil), // 26: GetRouteOwnershipResponse + (*GetIntersectingRoutesRequest)(nil), // 27: GetIntersectingRoutesRequest + (*GetIntersectingRoutesResponse)(nil), // 28: GetIntersectingRoutesResponse + (*GetSplitJobRequest)(nil), // 29: GetSplitJobRequest + (*GetSplitJobResponse)(nil), // 30: GetSplitJobResponse + (*ListSplitJobsRequest)(nil), // 31: ListSplitJobsRequest + (*ListSplitJobsResponse)(nil), // 32: ListSplitJobsResponse + (*AbandonSplitJobRequest)(nil), // 33: AbandonSplitJobRequest + (*AbandonSplitJobResponse)(nil), // 34: AbandonSplitJobResponse + (*RetrySplitJobRequest)(nil), // 35: RetrySplitJobRequest + (*RetrySplitJobResponse)(nil), // 36: RetrySplitJobResponse + nil, // 37: StartSplitMigrationRequest.OptionsEntry } var file_distribution_proto_depIdxs = []int32{ 0, // 0: RouteDescriptor.state:type_name -> RouteState @@ -1812,23 +2618,42 @@ var file_distribution_proto_depIdxs = []int32{ 9, // 14: CatalogSnapshotReset.routes:type_name -> RouteDescriptor 21, // 15: CatalogWatchEvent.snapshot:type_name -> CatalogSnapshotReset 20, // 16: CatalogWatchEvent.delta:type_name -> CatalogDeltaRecord - 5, // 17: Distribution.GetRoute:input_type -> GetRouteRequest - 7, // 18: Distribution.GetTimestamp:input_type -> GetTimestampRequest - 12, // 19: Distribution.ListRoutes:input_type -> ListRoutesRequest - 14, // 20: Distribution.SplitRange:input_type -> SplitRangeRequest - 16, // 21: Distribution.GetCatalogCapabilities:input_type -> CatalogCapabilitiesRequest - 18, // 22: Distribution.WatchCatalog:input_type -> CatalogWatchRequest - 6, // 23: Distribution.GetRoute:output_type -> GetRouteResponse - 8, // 24: Distribution.GetTimestamp:output_type -> GetTimestampResponse - 13, // 25: Distribution.ListRoutes:output_type -> ListRoutesResponse - 15, // 26: Distribution.SplitRange:output_type -> SplitRangeResponse - 17, // 27: Distribution.GetCatalogCapabilities:output_type -> CatalogCapabilitiesResponse - 22, // 28: Distribution.WatchCatalog:output_type -> CatalogWatchEvent - 23, // [23:29] is the sub-list for method output_type - 17, // [17:23] is the sub-list for method input_type - 17, // [17:17] is the sub-list for extension type_name - 17, // [17:17] is the sub-list for extension extendee - 0, // [0:17] is the sub-list for field type_name + 37, // 17: StartSplitMigrationRequest.options:type_name -> StartSplitMigrationRequest.OptionsEntry + 9, // 18: GetRouteOwnershipResponse.route:type_name -> RouteDescriptor + 9, // 19: GetIntersectingRoutesResponse.routes:type_name -> RouteDescriptor + 11, // 20: GetSplitJobResponse.job:type_name -> SplitJob + 11, // 21: ListSplitJobsResponse.jobs:type_name -> SplitJob + 5, // 22: Distribution.GetRoute:input_type -> GetRouteRequest + 7, // 23: Distribution.GetTimestamp:input_type -> GetTimestampRequest + 12, // 24: Distribution.ListRoutes:input_type -> ListRoutesRequest + 14, // 25: Distribution.SplitRange:input_type -> SplitRangeRequest + 16, // 26: Distribution.GetCatalogCapabilities:input_type -> CatalogCapabilitiesRequest + 18, // 27: Distribution.WatchCatalog:input_type -> CatalogWatchRequest + 23, // 28: Distribution.StartSplitMigration:input_type -> StartSplitMigrationRequest + 25, // 29: Distribution.GetRouteOwnership:input_type -> GetRouteOwnershipRequest + 27, // 30: Distribution.GetIntersectingRoutes:input_type -> GetIntersectingRoutesRequest + 29, // 31: Distribution.GetSplitJob:input_type -> GetSplitJobRequest + 31, // 32: Distribution.ListSplitJobs:input_type -> ListSplitJobsRequest + 33, // 33: Distribution.AbandonSplitJob:input_type -> AbandonSplitJobRequest + 35, // 34: Distribution.RetrySplitJob:input_type -> RetrySplitJobRequest + 6, // 35: Distribution.GetRoute:output_type -> GetRouteResponse + 8, // 36: Distribution.GetTimestamp:output_type -> GetTimestampResponse + 13, // 37: Distribution.ListRoutes:output_type -> ListRoutesResponse + 15, // 38: Distribution.SplitRange:output_type -> SplitRangeResponse + 17, // 39: Distribution.GetCatalogCapabilities:output_type -> CatalogCapabilitiesResponse + 22, // 40: Distribution.WatchCatalog:output_type -> CatalogWatchEvent + 24, // 41: Distribution.StartSplitMigration:output_type -> StartSplitMigrationResponse + 26, // 42: Distribution.GetRouteOwnership:output_type -> GetRouteOwnershipResponse + 28, // 43: Distribution.GetIntersectingRoutes:output_type -> GetIntersectingRoutesResponse + 30, // 44: Distribution.GetSplitJob:output_type -> GetSplitJobResponse + 32, // 45: Distribution.ListSplitJobs:output_type -> ListSplitJobsResponse + 34, // 46: Distribution.AbandonSplitJob:output_type -> AbandonSplitJobResponse + 36, // 47: Distribution.RetrySplitJob:output_type -> RetrySplitJobResponse + 35, // [35:48] is the sub-list for method output_type + 22, // [22:35] is the sub-list for method input_type + 22, // [22:22] is the sub-list for extension type_name + 22, // [22:22] is the sub-list for extension extendee + 0, // [0:22] is the sub-list for field type_name } func init() { file_distribution_proto_init() } @@ -1846,7 +2671,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: 5, - NumMessages: 18, + NumMessages: 33, NumExtensions: 0, NumServices: 1, }, diff --git a/proto/distribution.proto b/proto/distribution.proto index a71a64565..bd87c0180 100644 --- a/proto/distribution.proto +++ b/proto/distribution.proto @@ -9,6 +9,13 @@ service Distribution { rpc SplitRange (SplitRangeRequest) returns (SplitRangeResponse) {} rpc GetCatalogCapabilities (CatalogCapabilitiesRequest) returns (CatalogCapabilitiesResponse) {} rpc WatchCatalog (CatalogWatchRequest) returns (stream CatalogWatchEvent) {} + 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 { @@ -45,6 +52,9 @@ message RouteDescriptor { RouteState state = 5; uint64 parent_route_id = 6; uint64 split_at_hlc = 7; + bool staged_visibility_active = 8; + uint64 migration_job_id = 9; + uint64 min_write_ts_exclusive = 10; } enum SplitJobPhase { @@ -184,3 +194,69 @@ message CatalogWatchEvent { CatalogDeltaRecord delta = 2; } } + +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 610f7d6f2..e490cff71 100644 --- a/proto/distribution_grpc.pb.go +++ b/proto/distribution_grpc.pb.go @@ -25,6 +25,13 @@ const ( Distribution_SplitRange_FullMethodName = "/Distribution/SplitRange" Distribution_GetCatalogCapabilities_FullMethodName = "/Distribution/GetCatalogCapabilities" Distribution_WatchCatalog_FullMethodName = "/Distribution/WatchCatalog" + 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. @@ -37,6 +44,13 @@ type DistributionClient interface { SplitRange(ctx context.Context, in *SplitRangeRequest, opts ...grpc.CallOption) (*SplitRangeResponse, error) GetCatalogCapabilities(ctx context.Context, in *CatalogCapabilitiesRequest, opts ...grpc.CallOption) (*CatalogCapabilitiesResponse, error) WatchCatalog(ctx context.Context, in *CatalogWatchRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[CatalogWatchEvent], 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 { @@ -116,6 +130,76 @@ func (c *distributionClient) WatchCatalog(ctx context.Context, in *CatalogWatchR // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. type Distribution_WatchCatalogClient = grpc.ServerStreamingClient[CatalogWatchEvent] +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. @@ -126,6 +210,13 @@ type DistributionServer interface { SplitRange(context.Context, *SplitRangeRequest) (*SplitRangeResponse, error) GetCatalogCapabilities(context.Context, *CatalogCapabilitiesRequest) (*CatalogCapabilitiesResponse, error) WatchCatalog(*CatalogWatchRequest, grpc.ServerStreamingServer[CatalogWatchEvent]) 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() } @@ -154,6 +245,27 @@ func (UnimplementedDistributionServer) GetCatalogCapabilities(context.Context, * func (UnimplementedDistributionServer) WatchCatalog(*CatalogWatchRequest, grpc.ServerStreamingServer[CatalogWatchEvent]) error { return status.Error(codes.Unimplemented, "method WatchCatalog 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() {} @@ -276,6 +388,132 @@ func _Distribution_WatchCatalog_Handler(srv interface{}, stream grpc.ServerStrea // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. type Distribution_WatchCatalogServer = grpc.ServerStreamingServer[CatalogWatchEvent] +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) @@ -303,6 +541,34 @@ var Distribution_ServiceDesc = grpc.ServiceDesc{ MethodName: "GetCatalogCapabilities", Handler: _Distribution_GetCatalogCapabilities_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{ { diff --git a/proto/internal.pb.go b/proto/internal.pb.go index 720eee66a..ef52628ca 100644 --- a/proto/internal.pb.go +++ b/proto/internal.pb.go @@ -550,6 +550,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 = "" + @@ -580,7 +952,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" + @@ -591,10 +998,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 @@ -609,33 +1018,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() } @@ -649,7 +1069,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 fb8f70e55..7af3e5fd7 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. @@ -85,3 +87,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 450c4e5de..388df9b91 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,12 @@ 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 + GroupId uint64 `protobuf:"varint,3,opt,name=group_id,json=groupId,proto3" json:"group_id,omitempty"` // optional explicit Raft group for route-specific probes + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *RawLatestCommitTSRequest) Reset() { @@ -440,6 +450,20 @@ func (x *RawLatestCommitTSRequest) GetKey() []byte { return nil } +func (x *RawLatestCommitTSRequest) GetReadRouteVersion() uint64 { + if x != nil { + return x.ReadRouteVersion + } + return 0 +} + +func (x *RawLatestCommitTSRequest) GetGroupId() uint64 { + if x != nil { + return x.GroupId + } + return 0 +} + type RawLatestCommitTSResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Ts uint64 `protobuf:"varint,1,opt,name=ts,proto3" json:"ts,omitempty"` @@ -493,16 +517,20 @@ 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 - KeysOnly bool `protobuf:"varint,7,opt,name=keys_only,json=keysOnly,proto3" json:"keys_only,omitempty"` // when true, response kv entries omit values - 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 + KeysOnly bool `protobuf:"varint,7,opt,name=keys_only,json=keysOnly,proto3" json:"keys_only,omitempty"` // when true, response kv entries omit values + ReadRouteVersion uint64 `protobuf:"varint,8,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,9,opt,name=route_start,json=routeStart,proto3" json:"route_start,omitempty"` // route-key-normalized inclusive start, when already known + RouteEnd []byte `protobuf:"bytes,10,opt,name=route_end,json=routeEnd,proto3" json:"route_end,omitempty"` // route-key-normalized exclusive end; empty means +infinity + RouteBoundsPresent bool `protobuf:"varint,11,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() { @@ -584,6 +612,34 @@ func (x *RawScanAtRequest) GetKeysOnly() bool { return false } +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"` @@ -2493,11 +2549,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" + @@ -2506,12 +2563,14 @@ 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\"u\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\x12\x19\n" + + "\bgroup_id\x18\x03 \x01(\x04R\agroupId\"C\n" + "\x19RawLatestCommitTSResponse\x12\x0e\n" + "\x02ts\x18\x01 \x01(\x04R\x02ts\x12\x16\n" + - "\x06exists\x18\x02 \x01(\bR\x06exists\"\xc0\x01\n" + + "\x06exists\x18\x02 \x01(\bR\x06exists\"\xde\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" + @@ -2519,7 +2578,13 @@ const file_service_proto_rawDesc = "" + "\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\x12\x1b\n" + - "\tkeys_only\x18\a \x01(\bR\bkeysOnly\"3\n" + + "\tkeys_only\x18\a \x01(\bR\bkeysOnly\x12,\n" + + "\x12read_route_version\x18\b \x01(\x04R\x10readRouteVersion\x12\x1f\n" + + "\vroute_start\x18\t \x01(\fR\n" + + "routeStart\x12\x1b\n" + + "\troute_end\x18\n" + + " \x01(\fR\brouteEnd\x120\n" + + "\x14route_bounds_present\x18\v \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 c1f5a003e..cb91eca39 100644 --- a/proto/service.proto +++ b/proto/service.proto @@ -50,6 +50,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 { @@ -69,6 +70,8 @@ message RawDeleteResponse { message RawLatestCommitTSRequest { bytes key = 1; + uint64 read_route_version = 2; // stamped by server-side routing for migration read fences + uint64 group_id = 3; // optional explicit Raft group for route-specific probes } message RawLatestCommitTSResponse { @@ -84,6 +87,10 @@ message RawScanAtRequest { bool reverse = 5; uint64 group_id = 6; // optional explicit Raft group for non-range-owned keyspaces bool keys_only = 7; // when true, response kv entries omit values + uint64 read_route_version = 8; // stamped by server-side routing for migration read fences + bytes route_start = 9; // route-key-normalized inclusive start, when already known + bytes route_end = 10; // route-key-normalized exclusive end; empty means +infinity + bool route_bounds_present = 11; // true when route_start/route_end were supplied, including ["", +infinity) } message RawKVPair { diff --git a/store/list_helpers.go b/store/list_helpers.go index 0f56f6878..e6c4ed964 100644 --- a/store/list_helpers.go +++ b/store/list_helpers.go @@ -132,27 +132,58 @@ func IsListClaimKey(key []byte) bool { // ExtractListUserKeyFromDelta extracts the logical user key from a list delta key. func ExtractListUserKeyFromDelta(key []byte) []byte { trimmed := bytes.TrimPrefix(key, []byte(ListMetaDeltaPrefix)) - if len(trimmed) < wideColKeyLenSize+deltaKeyTSSize+deltaKeySeqSize { + end, ok := listUserKeyEnd(trimmed, deltaKeyTSSize+deltaKeySeqSize) + if !ok { return nil } - ukLen := binary.BigEndian.Uint32(trimmed[:wideColKeyLenSize]) - if uint32(len(trimmed)) < uint32(wideColKeyLenSize)+ukLen+uint32(deltaKeyTSSize+deltaKeySeqSize) { //nolint:gosec // constants fit in uint32 - return nil - } - return trimmed[wideColKeyLenSize : wideColKeyLenSize+ukLen] + return trimmed[wideColKeyLenSize:end] } // ExtractListUserKeyFromClaim extracts the logical user key from a list claim key. func ExtractListUserKeyFromClaim(key []byte) []byte { trimmed := bytes.TrimPrefix(key, []byte(ListClaimPrefix)) - if len(trimmed) < wideColKeyLenSize+sortableInt64Bytes { + end, ok := listUserKeyEnd(trimmed, sortableInt64Bytes) + if !ok { + return nil + } + return trimmed[wideColKeyLenSize:end] +} + +// ExtractListUserKeyFromDeltaScanKey extracts the logical user key from a +// delta scan prefix, a full delta key, or a scan cursor within that prefix. +func ExtractListUserKeyFromDeltaScanKey(key []byte) []byte { + return extractListUserKeyFromScanKey(key, []byte(ListMetaDeltaPrefix)) +} + +// ExtractListUserKeyFromClaimScanKey extracts the logical user key from a +// claim scan prefix, a full claim key, or a scan cursor within that prefix. +func ExtractListUserKeyFromClaimScanKey(key []byte) []byte { + return extractListUserKeyFromScanKey(key, []byte(ListClaimPrefix)) +} + +func extractListUserKeyFromScanKey(key []byte, prefix []byte) []byte { + if !bytes.HasPrefix(key, prefix) { return nil } - ukLen := binary.BigEndian.Uint32(trimmed[:wideColKeyLenSize]) - if uint32(len(trimmed)) < uint32(wideColKeyLenSize)+ukLen+uint32(sortableInt64Bytes) { //nolint:gosec // constants fit in uint32 + trimmed := key[len(prefix):] + end, ok := listUserKeyEnd(trimmed, 0) + if !ok { return nil } - return trimmed[wideColKeyLenSize : wideColKeyLenSize+ukLen] + return trimmed[wideColKeyLenSize:end] +} + +func listUserKeyEnd(trimmed []byte, suffixLen int) (int, bool) { + if len(trimmed) < wideColKeyLenSize+suffixLen { + return 0, false + } + userKeyLen := binary.BigEndian.Uint32(trimmed[:wideColKeyLenSize]) + requiredTail := uint64(wideColKeyLenSize) + uint64(suffixLen) //nolint:gosec // suffixLen is one of this file's fixed encoded suffix widths. + available := uint64(len(trimmed)) + if requiredTail > available || uint64(userKeyLen) > available-requiredTail { + return 0, false + } + return wideColKeyLenSize + int(userKeyLen), true //nolint:gosec // userKeyLen is bounded by len(trimmed) above. } // PrefixScanEnd returns the exclusive end key for a prefix scan. diff --git a/store/list_helpers_test.go b/store/list_helpers_test.go new file mode 100644 index 000000000..ea38a0fc3 --- /dev/null +++ b/store/list_helpers_test.go @@ -0,0 +1,61 @@ +package store + +import ( + "bytes" + "encoding/binary" + "math" + "testing" +) + +func TestExtractListUserKeyFromScanKeyBoundsOverflow(t *testing.T) { + t.Parallel() + + var lenPrefix [wideColKeyLenSize]byte + binary.BigEndian.PutUint32(lenPrefix[:], math.MaxUint32) + + for _, tc := range []struct { + name string + key []byte + extract func([]byte) []byte + }{ + { + name: "delta scan", + key: append(append([]byte(nil), []byte(ListMetaDeltaPrefix)...), lenPrefix[:]...), + extract: ExtractListUserKeyFromDeltaScanKey, + }, + { + name: "claim scan", + key: append(append([]byte(nil), []byte(ListClaimPrefix)...), lenPrefix[:]...), + extract: ExtractListUserKeyFromClaimScanKey, + }, + { + name: "full delta", + key: append(append(append([]byte(nil), []byte(ListMetaDeltaPrefix)...), lenPrefix[:]...), make([]byte, deltaKeyTSSize+deltaKeySeqSize)...), + extract: ExtractListUserKeyFromDelta, + }, + { + name: "full claim", + key: append(append(append([]byte(nil), []byte(ListClaimPrefix)...), lenPrefix[:]...), make([]byte, sortableInt64Bytes)...), + extract: ExtractListUserKeyFromClaim, + }, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + if got := tc.extract(tc.key); got != nil { + t.Fatalf("overflow user-key length: want nil, got %q", got) + } + }) + } +} + +func TestExtractListUserKeyFromScanKeyRoundTrip(t *testing.T) { + t.Parallel() + + userKey := []byte("list-user") + if got := ExtractListUserKeyFromDeltaScanKey(ListMetaDeltaScanPrefix(userKey)); !bytes.Equal(got, userKey) { + t.Fatalf("delta scan round trip: want %q, got %q", userKey, got) + } + if got := ExtractListUserKeyFromClaimScanKey(ListClaimScanPrefix(userKey)); !bytes.Equal(got, userKey) { + t.Fatalf("claim scan round trip: want %q, got %q", userKey, got) + } +} diff --git a/store/lsm_store.go b/store/lsm_store.go index ee101bfdb..0ce5af988 100644 --- a/store/lsm_store.go +++ b/store/lsm_store.go @@ -1104,6 +1104,33 @@ func (s *pebbleStore) GetAt(ctx context.Context, key []byte, ts uint64) ([]byte, return s.getAt(ctx, key, ts) } +func (s *pebbleStore) VersionExistsAtOrBefore(ctx context.Context, key []byte, ts uint64) (bool, error) { + s.dbMu.RLock() + defer s.dbMu.RUnlock() + if err := ctx.Err(); err != nil { + return false, errors.WithStack(err) + } + if readTSCompacted(ts, s.effectiveMinRetainedTS()) { + return false, ErrReadTSCompacted + } + + seekKey := encodeKey(key, ts) + iter, err := s.db.NewIter(&pebble.IterOptions{ + LowerBound: seekKey, + UpperBound: keyUpperBound(key), + }) + if err != nil { + return false, errors.WithStack(err) + } + defer iter.Close() + + if !iter.SeekGE(seekKey) { + return false, nil + } + userKey, _ := decodeKeyView(iter.Key()) + return bytes.Equal(userKey, key), nil +} + func (s *pebbleStore) GetAtBatch(ctx context.Context, keys [][]byte, ts uint64) (map[string][]byte, error) { if len(keys) == 0 { return map[string][]byte{}, nil diff --git a/store/mvcc_store.go b/store/mvcc_store.go index b1302d447..9776e2389 100644 --- a/store/mvcc_store.go +++ b/store/mvcc_store.go @@ -137,6 +137,15 @@ func latestVisible(vs []VersionedValue, ts uint64) (VersionedValue, bool) { return VersionedValue{}, false } +func versionExistsAtOrBefore(vs []VersionedValue, ts uint64) bool { + for i := len(vs) - 1; i >= 0; i-- { + if vs[i].TS <= ts { + return true + } + } + return false +} + func visibleValue(versions []VersionedValue, ts uint64) ([]byte, bool) { ver, ok := latestVisible(versions, ts) if !ok || ver.Tombstone { @@ -294,6 +303,21 @@ func (s *mvccStore) ExistsAt(_ context.Context, key []byte, ts uint64) (bool, er return true, nil } +func (s *mvccStore) VersionExistsAtOrBefore(_ context.Context, key []byte, ts uint64) (bool, error) { + s.mtx.RLock() + defer s.mtx.RUnlock() + if readTSCompacted(ts, s.minRetainedTS) { + return false, ErrReadTSCompacted + } + + v, ok := s.tree.Get(key) + if !ok { + return false, nil + } + versions, _ := v.([]VersionedValue) + return versionExistsAtOrBefore(versions, ts), nil +} + func computeScanCapHint(treeSize, limit int) int { capHint := boundedScanResultCapacity(limit) if treeSize < capHint { diff --git a/store/store.go b/store/store.go index 600c27cf7..362c3d505 100644 --- a/store/store.go +++ b/store/store.go @@ -105,6 +105,12 @@ type Snapshot interface { io.Closer } +// VersionPresenceReader reports whether a key has any committed version at or +// before a read timestamp, including tombstones and expired values. +type VersionPresenceReader interface { + VersionExistsAtOrBefore(ctx context.Context, key []byte, ts uint64) (bool, error) +} + // MVCCStore extends Store with multi-version concurrency control helpers. // The interface is timestamp-explicit; callers must supply the snapshot or // commit timestamp for every operation.