blob: ab57c3fe217163f77d662282e80ddb084c662d44 [file]
/*
Copyright 2026 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package spanner
import (
"bytes"
"context"
"hash/crc32"
"math/rand"
"sort"
"sync"
"sync/atomic"
"time"
sppb "cloud.google.com/go/spanner/apiv1/spannerpb"
"google.golang.org/grpc/connectivity"
)
const (
maxLocalReplicaDistance = 5
defaultMinEntriesForRandomPick = 1000
maxRangesPerPartition = 1024
groupCacheShardBits = 12
groupCacheShardCount = 1 << groupCacheShardBits
localLeaderSelectionCostBias = 0.5
)
var crc32cTable = crc32.MakeTable(crc32.Castagnoli)
type rangeMode int
const (
rangeModeCoveringSplit rangeMode = iota
rangeModePickRandom
)
type keyRangeCache struct {
endpointCache channelEndpointCache
updateMu sync.Mutex
configMu sync.RWMutex
lifecycleManager *endpointLifecycleManager
deterministicRandom bool
minEntriesForRandomPickHint int
state atomic.Value // *keyRangeCacheState
accessCounter atomic.Int64
}
type cachedTablet struct {
tabletUID uint64
incarnation []byte
serverAddress string
distance uint32
skip bool
role sppb.Tablet_Role
location string
endpoint atomic.Pointer[cachedTabletEndpointRef]
}
type eligibleReplica struct {
tablet *cachedTablet
endpoint channelEndpoint
selectionCost float64
}
type routeSelectionState struct {
sawMatchingReplica bool
sawCoolingDownReplica bool
sawNonCoolingDownReplica bool
hasUnavailableReplica bool
hasUnroutableReplica bool
}
func (s routeSelectionState) allCoolingDown() bool {
return s.sawMatchingReplica && s.sawCoolingDownReplica && !s.sawNonCoolingDownReplica
}
type cachedGroup struct {
groupUID uint64
mu sync.RWMutex
generation []byte
tablets []*cachedTablet
leaderIdx int
}
type cachedTabletEndpointRef struct {
endpoint channelEndpoint
}
type cachedRange struct {
startKey []byte
limitKey []byte
groupUID uint64
splitID uint64
generation []byte
lastAccess int64
}
type rangePartition struct {
startKey []byte
limitKey []byte
ranges []*cachedRange
}
type keyRangeCacheState struct {
partitions []*rangePartition
groupShards [groupCacheShardCount]map[uint64]*cachedGroup
groupCount int
rangeCount int
}
type keyRangeCacheRoutingConfig struct {
lifecycleManager *endpointLifecycleManager
deterministicRandom bool
minEntriesForRandomPick int
}
type keyRangeCacheStateBuilder struct {
cache *keyRangeCache
partitions []*rangePartition
groupShards [groupCacheShardCount]map[uint64]*cachedGroup
clonedGroupShards [groupCacheShardCount]bool
mutableGroups map[uint64]struct{}
overlappingRanges int
rangesInserted int
rangesRemoved int
clonedRangeShardCount int
rangeShardSizeSum int
rangeShardSizeMax int
groupShardSizeSum int
groupShardSizeMax int
groupCount int
rangeCount int
}
func newKeyRangeCache(endpointCache channelEndpointCache) *keyRangeCache {
if endpointCache == nil {
endpointCache = newPassthroughChannelEndpointCache()
}
cache := &keyRangeCache{
endpointCache: endpointCache,
minEntriesForRandomPickHint: defaultMinEntriesForRandomPick,
}
cache.state.Store(&keyRangeCacheState{})
return cache
}
func (c *keyRangeCache) useDeterministicRandom() {
c.configMu.Lock()
defer c.configMu.Unlock()
c.deterministicRandom = true
}
func (c *keyRangeCache) setMinEntriesForRandomPick(value int) {
c.configMu.Lock()
defer c.configMu.Unlock()
if value <= 0 {
value = defaultMinEntriesForRandomPick
}
c.minEntriesForRandomPickHint = value
}
func (c *keyRangeCache) setLifecycleManager(lifecycleManager *endpointLifecycleManager) {
c.configMu.Lock()
defer c.configMu.Unlock()
c.lifecycleManager = lifecycleManager
}
func (c *keyRangeCache) recordReplicaLatency(operationUID uint64, address string, latency time.Duration) {
endpointLatencyRegistryRecordLatency(operationUID, false, address, latency)
}
func (c *keyRangeCache) recordReplicaError(operationUID uint64, address string) {
endpointLatencyRegistryRecordError(operationUID, false, address)
}
func routingOperationUID(hint *sppb.RoutingHint) uint64 {
if hint == nil {
return 0
}
return hint.GetOperationUid()
}
func (c *keyRangeCache) loadState() *keyRangeCacheState {
state, _ := c.state.Load().(*keyRangeCacheState)
if state == nil {
return &keyRangeCacheState{}
}
return state
}
func (c *keyRangeCache) loadRoutingConfig() keyRangeCacheRoutingConfig {
c.configMu.RLock()
defer c.configMu.RUnlock()
minEntries := c.minEntriesForRandomPickHint
if minEntries <= 0 {
minEntries = defaultMinEntriesForRandomPick
}
return keyRangeCacheRoutingConfig{
lifecycleManager: c.lifecycleManager,
deterministicRandom: c.deterministicRandom,
minEntriesForRandomPick: minEntries,
}
}
func cloneCachedGroup(group *cachedGroup) *cachedGroup {
if group == nil {
return nil
}
group.mu.RLock()
defer group.mu.RUnlock()
cloned := &cachedGroup{
groupUID: group.groupUID,
generation: append([]byte(nil), group.generation...),
leaderIdx: group.leaderIdx,
tablets: make([]*cachedTablet, 0, len(group.tablets)),
}
for _, tablet := range group.tablets {
if tablet == nil {
cloned.tablets = append(cloned.tablets, nil)
continue
}
clonedTablet := &cachedTablet{
tabletUID: tablet.tabletUID,
incarnation: append([]byte(nil), tablet.incarnation...),
serverAddress: tablet.serverAddress,
distance: tablet.distance,
skip: tablet.skip,
role: tablet.role,
location: tablet.location,
}
clonedTablet.storeEndpoint(tablet.loadEndpoint())
cloned.tablets = append(cloned.tablets, clonedTablet)
}
return cloned
}
func (c *keyRangeCache) cloneState() *keyRangeCacheStateBuilder {
current := c.loadState()
builder := &keyRangeCacheStateBuilder{
cache: c,
partitions: append([]*rangePartition(nil), current.partitions...),
mutableGroups: make(map[uint64]struct{}),
groupCount: current.groupCount,
rangeCount: current.rangeCount,
}
for shardIdx := range current.groupShards {
builder.groupShards[shardIdx] = current.groupShards[shardIdx]
}
return builder
}
func (b *keyRangeCacheStateBuilder) snapshot() *keyRangeCacheState {
return &keyRangeCacheState{
partitions: b.partitions,
groupShards: b.groupShards,
groupCount: b.groupCount,
rangeCount: b.rangeCount,
}
}
func groupShardIndex(groupUID uint64) int {
return int(mixUint64(groupUID) & uint64(groupCacheShardCount-1))
}
func mixUint64(v uint64) uint64 {
v ^= v >> 30
v *= 0xbf58476d1ce4e5b9
v ^= v >> 27
v *= 0x94d049bb133111eb
v ^= v >> 31
return v
}
func (b *keyRangeCacheStateBuilder) cloneGroupShard(idx int) {
if idx < 0 || idx >= groupCacheShardCount || b.clonedGroupShards[idx] {
return
}
original := b.groupShards[idx]
b.groupShardSizeSum += len(original)
if len(original) > b.groupShardSizeMax {
b.groupShardSizeMax = len(original)
}
if len(original) == 0 {
b.groupShards[idx] = make(map[uint64]*cachedGroup)
b.clonedGroupShards[idx] = true
return
}
cloned := make(map[uint64]*cachedGroup, len(original))
for groupUID, group := range original {
cloned[groupUID] = group
}
b.groupShards[idx] = cloned
b.clonedGroupShards[idx] = true
}
func (c *keyRangeCache) addRanges(cacheUpdate *sppb.CacheUpdate) {
if cacheUpdate == nil {
return
}
c.updateMu.Lock()
defer c.updateMu.Unlock()
builder := c.cloneState()
newGroups := make([]*cachedGroup, 0, len(cacheUpdate.GetGroup()))
for _, groupIn := range cacheUpdate.GetGroup() {
newGroups = append(newGroups, builder.findOrInsertGroup(groupIn))
}
for _, rangeIn := range cacheUpdate.GetRange() {
builder.replaceRangeIfNewer(rangeIn)
}
for _, group := range newGroups {
builder.unrefGroup(group)
}
c.state.Store(builder.snapshot())
}
func (c *keyRangeCache) fillRoutingHint(ctx context.Context, preferLeader bool, mode rangeMode, directedReadOptions *sppb.DirectedReadOptions, hint *sppb.RoutingHint) channelEndpoint {
return c.fillRoutingHintWithCooldownTracker(ctx, preferLeader, mode, directedReadOptions, hint, nil)
}
func (c *keyRangeCache) fillRoutingHintWithCooldownTracker(ctx context.Context, preferLeader bool, mode rangeMode, directedReadOptions *sppb.DirectedReadOptions, hint *sppb.RoutingHint, cooldowns *endpointOverloadCooldownTracker) channelEndpoint {
if hint == nil || len(hint.GetKey()) == 0 {
return nil
}
if directedReadOptions == nil {
directedReadOptions = &sppb.DirectedReadOptions{}
}
state := c.loadState()
cfg := c.loadRoutingConfig()
targetRange := c.findRangeInState(state, hint.GetKey(), hint.GetLimitKey(), mode, cfg)
if targetRange == nil {
return nil
}
targetGroup := state.findGroup(targetRange.groupUID)
if targetGroup == nil {
return nil
}
hint.GroupUid = targetRange.groupUID
hint.SplitId = targetRange.splitID
hint.Key = append(hint.Key[:0], targetRange.startKey...)
hint.LimitKey = append(hint.LimitKey[:0], targetRange.limitKey...)
return targetGroup.fillRoutingHintWithCooldownTracker(ctx, c.endpointCache, cfg.lifecycleManager, cfg.deterministicRandom, preferLeader, directedReadOptions, hint, cooldowns)
}
func (c *keyRangeCache) clear() {
c.updateMu.Lock()
defer c.updateMu.Unlock()
c.state.Store(&keyRangeCacheState{})
c.accessCounter.Store(0)
}
func (c *keyRangeCache) size() int {
return c.loadState().rangeCount
}
func newRangePartition(ranges []*cachedRange) *rangePartition {
if len(ranges) == 0 {
return nil
}
return &rangePartition{
startKey: append([]byte(nil), ranges[0].startKey...),
limitKey: append([]byte(nil), ranges[len(ranges)-1].limitKey...),
ranges: ranges,
}
}
func buildRangePartitions(ranges []*cachedRange) []*rangePartition {
if len(ranges) == 0 {
return nil
}
sort.Slice(ranges, func(i, j int) bool {
return bytes.Compare(ranges[i].startKey, ranges[j].startKey) < 0
})
partitions := make([]*rangePartition, 0, (len(ranges)+maxRangesPerPartition-1)/maxRangesPerPartition)
for i := 0; i < len(ranges); i += maxRangesPerPartition {
end := i + maxRangesPerPartition
if end > len(ranges) {
end = len(ranges)
}
chunk := append([]*cachedRange(nil), ranges[i:end]...)
partitions = append(partitions, newRangePartition(chunk))
}
return partitions
}
func uniqueRangesFromPartitions(partitions []*rangePartition) []*cachedRange {
if len(partitions) == 0 {
return nil
}
total := 0
for _, partition := range partitions {
if partition != nil {
total += len(partition.ranges)
}
}
ranges := make([]*cachedRange, 0, total)
for _, partition := range partitions {
if partition == nil {
continue
}
ranges = append(ranges, partition.ranges...)
}
return ranges
}
func findPartitionStartIndex(partitions []*rangePartition, key []byte) int {
return sort.Search(len(partitions), func(i int) bool {
return bytes.Compare(partitions[i].limitKey, key) > 0
})
}
func findOverlappingPartitionWindow(partitions []*rangePartition, startKey, limitKey []byte) (int, int) {
start := findPartitionStartIndex(partitions, startKey)
if len(limitKey) == 0 {
end := start
if start < len(partitions) && bytes.Compare(partitions[start].startKey, startKey) <= 0 {
end = start + 1
}
return start, end
}
end := start
for end < len(partitions) && bytes.Compare(partitions[end].startKey, limitKey) < 0 {
end++
}
return start, end
}
func (b *keyRangeCacheStateBuilder) recordTouchedPartitions(start, end int) {
if start < 0 {
start = 0
}
if end > len(b.partitions) {
end = len(b.partitions)
}
for _, partition := range b.partitions[start:end] {
if partition == nil {
continue
}
size := len(partition.ranges)
b.clonedRangeShardCount++
b.rangeShardSizeSum += size
if size > b.rangeShardSizeMax {
b.rangeShardSizeMax = size
}
}
}
func (b *keyRangeCacheStateBuilder) replacePartitionWindow(start, end int, ranges []*cachedRange) {
b.recordTouchedPartitions(start, end)
rebuilt := buildRangePartitions(ranges)
next := make([]*rangePartition, 0, len(b.partitions)-(end-start)+len(rebuilt))
next = append(next, b.partitions[:start]...)
next = append(next, rebuilt...)
next = append(next, b.partitions[end:]...)
b.partitions = next
}
func (c *keyRangeCache) shrinkTo(newSize int) {
c.updateMu.Lock()
defer c.updateMu.Unlock()
builder := c.cloneState()
if newSize <= 0 {
c.state.Store(&keyRangeCacheState{})
c.accessCounter.Store(0)
return
}
if newSize >= builder.rangeCount {
return
}
allRanges := uniqueRangesFromPartitions(builder.partitions)
if newSize >= len(allRanges) {
return
}
numToShrink := len(allRanges) - newSize
numToSample := numToShrink * 2
if numToSample > len(allRanges) {
numToSample = len(allRanges)
}
perm := rand.Perm(len(allRanges))
sampled := make([]*cachedRange, 0, numToSample)
for i := 0; i < numToSample; i++ {
sampled = append(sampled, allRanges[perm[i]])
}
sort.Slice(sampled, func(i, j int) bool {
return sampled[i].lastAccess < sampled[j].lastAccess
})
evicted := make(map[*cachedRange]struct{}, numToShrink)
for i := 0; i < numToShrink; i++ {
evicted[sampled[i]] = struct{}{}
}
kept := make([]*cachedRange, 0, len(allRanges)-numToShrink)
for _, r := range allRanges {
if _, ok := evicted[r]; ok {
continue
}
kept = append(kept, r)
}
builder.recordTouchedPartitions(0, len(builder.partitions))
builder.partitions = buildRangePartitions(kept)
builder.rangeCount = len(allRanges) - numToShrink
c.state.Store(builder.snapshot())
}
func (c *keyRangeCache) accessTimeNow() int64 {
return c.accessCounter.Add(1)
}
func (c *keyRangeCache) findRangeInState(state *keyRangeCacheState, key, limit []byte, mode rangeMode, cfg keyRangeCacheRoutingConfig) *cachedRange {
if state == nil {
return nil
}
ranges := c.lookupRangesForState(state, key, limit)
low, high := 0, len(ranges)
for low < high {
mid := int(uint(low+high) >> 1)
if bytes.Compare(ranges[mid].limitKey, key) > 0 {
high = mid
} else {
low = mid + 1
}
}
idx := low
if idx >= len(ranges) {
return nil
}
first := ranges[idx]
startInRange := bytes.Compare(key, first.startKey) >= 0
if len(limit) == 0 {
if startInRange {
atomic.StoreInt64(&first.lastAccess, c.accessTimeNow())
return first
}
return nil
}
if startInRange && bytes.Compare(limit, first.limitKey) <= 0 {
atomic.StoreInt64(&first.lastAccess, c.accessTimeNow())
return first
}
if mode == rangeModeCoveringSplit {
return nil
}
total := 0
foundGap := !startInRange
sampledIdx := idx
lastLimit := first.startKey
hitEnd := false
i := idx
for ; i < len(ranges); i++ {
current := ranges[i]
if bytes.Compare(lastLimit, current.startKey) != 0 {
foundGap = true
if bytes.Compare(current.startKey, limit) >= 0 {
break
}
}
total++
if c.uniformRandom(total, key, limit, current.startKey, cfg.deterministicRandom) == 0 {
sampledIdx = i
}
lastLimit = current.limitKey
if bytes.Compare(lastLimit, limit) >= 0 || total >= cfg.minEntriesForRandomPick {
break
}
}
if i >= len(ranges) {
hitEnd = true
}
if hitEnd {
foundGap = true
}
if !foundGap || total >= cfg.minEntriesForRandomPick {
selected := ranges[sampledIdx]
atomic.StoreInt64(&selected.lastAccess, c.accessTimeNow())
return selected
}
return nil
}
func (c *keyRangeCache) lookupRangesForState(state *keyRangeCacheState, key, limit []byte) []*cachedRange {
if state == nil {
return nil
}
start, end := findOverlappingPartitionWindow(state.partitions, key, limit)
if start >= len(state.partitions) {
return nil
}
if end <= start {
return state.partitions[start].ranges
}
if end == start+1 {
return state.partitions[start].ranges
}
ranges := uniqueRangesFromPartitions(state.partitions[start:end])
sort.Slice(ranges, func(i, j int) bool {
return bytes.Compare(ranges[i].limitKey, ranges[j].limitKey) < 0
})
return ranges
}
func (c *keyRangeCache) uniformRandom(n int, seed1, seed2, seed3 []byte, deterministic bool) int {
if n <= 1 {
return 0
}
if deterministic {
data := make([]byte, 0, len(seed1)+len(seed2)+len(seed3))
data = append(data, seed1...)
data = append(data, seed2...)
data = append(data, seed3...)
return int(crc32.Checksum(data, crc32cTable) % uint32(n))
}
return rand.Intn(n)
}
func (b *keyRangeCacheStateBuilder) replaceRangeIfNewer(rangeIn *sppb.Range) {
if rangeIn == nil {
return
}
startKey := append([]byte(nil), rangeIn.GetStartKey()...)
limitKey := append([]byte(nil), rangeIn.GetLimitKey()...)
start, end := findOverlappingPartitionWindow(b.partitions, startKey, limitKey)
touchedRanges := uniqueRangesFromPartitions(b.partitions[start:end])
overlappingRanges := make([]*cachedRange, 0)
rebuiltRanges := make([]*cachedRange, 0, len(touchedRanges)+3)
for _, existing := range touchedRanges {
if bytes.Compare(existing.limitKey, startKey) <= 0 || bytes.Compare(existing.startKey, limitKey) >= 0 {
rebuiltRanges = append(rebuiltRanges, existing)
continue
}
cmp := bytes.Compare(rangeIn.GetGeneration(), existing.generation)
if cmp < 0 || (cmp == 0 && bytes.Equal(existing.startKey, startKey) && bytes.Equal(existing.limitKey, limitKey)) {
return
}
overlappingRanges = append(overlappingRanges, existing)
}
b.overlappingRanges += len(overlappingRanges)
if len(overlappingRanges) > 0 {
sort.Slice(overlappingRanges, func(i, j int) bool {
return bytes.Compare(overlappingRanges[i].startKey, overlappingRanges[j].startKey) < 0
})
first := overlappingRanges[0]
if bytes.Compare(first.startKey, startKey) < 0 {
rebuiltRanges = append(rebuiltRanges, &cachedRange{
startKey: append([]byte(nil), first.startKey...),
limitKey: append([]byte(nil), startKey...),
groupUID: first.groupUID,
splitID: first.splitID,
generation: append([]byte(nil), first.generation...),
lastAccess: first.lastAccess,
})
b.rangesInserted++
}
last := overlappingRanges[len(overlappingRanges)-1]
if bytes.Compare(last.limitKey, limitKey) > 0 {
rebuiltRanges = append(rebuiltRanges, &cachedRange{
startKey: append([]byte(nil), limitKey...),
limitKey: append([]byte(nil), last.limitKey...),
groupUID: last.groupUID,
splitID: last.splitID,
generation: append([]byte(nil), last.generation...),
lastAccess: last.lastAccess,
})
b.rangesInserted++
}
b.rangesRemoved += len(overlappingRanges)
}
rebuiltRanges = append(rebuiltRanges, &cachedRange{
startKey: startKey,
limitKey: limitKey,
groupUID: rangeIn.GetGroupUid(),
splitID: rangeIn.GetSplitId(),
generation: append([]byte(nil), rangeIn.GetGeneration()...),
lastAccess: b.cache.accessTimeNow(),
})
b.rangesInserted++
b.rangeCount += len(rebuiltRanges) - len(touchedRanges)
b.replacePartitionWindow(start, end, rebuiltRanges)
}
func (b *keyRangeCacheStateBuilder) findAndRefGroup(groupUID uint64) *cachedGroup {
return b.findGroup(groupUID)
}
func (b *keyRangeCacheStateBuilder) findOrInsertGroup(groupIn *sppb.Group) *cachedGroup {
if groupIn == nil {
return nil
}
groupUID := groupIn.GetGroupUid()
shardIdx := groupShardIndex(groupUID)
b.cloneGroupShard(shardIdx)
group, ok := b.groupShards[shardIdx][groupUID]
if !ok {
group = &cachedGroup{groupUID: groupUID, leaderIdx: -1}
b.groupShards[shardIdx][groupUID] = group
b.mutableGroups[groupUID] = struct{}{}
b.groupCount++
} else if _, mutable := b.mutableGroups[groupUID]; !mutable {
group = cloneCachedGroup(group)
b.groupShards[shardIdx][groupUID] = group
b.mutableGroups[groupUID] = struct{}{}
}
group.update(groupIn)
return group
}
func (b *keyRangeCacheStateBuilder) refGroup(group *cachedGroup) *cachedGroup {
return group
}
func (b *keyRangeCacheStateBuilder) unrefGroup(group *cachedGroup) {
}
func (s *keyRangeCacheState) findGroup(groupUID uint64) *cachedGroup {
if s == nil {
return nil
}
shard := s.groupShards[groupShardIndex(groupUID)]
if len(shard) == 0 {
return nil
}
return shard[groupUID]
}
func (b *keyRangeCacheStateBuilder) findGroup(groupUID uint64) *cachedGroup {
if b == nil {
return nil
}
shard := b.groupShards[groupShardIndex(groupUID)]
if len(shard) == 0 {
return nil
}
return shard[groupUID]
}
func (t *cachedTablet) update(tabletIn *sppb.Tablet) {
if tabletIn == nil {
return
}
if t.tabletUID > 0 && bytes.Compare(t.incarnation, tabletIn.GetIncarnation()) > 0 {
return
}
t.tabletUID = tabletIn.GetTabletUid()
t.incarnation = append([]byte(nil), tabletIn.GetIncarnation()...)
t.distance = tabletIn.GetDistance()
t.skip = tabletIn.GetSkip()
t.role = tabletIn.GetRole()
t.location = tabletIn.GetLocation()
if t.serverAddress != tabletIn.GetServerAddress() {
t.serverAddress = tabletIn.GetServerAddress()
t.storeEndpoint(nil)
}
}
func (t *cachedTablet) loadEndpoint() channelEndpoint {
if t == nil {
return nil
}
ref := t.endpoint.Load()
if ref == nil {
return nil
}
return ref.endpoint
}
func (t *cachedTablet) storeEndpoint(endpoint channelEndpoint) {
if t == nil {
return
}
if endpoint == nil {
t.endpoint.Store(nil)
return
}
t.endpoint.Store(&cachedTabletEndpointRef{endpoint: endpoint})
}
func (t *cachedTablet) clearShutdownEndpoint() channelEndpoint {
endpoint := t.loadEndpoint()
if endpoint == nil {
return nil
}
conn := endpoint.GetConn()
if conn == nil {
return endpoint
}
if conn.GetState() == connectivity.Shutdown {
t.storeEndpoint(nil)
return nil
}
return endpoint
}
func (t *cachedTablet) getOrLoadEndpointIfPresent(endpointCache channelEndpointCache) channelEndpoint {
endpoint := t.clearShutdownEndpoint()
if endpoint != nil || endpointCache == nil {
return endpoint
}
endpoint = endpointCache.GetIfPresent(t.serverAddress)
if endpoint != nil {
t.storeEndpoint(endpoint)
}
return endpoint
}
func (t *cachedTablet) matches(directedReadOptions *sppb.DirectedReadOptions) bool {
if directedReadOptions == nil {
return t.distance <= maxLocalReplicaDistance
}
switch replicas := directedReadOptions.GetReplicas().(type) {
case *sppb.DirectedReadOptions_IncludeReplicas_:
for _, selection := range replicas.IncludeReplicas.GetReplicaSelections() {
if t.matchesReplicaSelection(selection) {
return true
}
}
return false
case *sppb.DirectedReadOptions_ExcludeReplicas_:
for _, selection := range replicas.ExcludeReplicas.GetReplicaSelections() {
if t.matchesReplicaSelection(selection) {
return false
}
}
return true
default:
return t.distance <= maxLocalReplicaDistance
}
}
func (t *cachedTablet) matchesReplicaSelection(selection *sppb.DirectedReadOptions_ReplicaSelection) bool {
if selection == nil {
return true
}
if selection.GetLocation() != "" && selection.GetLocation() != t.location {
return false
}
switch selection.GetType() {
case sppb.DirectedReadOptions_ReplicaSelection_READ_WRITE:
return t.role == sppb.Tablet_READ_WRITE || t.role == sppb.Tablet_ROLE_UNSPECIFIED
case sppb.DirectedReadOptions_ReplicaSelection_READ_ONLY:
return t.role == sppb.Tablet_READ_ONLY
default:
return true
}
}
func (t *cachedTablet) shouldSkip(hint *sppb.RoutingHint) bool {
return t.shouldSkipWithCooldownTracker(hint, nil)
}
func (t *cachedTablet) shouldSkipWithCooldownTracker(hint *sppb.RoutingHint, cooldowns *endpointOverloadCooldownTracker) bool {
if hint == nil {
return true
}
if t.skip || t.serverAddress == "" {
hint.SkippedTabletUid = append(hint.SkippedTabletUid, &sppb.RoutingHint_SkippedTablet{
TabletUid: t.tabletUID,
Incarnation: append([]byte(nil), t.incarnation...),
})
return true
}
if endpoint := t.clearShutdownEndpoint(); endpoint != nil && !endpoint.IsHealthy() {
hint.SkippedTabletUid = append(hint.SkippedTabletUid, &sppb.RoutingHint_SkippedTablet{
TabletUid: t.tabletUID,
Incarnation: append([]byte(nil), t.incarnation...),
})
return true
}
if isEndpointCoolingDown(cooldowns, t.serverAddress) {
return true
}
return false
}
func (t *cachedTablet) shouldSkipForRouting(endpointCache channelEndpointCache, lifecycleManager *endpointLifecycleManager, hint *sppb.RoutingHint, cooldowns *endpointOverloadCooldownTracker, skippedTabletUIDs map[uint64]struct{}, pendingCreations map[string]struct{}, state *routeSelectionState) bool {
if hint == nil {
return true
}
if state != nil {
state.sawMatchingReplica = true
}
if t.skip || t.serverAddress == "" {
if state != nil {
state.sawNonCoolingDownReplica = true
state.hasUnroutableReplica = true
}
t.addSkippedTablet(hint, skippedTabletUIDs)
return true
}
if isEndpointCoolingDown(cooldowns, t.serverAddress) {
if state != nil {
state.sawCoolingDownReplica = true
}
return true
}
if state != nil {
state.sawNonCoolingDownReplica = true
}
endpoint := t.getOrLoadEndpointIfPresent(endpointCache)
if endpoint == nil {
if state != nil {
state.hasUnavailableReplica = true
}
if pendingCreations != nil {
pendingCreations[t.serverAddress] = struct{}{}
if lifecycleManager != nil {
lifecycleManager.requestEndpointRecreation(t.serverAddress)
}
return true
}
if lifecycleManager != nil {
lifecycleManager.requestEndpointRecreation(t.serverAddress)
}
if t.maybeAddRecentTransientFailureSkip(lifecycleManager, hint, skippedTabletUIDs) {
return true
}
return true
}
if endpoint.IsHealthy() {
return false
}
if lifecycleManager != nil {
lifecycleManager.requestEndpointRecreation(t.serverAddress)
}
if endpoint.IsTransientFailure() {
if state != nil {
state.hasUnavailableReplica = true
}
t.addSkippedTablet(hint, skippedTabletUIDs)
return true
}
if state != nil {
state.hasUnavailableReplica = true
}
if t.maybeAddRecentTransientFailureSkip(lifecycleManager, hint, skippedTabletUIDs) {
return true
}
return true
}
func (t *cachedTablet) recordKnownTransientFailure(endpointCache channelEndpointCache, lifecycleManager *endpointLifecycleManager, hint *sppb.RoutingHint, cooldowns *endpointOverloadCooldownTracker, skippedTabletUIDs map[uint64]struct{}) {
if hint == nil || t.skip || t.serverAddress == "" || isEndpointCoolingDown(cooldowns, t.serverAddress) {
return
}
endpoint := t.getOrLoadEndpointIfPresent(endpointCache)
if endpoint != nil && endpoint.IsTransientFailure() {
t.addSkippedTablet(hint, skippedTabletUIDs)
return
}
t.maybeAddRecentTransientFailureSkip(lifecycleManager, hint, skippedTabletUIDs)
}
func (t *cachedTablet) maybeAddRecentTransientFailureSkip(lifecycleManager *endpointLifecycleManager, hint *sppb.RoutingHint, skippedTabletUIDs map[uint64]struct{}) bool {
if lifecycleManager == nil || !lifecycleManager.wasRecentlyEvictedTransientFailure(t.serverAddress) {
return false
}
t.addSkippedTablet(hint, skippedTabletUIDs)
return true
}
func (t *cachedTablet) addSkippedTablet(hint *sppb.RoutingHint, skippedTabletUIDs map[uint64]struct{}) {
if hint == nil {
return
}
if skippedTabletUIDs != nil {
if _, ok := skippedTabletUIDs[t.tabletUID]; ok {
return
}
skippedTabletUIDs[t.tabletUID] = struct{}{}
}
hint.SkippedTabletUid = append(hint.SkippedTabletUid, &sppb.RoutingHint_SkippedTablet{
TabletUid: t.tabletUID,
Incarnation: append([]byte(nil), t.incarnation...),
})
}
func (t *cachedTablet) pick(hint *sppb.RoutingHint) channelEndpoint {
if hint != nil {
hint.TabletUid = t.tabletUID
}
return t.loadEndpoint()
}
func (g *cachedGroup) update(groupIn *sppb.Group) {
if groupIn == nil {
return
}
g.mu.Lock()
defer g.mu.Unlock()
if bytes.Compare(groupIn.GetGeneration(), g.generation) > 0 {
g.generation = append([]byte(nil), groupIn.GetGeneration()...)
if idx := int(groupIn.GetLeaderIndex()); idx >= 0 && idx < len(groupIn.GetTablets()) {
g.leaderIdx = idx
} else {
g.leaderIdx = -1
}
}
if len(g.tablets) == len(groupIn.GetTablets()) {
mismatch := false
for i := range g.tablets {
if g.tablets[i].tabletUID != groupIn.GetTablets()[i].GetTabletUid() {
mismatch = true
break
}
}
if !mismatch {
for i := range g.tablets {
g.tablets[i].update(groupIn.GetTablets()[i])
}
return
}
}
tabletByUID := make(map[uint64]*cachedTablet, len(g.tablets))
for _, tablet := range g.tablets {
tabletByUID[tablet.tabletUID] = tablet
}
newTablets := make([]*cachedTablet, 0, len(groupIn.GetTablets()))
for _, tabletIn := range groupIn.GetTablets() {
tablet := tabletByUID[tabletIn.GetTabletUid()]
if tablet == nil {
tablet = &cachedTablet{}
}
tablet.update(tabletIn)
newTablets = append(newTablets, tablet)
}
g.tablets = newTablets
}
func (g *cachedGroup) hasLeaderLocked() bool {
return g.leaderIdx >= 0 && g.leaderIdx < len(g.tablets)
}
func (g *cachedGroup) leaderLocked() *cachedTablet {
if !g.hasLeaderLocked() {
return nil
}
return g.tablets[g.leaderIdx]
}
func (g *cachedGroup) fillRoutingHint(ctx context.Context, endpointCache channelEndpointCache, preferLeader bool, directedReadOptions *sppb.DirectedReadOptions, hint *sppb.RoutingHint) channelEndpoint {
return g.fillRoutingHintWithCooldownTracker(ctx, endpointCache, nil, false, preferLeader, directedReadOptions, hint, nil)
}
func (g *cachedGroup) fillRoutingHintWithCooldownTracker(ctx context.Context, endpointCache channelEndpointCache, lifecycleManager *endpointLifecycleManager, deterministicRandom bool, preferLeader bool, directedReadOptions *sppb.DirectedReadOptions, hint *sppb.RoutingHint, cooldowns *endpointOverloadCooldownTracker) channelEndpoint {
pendingCreations := make(map[string]struct{})
selected, state := g.fillRoutingHintAttempt(endpointCache, lifecycleManager, deterministicRandom, preferLeader, directedReadOptions, hint, cooldowns, pendingCreations)
if selected != nil {
return selected.pick(hint)
}
if state.allCoolingDown() {
g.mu.RLock()
selected = g.selectCoolingDownTabletLocked(endpointCache, deterministicRandom, preferLeader, directedReadOptions, hint)
if selected != nil {
g.recordKnownTransientFailuresLocked(endpointCache, lifecycleManager, selected, directedReadOptions, hint, cooldowns, skippedTabletUIDsFromHint(hint))
g.mu.RUnlock()
return selected.pick(hint)
}
g.mu.RUnlock()
}
if len(pendingCreations) == 0 || !shouldSynchronouslyWarmEndpoints(endpointCache) {
return nil
}
warmPendingEndpoints(ctx, endpointCache, pendingCreations)
selected, state = g.fillRoutingHintAttempt(endpointCache, lifecycleManager, deterministicRandom, preferLeader, directedReadOptions, hint, cooldowns, nil)
if selected == nil {
if !state.allCoolingDown() {
return nil
}
g.mu.RLock()
selected = g.selectCoolingDownTabletLocked(endpointCache, deterministicRandom, preferLeader, directedReadOptions, hint)
if selected == nil {
g.mu.RUnlock()
return nil
}
g.recordKnownTransientFailuresLocked(endpointCache, lifecycleManager, selected, directedReadOptions, hint, cooldowns, skippedTabletUIDsFromHint(hint))
g.mu.RUnlock()
}
return selected.pick(hint)
}
func shouldSynchronouslyWarmEndpoints(endpointCache channelEndpointCache) bool {
if endpointCache == nil {
return false
}
_, blocksOnGet := endpointCache.(*endpointClientCache)
return !blocksOnGet
}
func (g *cachedGroup) fillRoutingHintAttempt(endpointCache channelEndpointCache, lifecycleManager *endpointLifecycleManager, deterministicRandom bool, preferLeader bool, directedReadOptions *sppb.DirectedReadOptions, hint *sppb.RoutingHint, cooldowns *endpointOverloadCooldownTracker, pendingCreations map[string]struct{}) (*cachedTablet, routeSelectionState) {
g.mu.RLock()
defer g.mu.RUnlock()
if directedReadOptions == nil {
directedReadOptions = &sppb.DirectedReadOptions{}
}
hasDirectedReadOptions := directedReadOptions.GetReplicas() != nil
skippedTabletUIDs := skippedTabletUIDsFromHint(hint)
var state routeSelectionState
if !preferLeader || routingOperationUID(hint) > 0 {
selected := g.selectScoreAwareTabletLocked(endpointCache, lifecycleManager, deterministicRandom, preferLeader, hasDirectedReadOptions, directedReadOptions, hint, cooldowns, skippedTabletUIDs, pendingCreations, &state)
if selected != nil {
g.recordKnownTransientFailuresLocked(endpointCache, lifecycleManager, selected, directedReadOptions, hint, cooldowns, skippedTabletUIDs)
}
return selected, state
}
leader := g.leaderLocked()
if !hasDirectedReadOptions && leader != nil && leader.distance <= maxLocalReplicaDistance && !leader.shouldSkipForRouting(endpointCache, lifecycleManager, hint, cooldowns, skippedTabletUIDs, pendingCreations, &state) {
g.recordKnownTransientFailuresLocked(endpointCache, lifecycleManager, leader, directedReadOptions, hint, cooldowns, skippedTabletUIDs)
return leader, state
}
for _, tablet := range g.tablets {
if !tablet.matches(directedReadOptions) {
continue
}
if tablet.shouldSkipForRouting(endpointCache, lifecycleManager, hint, cooldowns, skippedTabletUIDs, pendingCreations, &state) {
continue
}
g.recordKnownTransientFailuresLocked(endpointCache, lifecycleManager, tablet, directedReadOptions, hint, cooldowns, skippedTabletUIDs)
return tablet, state
}
return nil, state
}
func (g *cachedGroup) selectScoreAwareTabletLocked(endpointCache channelEndpointCache, lifecycleManager *endpointLifecycleManager, deterministicRandom bool, preferLeader bool, hasDirectedReadOptions bool, directedReadOptions *sppb.DirectedReadOptions, hint *sppb.RoutingHint, cooldowns *endpointOverloadCooldownTracker, skippedTabletUIDs map[uint64]struct{}, pendingCreations map[string]struct{}, state *routeSelectionState) *cachedTablet {
preferredLeader := g.localLeaderForScoreBiasLocked(hasDirectedReadOptions)
candidates := make([]eligibleReplica, 0, len(g.tablets))
for _, tablet := range g.tablets {
if !tablet.matches(directedReadOptions) {
continue
}
if tablet.shouldSkipForRouting(endpointCache, lifecycleManager, hint, cooldowns, skippedTabletUIDs, pendingCreations, state) {
continue
}
endpoint := tablet.loadEndpoint()
if endpoint == nil {
continue
}
candidates = append(candidates, eligibleReplica{
tablet: tablet,
endpoint: endpoint,
selectionCost: selectionCostForTablet(routingOperationUID(hint), preferLeader, endpoint, tablet, preferredLeader),
})
}
selected := selectEligibleReplica(candidates, deterministicRandom)
if selected == nil {
return nil
}
return selected.tablet
}
func (g *cachedGroup) selectCoolingDownTabletLocked(endpointCache channelEndpointCache, deterministicRandom bool, preferLeader bool, directedReadOptions *sppb.DirectedReadOptions, hint *sppb.RoutingHint) *cachedTablet {
hasDirectedReadOptions := directedReadOptions != nil && directedReadOptions.GetReplicas() != nil
preferredLeader := g.localLeaderForScoreBiasLocked(hasDirectedReadOptions)
candidates := make([]eligibleReplica, 0, len(g.tablets))
for _, tablet := range g.tablets {
if tablet == nil || !tablet.matches(directedReadOptions) || tablet.skip || tablet.serverAddress == "" {
continue
}
endpoint := tablet.getOrLoadEndpointIfPresent(endpointCache)
if endpoint == nil || !endpoint.IsHealthy() {
continue
}
candidates = append(candidates, eligibleReplica{
tablet: tablet,
endpoint: endpoint,
selectionCost: selectionCostForTablet(routingOperationUID(hint), preferLeader, endpoint, tablet, preferredLeader),
})
}
selected := selectEligibleReplica(candidates, deterministicRandom)
if selected == nil {
return nil
}
return selected.tablet
}
func (g *cachedGroup) localLeaderForScoreBiasLocked(hasDirectedReadOptions bool) *cachedTablet {
leader := g.leaderLocked()
if hasDirectedReadOptions || leader == nil || leader.distance > maxLocalReplicaDistance {
return nil
}
return leader
}
func selectionCostForTablet(operationUID uint64, preferLeader bool, endpoint channelEndpoint, tablet *cachedTablet, preferredLeader *cachedTablet) float64 {
if tablet == nil {
return 0
}
cost := endpointLatencyRegistrySelectionCost(operationUID, preferLeader, endpoint, tablet.serverAddress)
if preferredLeader != nil && tablet == preferredLeader {
return cost * localLeaderSelectionCostBias
}
return cost
}
func selectEligibleReplica(candidates []eligibleReplica, alwaysSelectBest bool) *eligibleReplica {
if len(candidates) == 0 {
return nil
}
if len(candidates) == 1 {
return &candidates[0]
}
if alwaysSelectBest {
best := &candidates[0]
for i := 1; i < len(candidates); i++ {
if candidates[i].selectionCost < best.selectionCost {
best = &candidates[i]
}
}
return best
}
selectedIndex := defaultPowerOfTwoReplicaSelector.chooseIndex(len(candidates), func(index int) float64 {
return candidates[index].selectionCost
})
if selectedIndex < 0 || selectedIndex >= len(candidates) {
return &candidates[0]
}
return &candidates[selectedIndex]
}
func warmPendingEndpoints(ctx context.Context, endpointCache channelEndpointCache, pendingCreations map[string]struct{}) {
if endpointCache == nil || len(pendingCreations) == 0 {
return
}
for address := range pendingCreations {
endpointCache.Get(ctx, address)
}
}
func (g *cachedGroup) recordKnownTransientFailuresLocked(endpointCache channelEndpointCache, lifecycleManager *endpointLifecycleManager, selected *cachedTablet, directedReadOptions *sppb.DirectedReadOptions, hint *sppb.RoutingHint, cooldowns *endpointOverloadCooldownTracker, skippedTabletUIDs map[uint64]struct{}) {
for _, tablet := range g.tablets {
if tablet == selected || !tablet.matches(directedReadOptions) {
continue
}
tablet.recordKnownTransientFailure(endpointCache, lifecycleManager, hint, cooldowns, skippedTabletUIDs)
}
}
func skippedTabletUIDsFromHint(hint *sppb.RoutingHint) map[uint64]struct{} {
if hint == nil || len(hint.GetSkippedTabletUid()) == 0 {
return make(map[uint64]struct{})
}
skippedTabletUIDs := make(map[uint64]struct{}, len(hint.GetSkippedTabletUid()))
for _, skippedTablet := range hint.GetSkippedTabletUid() {
skippedTabletUIDs[skippedTablet.GetTabletUid()] = struct{}{}
}
return skippedTabletUIDs
}