Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
43b4d73
store: add migration version import export
bootjp Jul 13, 2026
edf74ff
store: tighten migration export accounting
bootjp Jul 13, 2026
f95b557
distribution: add migration bracket planner
bootjp Jul 13, 2026
af847ef
Fix migration bracket edge cases
bootjp Jul 13, 2026
eab9622
Add migration fence drain guards
bootjp Jul 13, 2026
07a428e
Address migration guard review notes
bootjp Jul 13, 2026
7c17803
kv: fence broad mapped prefix deletes
bootjp Jul 13, 2026
e3079c0
store: fix migration export blockers
bootjp Jul 13, 2026
643a5a6
store: fix migration export metadata handling
bootjp Jul 13, 2026
061275a
Bound migration export sparse scans
bootjp Jul 13, 2026
31ed507
Fix migration export metadata handling
bootjp Jul 13, 2026
2065650
Fix migration route bracket filtering
bootjp Jul 13, 2026
9f0b7f1
store: skip export writer registry rows
bootjp Jul 14, 2026
c5dae8f
Fix bounded store export edge cases
bootjp Jul 14, 2026
c459255
Fix migration route edge cases
bootjp Jul 14, 2026
2b2cbd2
store: harden Pebble migration export
bootjp Jul 14, 2026
67f3e9d
store: separate list delta key prefix
bootjp Jul 14, 2026
eebdbf2
Stabilize stream latency seed writes
bootjp Jul 14, 2026
05b72cf
Bound migration export scan skips
bootjp Jul 14, 2026
5280996
Fix migration routing edge cases
bootjp Jul 14, 2026
a653226
Fence routed migration exports
bootjp Jul 14, 2026
e7467d5
Route legacy list deltas and stream scans
bootjp Jul 14, 2026
aa94570
Add migration bracket planner (#1086)
bootjp Jul 14, 2026
9f8dab7
Merge remote-tracking branch 'origin/design/hotspot-split-m2-wire' in…
bootjp Jul 14, 2026
6892dd9
Skip txn locks in migration export
bootjp Jul 14, 2026
fba7560
Filter legacy list deltas by value
bootjp Jul 14, 2026
ccda608
migration: merge m2 wire and fix legacy deltas
bootjp Jul 14, 2026
585e5ac
Merge remote-tracking branch 'origin/design/hotspot-split-m2-store-ex…
bootjp Jul 14, 2026
5acb33f
migration: fence S3 bucket metadata writes
bootjp Jul 14, 2026
84704f9
Close legacy list export gaps
bootjp Jul 14, 2026
f1aeeeb
Guard split fence retry paths
bootjp Jul 14, 2026
8d11161
Guard snapshot spooling disk headroom
bootjp Jul 14, 2026
0215c38
Raise snapshot spool headroom reserve
bootjp Jul 14, 2026
774779b
Fix raft startup and snapshot spool guards
bootjp Jul 14, 2026
46722f6
Handle route fences in adapter retries
bootjp Jul 14, 2026
70cf6f8
Stabilize raft snapshot dispatch and maintenance gates
bootjp Jul 14, 2026
a241809
Honor partition resolver in fence prechecks
bootjp Jul 14, 2026
ee363c4
Make raft startup and fence checks deterministic
bootjp Jul 14, 2026
54ad999
Gate public startup after raft replay
bootjp Jul 14, 2026
e20475f
Stabilize raft dispatch and S3 cleanup fences
bootjp Jul 14, 2026
c3a8ecf
Stabilize raft startup and write fences
bootjp Jul 14, 2026
61feed4
Stabilize route-fence cleanup retries
bootjp Jul 14, 2026
f373cd3
Keep heartbeat responses coalescing under read-index load
bootjp Jul 14, 2026
8f821fe
Stabilize raft snapshot recovery checkpoints
bootjp Jul 14, 2026
3e988ef
Enforce current route fences at apply time
bootjp Jul 14, 2026
04e8053
Prioritize received raft snapshots
bootjp Jul 14, 2026
d818602
Stabilize snapshot catch-up and fence checks
bootjp Jul 14, 2026
db9843a
Stabilize startup reads and snapshot recovery
bootjp Jul 15, 2026
a6d7d76
Reset stale raft peer connections
bootjp Jul 15, 2026
c8f865e
Stabilize snapshot catch-up and fence routing
bootjp Jul 15, 2026
b553090
Keep SQS receive route fences visible
bootjp Jul 15, 2026
d2b22ac
Stabilize redis proxy and raft recovery
bootjp Jul 15, 2026
549a786
Add migration fence drain guards (#1087)
bootjp Jul 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 116 additions & 4 deletions adapter/distribution_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/bootjp/elastickv/distribution"
"github.com/bootjp/elastickv/kv"
pb "github.com/bootjp/elastickv/proto"
"github.com/bootjp/elastickv/store"
"github.com/cockroachdb/errors"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
Expand All @@ -22,6 +23,7 @@ type DistributionServer struct {
catalog *distribution.CatalogStore
coordinator kv.Coordinator
readTracker *kv.ActiveTimestampTracker
readBlocked func() bool
reloadRetry struct {
attempts int
interval time.Duration
Expand All @@ -46,6 +48,12 @@ func WithDistributionActiveTimestampTracker(tracker *kv.ActiveTimestampTracker)
}
}

func WithDistributionReadGate(blocked func() bool) DistributionServerOption {
return func(s *DistributionServer) {
s.readBlocked = blocked
}
}

// WithCatalogReloadRetryPolicy configures the retry policy used after split
// commit when waiting for the local catalog snapshot to become visible.
func WithCatalogReloadRetryPolicy(attempts int, interval time.Duration) DistributionServerOption {
Expand Down Expand Up @@ -95,13 +103,30 @@ func NewDistributionServer(e *distribution.Engine, catalog *distribution.Catalog
return s
}

func (s *DistributionServer) SetReadGate(blocked func() bool) {
if s != nil {
s.readBlocked = blocked
}
}

func (s *DistributionServer) requireReadReady() error {
if s != nil && s.readBlocked != nil && s.readBlocked() {
//nolint:wrapcheck // Preserve the gRPC status code for startup readers.
return status.Error(codes.Unavailable, "distribution startup has not completed")
}
return nil
}

// UpdateRoute allows updating route information.
func (s *DistributionServer) UpdateRoute(start, end []byte, group uint64) {
s.engine.UpdateRoute(start, end, group)
}

// GetRoute returns route for a key.
func (s *DistributionServer) GetRoute(ctx context.Context, req *pb.GetRouteRequest) (*pb.GetRouteResponse, error) {
if err := s.requireReadReady(); err != nil {
return nil, err
}
r, ok := s.engine.GetRoute(req.Key)
if !ok {
return &pb.GetRouteResponse{}, nil
Expand All @@ -121,6 +146,9 @@ func (s *DistributionServer) GetTimestamp(ctx context.Context, req *pb.GetTimest

// ListRoutes returns all durable routes from catalog storage.
func (s *DistributionServer) ListRoutes(ctx context.Context, req *pb.ListRoutesRequest) (*pb.ListRoutesResponse, error) {
if err := s.requireReadReady(); err != nil {
return nil, err
}
snapshot, err := s.loadCatalogSnapshot(ctx)
if err != nil {
return nil, err
Expand Down Expand Up @@ -162,14 +190,18 @@ func (s *DistributionServer) SplitRange(ctx context.Context, req *pb.SplitRangeR
if err := validateSplitKey(parent, splitKey); err != nil {
return nil, err
}
splitJobReadKeys, err := s.splitJobOverlapReadKeys(ctx, snapshot, parent)
if err != nil {
return nil, err
}

leftID, rightID, err := s.allocateChildRouteIDs(ctx, snapshot.ReadTS, snapshot.Routes)
if err != nil {
return nil, err
}
left, right := splitCatalogRoutes(parent, splitKey, leftID, rightID)

saved, err := s.saveSplitResultViaCoordinator(ctx, snapshot.ReadTS, req.GetExpectedCatalogVersion(), parent.RouteID, left, right)
saved, err := s.saveSplitResultViaCoordinator(ctx, snapshot.ReadTS, req.GetExpectedCatalogVersion(), parent.RouteID, splitJobReadKeys, left, right)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -210,6 +242,7 @@ func (s *DistributionServer) saveSplitResultViaCoordinator(
readTS uint64,
expectedVersion uint64,
parentID uint64,
readKeys [][]byte,
left distribution.RouteDescriptor,
right distribution.RouteDescriptor,
) (distribution.CatalogSnapshot, error) {
Expand All @@ -227,10 +260,14 @@ func (s *DistributionServer) saveSplitResultViaCoordinator(
return distribution.CatalogSnapshot{}, grpcStatusErrorf(codes.Internal, "build split mutations: %v", err)
}
if _, err := s.coordinator.Dispatch(ctx, &kv.OperationGroup[kv.OP]{
Elems: ops,
IsTxn: true,
StartTS: readTS,
Elems: ops,
IsTxn: true,
StartTS: readTS,
ReadKeys: readKeys,
}); err != nil {
if errors.Is(err, store.ErrWriteConflict) {
return distribution.CatalogSnapshot{}, grpcStatusError(codes.Aborted, errDistributionCatalogConflict.Error())
}
return distribution.CatalogSnapshot{}, grpcStatusErrorf(codes.Internal, "commit split mutations: %v", err)
}
return s.loadCatalogSnapshotAtLeastVersion(ctx, nextVersion)
Expand Down Expand Up @@ -377,6 +414,81 @@ func validateSplitKey(parent distribution.RouteDescriptor, splitKey []byte) erro
return nil
}

func (s *DistributionServer) splitJobOverlapReadKeys(ctx context.Context, snapshot distribution.CatalogSnapshot, parent distribution.RouteDescriptor) ([][]byte, error) {
jobs, err := s.catalog.ListSplitJobsAt(ctx, snapshot.ReadTS)
if err != nil {
return nil, grpcStatusErrorf(codes.Internal, "load split jobs: %v", err)
}
readKeys := splitJobReadFenceKeys(jobs)
for _, job := range jobs {
if !splitJobIsLive(job) {
continue
}
for _, interval := range liveSplitJobIntervals(job, snapshot.Routes) {
if routeRangeIntersects(parent.Start, parent.End, interval.start, interval.end) {
return nil, grpcStatusError(codes.Aborted, distribution.ErrSplitJobOverlap.Error())
}
}
}
return readKeys, nil
}

func splitJobReadFenceKeys(jobs []distribution.SplitJob) [][]byte {
readKeys := make([][]byte, 0, len(jobs)+1)
readKeys = append(readKeys, distribution.CatalogNextSplitJobIDKey())
for _, job := range jobs {
if splitJobIsLive(job) {
readKeys = append(readKeys, distribution.CatalogSplitJobKey(job.JobID))
}
}
return readKeys
}

func splitJobIsLive(job distribution.SplitJob) bool {
return job.Phase != distribution.SplitJobPhaseDone && job.Phase != distribution.SplitJobPhaseAbandoned
}

type routeInterval struct {
start []byte
end []byte
}

const initialLiveSplitJobIntervalCapacity = 2

func liveSplitJobIntervals(job distribution.SplitJob, routes []distribution.RouteDescriptor) []routeInterval {
out := make([]routeInterval, 0, initialLiveSplitJobIntervalCapacity)
for _, route := range routes {
switch {
case route.RouteID == job.SourceRouteID:
out = append(out, routeInterval{
start: distribution.CloneBytes(job.SplitKey),
end: distribution.CloneBytes(route.End),
})
case route.ParentRouteID == job.SourceRouteID && routeRangeIntersects(route.Start, route.End, job.SplitKey, nil):
out = append(out, routeInterval{
start: distribution.CloneBytes(route.Start),
end: distribution.CloneBytes(route.End),
})
case job.JobID != 0 && route.MigrationJobID == job.JobID:
out = append(out, routeInterval{
start: distribution.CloneBytes(route.Start),
end: distribution.CloneBytes(route.End),
})
}
}
return out
}

func routeRangeIntersects(aStart, aEnd, bStart, bEnd []byte) bool {
if aEnd != nil && bytes.Compare(aEnd, bStart) <= 0 {
return false
}
if bEnd != nil && bytes.Compare(bEnd, aStart) <= 0 {
return false
}
return true
}

func splitCatalogRoutes(
parent distribution.RouteDescriptor,
splitKey []byte,
Expand Down
Loading
Loading