Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
72fcc6b
distribution: add migration wire contracts
bootjp Jul 13, 2026
7dd2e6d
distribution: pin route descriptor v2 codec
bootjp Jul 13, 2026
ef2ba76
distribution: preserve migration route fences
bootjp Jul 13, 2026
2388b3e
Preserve migration read fences
bootjp Jul 13, 2026
781dc36
Fix fenced raw scan edge cases
bootjp Jul 13, 2026
1afde8f
Preserve raw scan route bounds
bootjp Jul 14, 2026
33c793f
Honor route-bounded local scans
bootjp Jul 14, 2026
a456f48
Preserve route group on bounded scan proxy
bootjp Jul 14, 2026
bcdee5b
Allow route-bound reverse raw scans
bootjp Jul 14, 2026
5994c04
Allow route-bound grouped reverse scans
bootjp Jul 14, 2026
c88f654
Gate route v2 writes and bound scans
bootjp Jul 14, 2026
0d130ad
migration: fix route-filtered scans
bootjp Jul 14, 2026
9927a6e
kv: tidy route-fence scan tests
bootjp Jul 17, 2026
e6776d3
Merge main into hotspot split M2 wire
bootjp Jul 18, 2026
8366ec2
Update TLA tools checksum
bootjp Jul 18, 2026
e42f505
kv: fence reads on catalog version
bootjp Jul 18, 2026
08929f9
test: table read fence scan variants
bootjp Jul 18, 2026
449fe36
kv: route wide-column scans consistently
bootjp Jul 18, 2026
65e2973
kv: harden read-fenced scans
bootjp Jul 18, 2026
bd17534
kv: make read routing snapshots atomic
bootjp Jul 18, 2026
2f73906
kv: harden fenced prefix scans
bootjp Jul 18, 2026
f9b1fce
Merge main into hotspot split M2 wire
bootjp Jul 19, 2026
6bebfda
Merge current main into hotspot split M2 wire
bootjp Jul 19, 2026
94bf4cd
distribution: patch split HLC at codec offset
bootjp Jul 19, 2026
5b993b3
store: add migration version import export
bootjp Jul 19, 2026
b4b72f3
Fix legacy list TTL delta handling
bootjp Jul 19, 2026
4061c00
Refactor split and scan route selection
bootjp Jul 19, 2026
87d5776
Fix distribution registration startup wiring
bootjp Jul 19, 2026
f624305
Fix snapshot applied-index test peers
bootjp Jul 19, 2026
9bde1cd
Merge current main
bootjp Jul 19, 2026
707cc61
routing: normalize list auxiliary scans
bootjp Jul 19, 2026
e91cba4
Merge updated hotspot split M2 wire parent
bootjp Jul 19, 2026
93dfbb3
Fix migration export review blockers
bootjp Jul 23, 2026
1df401d
Merge remote-tracking branch 'origin/design/hotspot-split-m2-wire' in…
bootjp Jul 23, 2026
4a04a83
Preserve migration metadata and retry route fences
bootjp Jul 23, 2026
c7dd014
Expose shard stores for blob backfill scans
bootjp Jul 23, 2026
dcb7e2c
Use allocated split commit timestamp
bootjp Jul 23, 2026
db7493c
store: preserve legacy wide-column routing
bootjp Jul 23, 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
13 changes: 12 additions & 1 deletion .github/workflows/golangci-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,18 @@ jobs:
steps:
- name: Check out code into the Go module directory
uses: actions/checkout@v7
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: "1.x"
- name: Install golangci-lint
run: |
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b "$(go env GOPATH)/bin" v2.9.0
echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH"
- name: Run golangci-lint
run: golangci-lint run --config=.golangci.yaml
- name: golangci-lint
continue-on-error: true
uses: reviewdog/action-golangci-lint@v2
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
Expand All @@ -28,4 +39,4 @@ jobs:
filter_mode: nofilter
reporter: github-pr-review
cache: false
fail_on_error: true
fail_on_error: false
30 changes: 23 additions & 7 deletions .github/workflows/redis-proxy-docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,29 @@ jobs:

- name: Docker metadata
id: meta
uses: docker/metadata-action@v6
with:
images: ghcr.io/${{ github.repository }}/redis-proxy
tags: |
type=sha
type=ref,event=branch
type=raw,value=latest,enable={{is_default_branch}}
shell: bash
env:
IMAGE: ghcr.io/${{ github.repository }}/redis-proxy
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
run: |
short_sha="${GITHUB_SHA::7}"
ref_tag="$(printf '%s' "$GITHUB_REF_NAME" | sed -E 's/[^A-Za-z0-9_.-]+/-/g; s/^-+//; s/-+$//')"
ref_tag="${ref_tag:0:128}"
{
echo "tags<<EOF"
echo "${IMAGE}:sha-${short_sha}"
if [[ "${GITHUB_EVENT_NAME}" != "pull_request" && -n "${ref_tag}" ]]; then
echo "${IMAGE}:${ref_tag}"
fi
if [[ "${GITHUB_REF_NAME}" == "${DEFAULT_BRANCH}" ]]; then
echo "${IMAGE}:latest"
fi
echo "EOF"
echo "labels<<EOF"
echo "org.opencontainers.image.source=${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}"
echo "org.opencontainers.image.revision=${GITHUB_SHA}"
echo "EOF"
} >> "$GITHUB_OUTPUT"

- name: Build and push
uses: docker/build-push-action@v7
Expand Down
183 changes: 157 additions & 26 deletions adapter/distribution_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ type DistributionServer struct {
watchInterval time.Duration
watchLeader func() bool
fsObserver DistributionFilesystemObserver
readBlocked func() bool
reloadRetry struct {
attempts int
interval time.Duration
Expand Down Expand Up @@ -65,6 +66,12 @@ func WithDistributionFilesystemObserver(observer DistributionFilesystemObserver)
}
}

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 @@ -136,13 +143,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(kv.RouteKey(req.Key))
if !ok {
return &pb.GetRouteResponse{}, nil
Expand All @@ -162,6 +186,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 @@ -352,32 +379,19 @@ func (s *DistributionServer) SplitRange(ctx context.Context, req *pb.SplitRangeR
return nil, err
}

parent, found := findRouteByID(snapshot.Routes, req.GetRouteId())
if !found {
return nil, grpcStatusError(codes.NotFound, errDistributionUnknownRoute.Error())
}

rawSplitKey := req.GetSplitKey()
splitKey := distribution.CloneBytes(fskeys.NormalizeSplitBoundary(kv.RouteKey(rawSplitKey)))
if err := validateSplitKey(parent, splitKey); err != nil {
s.observeFilePinnedHotspotIfNeeded(rawSplitKey, splitKey, err)
return nil, err
}

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

saved, err := s.saveSplitResultViaCoordinator(ctx, snapshot.ReadTS, req.GetExpectedCatalogVersion(), parent.RouteID, left, right)
saved, err := s.saveSplitResultViaCoordinator(ctx, snapshot.ReadTS, req.GetExpectedCatalogVersion(), plan.parentID, plan.readKeys, plan.left, plan.right)
if err != nil {
return nil, err
}
if err := s.applyEngineSnapshot(saved); err != nil {
return nil, err
}
savedLeft, savedRight, err := splitChildrenFromSnapshot(saved, left.RouteID, right.RouteID)
savedLeft, savedRight, err := splitChildrenFromSnapshot(saved, plan.left.RouteID, plan.right.RouteID)
if err != nil {
return nil, err
}
Expand All @@ -389,6 +403,37 @@ func (s *DistributionServer) SplitRange(ctx context.Context, req *pb.SplitRangeR
}, nil
}

type splitRangePlan struct {
parentID uint64
readKeys [][]byte
left distribution.RouteDescriptor
right distribution.RouteDescriptor
}

func (s *DistributionServer) planSplitRange(ctx context.Context, snapshot distribution.CatalogSnapshot, req *pb.SplitRangeRequest) (splitRangePlan, error) {
parent, found := findRouteByID(snapshot.Routes, req.GetRouteId())
if !found {
return splitRangePlan{}, grpcStatusError(codes.NotFound, errDistributionUnknownRoute.Error())
}

rawSplitKey := req.GetSplitKey()
splitKey := distribution.CloneBytes(fskeys.NormalizeSplitBoundary(kv.RouteKey(rawSplitKey)))
if err := validateSplitKey(parent, splitKey); err != nil {
s.observeFilePinnedHotspotIfNeeded(rawSplitKey, splitKey, err)
return splitRangePlan{}, err
}
readKeys, err := s.splitJobOverlapReadKeys(ctx, snapshot, parent)
if err != nil {
return splitRangePlan{}, err
}
leftID, rightID, err := s.allocateChildRouteIDs(ctx, snapshot.ReadTS, snapshot.Routes)
if err != nil {
return splitRangePlan{}, err
}
left, right := splitCatalogRoutes(parent, splitKey, leftID, rightID, 0)
return splitRangePlan{parentID: parent.RouteID, readKeys: readKeys, left: left, right: right}, nil
}

func (s *DistributionServer) pinReadTS(ts uint64) *kv.ActiveTimestampToken {
if s == nil || s.readTracker == nil {
return nil
Expand Down Expand Up @@ -437,17 +482,14 @@ func (s *DistributionServer) saveSplitResultViaCoordinator(
readTS uint64,
expectedVersion uint64,
parentID uint64,
readKeys [][]byte,
left distribution.RouteDescriptor,
right distribution.RouteDescriptor,
) (distribution.CatalogSnapshot, error) {
if expectedVersion == math.MaxUint64 {
return distribution.CatalogSnapshot{}, grpcStatusError(codes.Internal, "catalog version overflow")
}
nextVersion := expectedVersion + 1
if right.RouteID == math.MaxUint64 {
return distribution.CatalogSnapshot{}, grpcStatusError(codes.Internal, errDistributionRouteIDOverflow.Error())
nextVersion, nextRouteID, err := nextCatalogSplitIDs(expectedVersion, right.RouteID)
if err != nil {
return distribution.CatalogSnapshot{}, err
}
nextRouteID := right.RouteID + 1
commitTS, err := kv.NextTimestampAfterThrough(ctx, s.coordinator, readTS, "split range: allocate commitTS")
if err != nil {
return distribution.CatalogSnapshot{}, grpcStatusErrorf(codes.Internal, "allocate split commit timestamp: %v", err)
Expand Down Expand Up @@ -482,8 +524,12 @@ func (s *DistributionServer) saveSplitResultViaCoordinator(
IsTxn: true,
StartTS: readTS,
CommitTS: commitTS,
ReadKeys: readKeys,
})
if 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)
}
if resp == nil || resp.CommitTS == 0 {
Expand All @@ -492,6 +538,16 @@ func (s *DistributionServer) saveSplitResultViaCoordinator(
return s.loadCatalogSnapshotAtVersion(ctx, resp.CommitTS, nextVersion)
}

func nextCatalogSplitIDs(expectedVersion uint64, rightRouteID uint64) (uint64, uint64, error) {
if expectedVersion == math.MaxUint64 {
return 0, 0, grpcStatusError(codes.Internal, "catalog version overflow")
}
if rightRouteID == math.MaxUint64 {
return 0, 0, grpcStatusError(codes.Internal, errDistributionRouteIDOverflow.Error())
}
return expectedVersion + 1, rightRouteID + 1, nil
}

func catalogStoreMutationsToOps(mutations []*store.KVPairMutation) ([]*kv.Elem[kv.OP], error) {
ops := make([]*kv.Elem[kv.OP], 0, len(mutations))
for _, mutation := range mutations {
Expand Down Expand Up @@ -672,6 +728,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 All @@ -687,10 +818,10 @@ func splitCatalogRoutes(
GroupID: parent.GroupID,
State: parent.State,
ParentRouteID: parent.RouteID,
SplitAtHLC: splitAtHLC,
StagedVisibilityActive: parent.StagedVisibilityActive,
MigrationJobID: parent.MigrationJobID,
MinWriteTSExclusive: parent.MinWriteTSExclusive,
SplitAtHLC: splitAtHLC,
}
right := distribution.RouteDescriptor{
RouteID: rightID,
Expand All @@ -699,10 +830,10 @@ func splitCatalogRoutes(
GroupID: parent.GroupID,
State: parent.State,
ParentRouteID: parent.RouteID,
SplitAtHLC: splitAtHLC,
StagedVisibilityActive: parent.StagedVisibilityActive,
MigrationJobID: parent.MigrationJobID,
MinWriteTSExclusive: parent.MinWriteTSExclusive,
SplitAtHLC: splitAtHLC,
}
return left, right
}
Expand Down Expand Up @@ -758,10 +889,10 @@ func toProtoRouteDescriptor(route distribution.RouteDescriptor) *pb.RouteDescrip
RaftGroupId: route.GroupID,
State: toProtoRouteState(route.State),
ParentRouteId: route.ParentRouteID,
SplitAtHlc: route.SplitAtHLC,
StagedVisibilityActive: route.StagedVisibilityActive,
MigrationJobId: route.MigrationJobID,
MinWriteTsExclusive: route.MinWriteTSExclusive,
SplitAtHlc: route.SplitAtHLC,
}
}

Expand Down
Loading
Loading