mirror of
https://github.com/hanzoai/traces.git
synced 2026-10-30 18:20:41 +00:00
* Update dskit and adjust memcached interface signature * Generate config manifest
517 lines
16 KiB
Go
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))
|
|
}
|