Files
Zach LeslieandGitHub a6f3928b05 Update dskit and adjust memcached interface signature (#5604)
* Update dskit and adjust memcached interface signature

* Generate config manifest
2025-09-03 18:34:46 +02:00

517 lines
16 KiB
Go

package ring
import (
"bytes"
"fmt"
"math"
"math/rand"
"slices"
"strconv"
"strings"
"time"
shardUtil "github.com/grafana/dskit/ring/shard"
)
var ErrNoActivePartitionFound = fmt.Errorf("no active partition found")
// PartitionRing holds an immutable view of the partitions ring.
//
// Design principles:
// - Immutable: the PartitionRingDesc hold by PartitionRing is immutable. When PartitionRingDesc changes
// a new instance of PartitionRing should be created. The partitions ring is expected to change infrequently
// (e.g. there's no heartbeat), so creating a new PartitionRing each time the partitions ring changes is
// not expected to have a significant overhead.
type PartitionRing struct {
// desc is a snapshot of the partition ring. This data is immutable and MUST NOT be modified.
desc PartitionRingDesc
// ringTokens is a sorted list of all tokens registered by all partitions.
ringTokens Tokens
// partitionByToken is a map where they key is a registered token and the value is ID of the partition
// that registered that token.
partitionByToken map[Token]int32
// ownersByPartition is a map where the key is the partition ID and the value is a list of owner IDs.
ownersByPartition map[int32][]string
// shuffleShardCache is used to cache subrings generated with shuffle sharding.
shuffleShardCache *partitionRingShuffleShardCache
// activePartitionsCount is a saved count of active partitions to avoid recomputing it.
activePartitionsCount int
}
func NewPartitionRing(desc PartitionRingDesc) *PartitionRing {
return &PartitionRing{
desc: desc,
ringTokens: desc.tokens(),
partitionByToken: desc.partitionByToken(),
ownersByPartition: desc.ownersByPartition(),
activePartitionsCount: desc.activePartitionsCount(),
shuffleShardCache: newPartitionRingShuffleShardCache(),
}
}
// ActivePartitionForKey returns partition for the given key. Only active partitions are considered.
// Only one partition is returned: in other terms, the replication factor is always 1.
func (r *PartitionRing) ActivePartitionForKey(key uint32) (int32, error) {
var (
start = searchToken(r.ringTokens, key)
iterations = 0
tokensCount = len(r.ringTokens)
)
for i := start; iterations < tokensCount; i++ {
iterations++
if i >= tokensCount {
i %= len(r.ringTokens)
}
token := r.ringTokens[i]
partitionID, ok := r.partitionByToken[Token(token)]
if !ok {
return 0, ErrInconsistentTokensInfo
}
partition, ok := r.desc.Partitions[partitionID]
if !ok {
return 0, ErrInconsistentTokensInfo
}
// If the partition is not active we'll keep walking the ring.
if partition.IsActive() {
return partitionID, nil
}
}
return 0, ErrNoActivePartitionFound
}
// ShuffleShardSize returns number of partitions that would be in the result of ShuffleShard call with the same size.
func (r *PartitionRing) ShuffleShardSize(size int) int {
if size <= 0 || size > r.activePartitionsCount {
return r.activePartitionsCount
}
if size < r.activePartitionsCount {
return size
}
return r.activePartitionsCount
}
// ShuffleShard returns a subring for the provided identifier (eg. a tenant ID)
// and size (number of partitions).
//
// The algorithm used to build the subring is a shuffle sharder based on probabilistic
// hashing. We pick N unique partitions, walking the ring starting from random but
// predictable numbers. The random generator is initialised with a seed based on the
// provided identifier.
//
// This function returns a subring containing ONLY ACTIVE partitions.
//
// This function supports caching.
//
// This implementation guarantees:
//
// - Stability: given the same ring, two invocations returns the same result.
//
// - Consistency: adding/removing 1 partition from the ring generates a resulting
// subring with no more then 1 difference.
//
// - Shuffling: probabilistically, for a large enough cluster each identifier gets a different
// set of instances, with a reduced number of overlapping instances between two identifiers.
func (r *PartitionRing) ShuffleShard(identifier string, size int) (*PartitionRing, error) {
if cached := r.shuffleShardCache.getSubring(identifier, size); cached != nil {
return cached, nil
}
// No need to pass the time if there's no lookback.
subring, err := r.shuffleShard(identifier, size, 0, time.Time{})
if err != nil {
return nil, err
}
r.shuffleShardCache.setSubring(identifier, size, subring)
return subring, nil
}
// ShuffleShardWithLookback is like ShuffleShard() but the returned subring includes all instances
// that have been part of the identifier's shard in [now - lookbackPeriod, now] time window.
//
// This function can return a mix of ACTIVE and INACTIVE partitions. INACTIVE partitions are only
// included if they were part of the identifier's shard within the lookbackPeriod. PENDING partitions
// are never returned.
//
// This function supports caching, but the cache will only be effective if successive calls for the
// same identifier are with the same lookbackPeriod and increasing values of now.
func (r *PartitionRing) ShuffleShardWithLookback(identifier string, size int, lookbackPeriod time.Duration, now time.Time) (*PartitionRing, error) {
if cached := r.shuffleShardCache.getSubringWithLookback(identifier, size, lookbackPeriod, now); cached != nil {
return cached, nil
}
subring, err := r.shuffleShard(identifier, size, lookbackPeriod, now)
if err != nil {
return nil, err
}
r.shuffleShardCache.setSubringWithLookback(identifier, size, lookbackPeriod, now, subring)
return subring, nil
}
func (r *PartitionRing) shuffleShard(identifier string, size int, lookbackPeriod time.Duration, now time.Time) (*PartitionRing, error) {
// If the size is too small or too large, run with a size equal to the total number of partitions.
// We have to run the function anyway because the logic may filter out some INACTIVE partitions.
if size <= 0 || size >= len(r.desc.Partitions) {
size = len(r.desc.Partitions)
}
var lookbackUntil int64
if lookbackPeriod > 0 {
lookbackUntil = now.Add(-lookbackPeriod).Unix()
}
// Initialise the random generator used to select instances in the ring.
// There are no zones
random := rand.New(rand.NewSource(shardUtil.ShuffleShardSeed(identifier, "")))
// To select one more instance while guaranteeing the "consistency" property,
// we do pick a random value from the generator and resolve uniqueness collisions
// (if any) continuing walking the ring.
tokensCount := len(r.ringTokens)
result := make(map[int32]struct{}, size)
exclude := map[int32]struct{}{}
for len(result) < size {
start := searchToken(r.ringTokens, random.Uint32())
iterations := 0
found := false
for p := start; !found && iterations < tokensCount; p++ {
iterations++
// Wrap p around in the ring.
if p >= tokensCount {
p %= tokensCount
}
pid, ok := r.partitionByToken[Token(r.ringTokens[p])]
if !ok {
return nil, ErrInconsistentTokensInfo
}
// Ensure the partition has not already been included or excluded.
if _, ok := result[pid]; ok {
continue
}
if _, ok := exclude[pid]; ok {
continue
}
p, ok := r.desc.Partitions[pid]
if !ok {
return nil, ErrInconsistentTokensInfo
}
// PENDING partitions should be skipped because they're not ready for read or write yet,
// and they don't need to be looked back.
if p.IsPending() {
exclude[pid] = struct{}{}
continue
}
var (
withinLookbackPeriod = lookbackPeriod > 0 && p.GetStateTimestamp() >= lookbackUntil
shouldExtend = withinLookbackPeriod
shouldInclude = p.IsActive() || withinLookbackPeriod
)
// Either include or exclude the found partition.
if shouldInclude {
result[pid] = struct{}{}
} else {
exclude[pid] = struct{}{}
}
// Extend the shard, if requested.
if shouldExtend {
size++
}
// We can stop searching for other partitions only if this partition was included
// and no extension was requested, which means it's the "stop partition" for this cycle.
if shouldInclude && !shouldExtend {
found = true
}
}
// If we iterated over all tokens, and no new partition has been found, we can stop looking for more partitions.
if !found {
break
}
}
return NewPartitionRing(r.desc.WithPartitions(result)), nil
}
// PartitionsCount returns the number of partitions in the ring.
func (r *PartitionRing) PartitionsCount() int {
return len(r.desc.Partitions)
}
// ActivePartitionsCount returns the number of active partitions in the ring.
func (r *PartitionRing) ActivePartitionsCount() int {
return r.activePartitionsCount
}
// Partitions returns the partitions in the ring.
// The returned slice is a deep copy, so the caller can freely manipulate it.
func (r *PartitionRing) Partitions() []PartitionDesc {
res := make([]PartitionDesc, 0, len(r.desc.Partitions))
for _, partition := range r.desc.Partitions {
res = append(res, partition.Clone())
}
return res
}
// PartitionIDs returns a sorted list of all partition IDs in the ring.
// The returned slice is a copy, so the caller can freely manipulate it.
func (r *PartitionRing) PartitionIDs() []int32 {
ids := make([]int32, 0, len(r.desc.Partitions))
for id := range r.desc.Partitions {
ids = append(ids, id)
}
slices.Sort(ids)
return ids
}
// PendingPartitionIDs returns a sorted list of all PENDING partition IDs in the ring.
// The returned slice is a copy, so the caller can freely manipulate it.
func (r *PartitionRing) PendingPartitionIDs() []int32 {
ids := make([]int32, 0, len(r.desc.Partitions))
for id, partition := range r.desc.Partitions {
if partition.IsPending() {
ids = append(ids, id)
}
}
slices.Sort(ids)
return ids
}
// ActivePartitionIDs returns a sorted list of all ACTIVE partition IDs in the ring.
// The returned slice is a copy, so the caller can freely manipulate it.
func (r *PartitionRing) ActivePartitionIDs() []int32 {
ids := make([]int32, 0, len(r.desc.Partitions))
for id, partition := range r.desc.Partitions {
if partition.IsActive() {
ids = append(ids, id)
}
}
slices.Sort(ids)
return ids
}
// InactivePartitionIDs returns a sorted list of all INACTIVE partition IDs in the ring.
// The returned slice is a copy, so the caller can freely manipulate it.
func (r *PartitionRing) InactivePartitionIDs() []int32 {
ids := make([]int32, 0, len(r.desc.Partitions))
for id, partition := range r.desc.Partitions {
if partition.IsInactive() {
ids = append(ids, id)
}
}
slices.Sort(ids)
return ids
}
// PartitionOwnerIDs returns a list of owner IDs for the given partitionID.
// The returned slice is NOT a copy and should be never modified by the caller.
func (r *PartitionRing) PartitionOwnerIDs(partitionID int32) (doNotModify []string) {
return r.ownersByPartition[partitionID]
}
// PartitionOwnerIDsCopy is like PartitionOwnerIDs(), but the returned slice is a copy,
// so the caller can freely manipulate it.
func (r *PartitionRing) PartitionOwnerIDsCopy(partitionID int32) []string {
ids := r.ownersByPartition[partitionID]
if len(ids) == 0 {
return nil
}
return slices.Clone(ids)
}
// MultiPartitionOwnerIDs returns the ownerIDs of the given partitionID removing the suffix added to support ownership of multiple partitions.
// The slice returned will try to use the provided buf, and it can be modified (it will modify the buf if it was used).
func (r *PartitionRing) MultiPartitionOwnerIDs(partitionID int32, buf []string) []string {
ids := r.ownersByPartition[partitionID]
if len(ids) == 0 {
return nil
}
if cap(buf) < len(ids) {
buf = make([]string, len(ids))
} else {
buf = buf[:len(ids)]
}
for i, ownerID := range ids {
if p := strings.LastIndexByte(ownerID, '/'); p != -1 {
buf[i] = ownerID[:p]
} else {
// This isn't expected here: all owner IDs should have a suffix when multiple partitions can be owned.
buf[i] = ownerID
}
}
return buf
}
func (r *PartitionRing) String() string {
buf := bytes.Buffer{}
for pid, pd := range r.desc.Partitions {
buf.WriteString(fmt.Sprintf(" %d:%v", pid, pd.State.String()))
}
return fmt.Sprintf("PartitionRing{ownersCount: %d, partitionsCount: %d, partitions: {%s}}", len(r.desc.Owners), len(r.desc.Partitions), buf.String())
}
// GetTokenRangesForPartition returns token-range owned by given partition. Note that this
// method does NOT take partition state into account, so if only active partitions should be
// considered, then PartitionRing with only active partitions must be created first (e.g. using ShuffleShard method).
func (r *PartitionRing) GetTokenRangesForPartition(partitionID int32) (TokenRanges, error) {
partition, ok := r.desc.Partitions[partitionID]
if !ok {
return nil, ErrPartitionDoesNotExist
}
// 1 range (2 values) per token + one additional if we need to split the rollover range.
ranges := make(TokenRanges, 0, 2*(len(partition.Tokens)+1))
addRange := func(start, end uint32) {
// check if we can group ranges. If so, we just update end of previous range.
if len(ranges) > 0 && ranges[len(ranges)-1] == start-1 {
ranges[len(ranges)-1] = end
} else {
ranges = append(ranges, start, end)
}
}
// "last" range is range that includes token math.MaxUint32.
ownsLastRange := false
startOfLastRange := uint32(0)
// We start with all tokens, but will remove tokens we already skipped, to let binary search do less work.
ringTokens := r.ringTokens
for iter, t := range partition.Tokens {
lastOwnedToken := t - 1
ix := searchToken(ringTokens, lastOwnedToken)
prevIx := ix - 1
if prevIx < 0 {
// We can only find "last" range during first iteration.
if iter > 0 {
return nil, ErrInconsistentTokensInfo
}
prevIx = len(ringTokens) - 1
ownsLastRange = true
startOfLastRange = ringTokens[prevIx]
// We can only claim token 0 if our actual token in the ring (which is exclusive end of range) was not 0.
if t > 0 {
addRange(0, lastOwnedToken)
}
} else {
addRange(ringTokens[prevIx], lastOwnedToken)
}
// Reduce number of tokens we need to search through. We keep current token to serve as min boundary for next search,
// to make sure we don't find another "last" range (where prevIx < 0).
ringTokens = ringTokens[ix:]
}
if ownsLastRange {
addRange(startOfLastRange, math.MaxUint32)
}
return ranges, nil
}
// ActivePartitionBatchRing wraps PartitionRing and implements DoBatchRing to lookup ACTIVE partitions.
type ActivePartitionBatchRing struct {
ring *PartitionRing
}
func NewActivePartitionBatchRing(ring *PartitionRing) *ActivePartitionBatchRing {
return &ActivePartitionBatchRing{
ring: ring,
}
}
// InstancesCount returns the number of active partitions in the ring.
//
// InstancesCount implements DoBatchRing.InstancesCount.
func (r *ActivePartitionBatchRing) InstancesCount() int {
return r.ring.ActivePartitionsCount()
}
// ReplicationFactor returns 1 as partitions replication factor: an entry (looked by key via Get())
// is always stored in 1 and only 1 partition.
//
// ReplicationFactor implements DoBatchRing.ReplicationFactor.
func (r *ActivePartitionBatchRing) ReplicationFactor() int {
return 1
}
// Get implements DoBatchRing.Get.
func (r *ActivePartitionBatchRing) Get(key uint32, _ Operation, bufInstances []InstanceDesc, _, _ []string) (ReplicationSet, error) {
partitionID, err := r.ring.ActivePartitionForKey(key)
if err != nil {
return ReplicationSet{}, err
}
// Ensure we have enough capacity in bufInstances.
if cap(bufInstances) < 1 {
bufInstances = []InstanceDesc{{}}
} else {
bufInstances = bufInstances[:1]
}
partitionIDString := strconv.Itoa(int(partitionID))
bufInstances[0] = InstanceDesc{
Addr: partitionIDString,
Timestamp: 0,
State: ACTIVE,
Id: partitionIDString,
}
return ReplicationSet{
Instances: bufInstances,
MaxErrors: 0,
MaxUnavailableZones: 0,
ZoneAwarenessEnabled: false,
}, nil
}
func multiPartitionOwnerInstanceID(instanceID string, partitionID int32) string {
return instanceID + "/" + strconv.Itoa(int(partitionID))
}