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
649 lines
23 KiB
Go
649 lines
23 KiB
Go
package ring
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
"sync"
|
|
"time"
|
|
|
|
kitlog "github.com/go-kit/log"
|
|
"github.com/go-kit/log/level"
|
|
|
|
"github.com/grafana/dskit/cancellation"
|
|
"github.com/grafana/dskit/spanlogger"
|
|
)
|
|
|
|
// Used to communicate, via a context, the number of replicas that are available for a DoUntilQuorum call.
|
|
type availableReplicasContextKey struct{}
|
|
|
|
// ReplicationSet describes the instances to talk to for a given key, and how
|
|
// many errors to tolerate.
|
|
type ReplicationSet struct {
|
|
Instances []InstanceDesc
|
|
|
|
// Maximum number of tolerated failing instances. Max errors and max unavailable zones are
|
|
// mutually exclusive.
|
|
MaxErrors int
|
|
|
|
// Maximum number of different zones in which instances can fail. Max unavailable zones and
|
|
// max errors are mutually exclusive.
|
|
MaxUnavailableZones int
|
|
|
|
ZoneAwarenessEnabled bool
|
|
}
|
|
|
|
// Do function f in parallel for all replicas in the set, erroring if we exceed
|
|
// MaxErrors and returning early otherwise.
|
|
// Return a slice of all results from f, or nil if an error occurred.
|
|
func (r ReplicationSet) Do(ctx context.Context, delay time.Duration, f func(context.Context, *InstanceDesc) (interface{}, error)) ([]interface{}, error) {
|
|
// Initialise the result tracker, which is use to keep track of successes and failures.
|
|
var tracker replicationSetResultTracker
|
|
if r.MaxUnavailableZones > 0 {
|
|
tracker = newZoneAwareResultTracker(r.Instances, r.MaxUnavailableZones, nil, kitlog.NewNopLogger())
|
|
} else {
|
|
tracker = newDefaultResultTracker(r.Instances, r.MaxErrors, kitlog.NewNopLogger())
|
|
}
|
|
|
|
var (
|
|
ch = make(chan instanceResult[any], len(r.Instances))
|
|
forceStart = make(chan struct{}, r.MaxErrors)
|
|
)
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
|
|
// Spawn a goroutine for each instance.
|
|
for i := range r.Instances {
|
|
go func(i int, ing *InstanceDesc) {
|
|
// Wait to send extra requests. Works only when zone-awareness is disabled.
|
|
if delay > 0 && r.MaxUnavailableZones == 0 && i >= len(r.Instances)-r.MaxErrors {
|
|
after := time.NewTimer(delay)
|
|
defer after.Stop()
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-forceStart:
|
|
case <-after.C:
|
|
}
|
|
}
|
|
result, err := f(ctx, ing)
|
|
ch <- instanceResult[any]{
|
|
result: result,
|
|
err: err,
|
|
instance: ing,
|
|
}
|
|
}(i, &r.Instances[i])
|
|
}
|
|
|
|
results := make([]interface{}, 0, len(r.Instances))
|
|
|
|
for !tracker.succeeded() {
|
|
select {
|
|
case res := <-ch:
|
|
tracker.done(res.instance, res.err)
|
|
if res.err != nil {
|
|
if tracker.failed() {
|
|
return nil, res.err
|
|
}
|
|
|
|
// force one of the delayed requests to start
|
|
if delay > 0 && r.MaxUnavailableZones == 0 {
|
|
forceStart <- struct{}{}
|
|
}
|
|
} else {
|
|
results = append(results, res.result)
|
|
}
|
|
|
|
case <-ctx.Done():
|
|
return nil, ctx.Err()
|
|
}
|
|
}
|
|
|
|
return results, nil
|
|
}
|
|
|
|
type DoUntilQuorumConfig struct {
|
|
// MinimizeRequests enables request minimization.
|
|
// See docs for DoUntilQuorum for more information.
|
|
MinimizeRequests bool
|
|
|
|
// HedgingDelay configures the delay used before initiating hedged requests.
|
|
// Hedging is only enabled if HedgingDelay is non-zero and MinimizeRequests is true.
|
|
// See docs for DoUntilQuorum for more information.
|
|
HedgingDelay time.Duration
|
|
|
|
// Logger to emit log lines and span events to during the call.
|
|
// Can be nil, in which case no log lines or span events are emitted.
|
|
Logger *spanlogger.SpanLogger
|
|
|
|
// IsTerminalError allows DoUntilQuorum to detect terminal errors generated by requests.
|
|
//
|
|
// If IsTerminalError is non-nil and a request returns an error for which IsTerminalError returns true,
|
|
// DoUntilQuorum will immediately cancel any inflight requests and return the error.
|
|
//
|
|
// This is useful to cancel DoUntilQuorum when an unrecoverable error occurs and it does not
|
|
// make sense to attempt requests to other instances. For example, if a client-side limit on the
|
|
// total response size across all instances is reached, making further requests to other
|
|
// instances would not be worthwhile.
|
|
IsTerminalError func(error) bool
|
|
|
|
// ZoneSorter orders the provided zones in preference order, for use when MinimizeRequests is true
|
|
// and DoUntilQuorum is operating in zone-aware mode. If not set, zones will be used in a
|
|
// randomly-selected order.
|
|
//
|
|
// Earlier zones will be used first.
|
|
// The function can modify the provided slice of zones in place.
|
|
// All provided zones must be returned exactly once.
|
|
//
|
|
// This can be used to prioritise zones that are more likely to succeed, or are expected to complete
|
|
// faster, for example.
|
|
ZoneSorter ZoneSorter
|
|
|
|
// IncludeReplicaCount indicates that we should unclude in a DoUntilQuorum call, as part of the context, a count of the
|
|
// number of available replicas. This can be propagated to a replica and used to make informed decisions about load
|
|
// shedding.
|
|
IncludeReplicaCount bool
|
|
}
|
|
|
|
func (c DoUntilQuorumConfig) Validate() error {
|
|
if c.HedgingDelay < 0 {
|
|
return errors.New("invalid DoUntilQuorumConfig: HedgingDelay must be non-negative")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// DoUntilQuorum runs function f in parallel for all replicas in r.
|
|
//
|
|
// # Result selection
|
|
//
|
|
// If r.MaxUnavailableZones is greater than zero, or r.ZoneAwarenessEnabled is true, DoUntilQuorum operates in zone-aware mode:
|
|
// - DoUntilQuorum returns an error if calls to f for instances in more than r.MaxUnavailableZones zones return errors
|
|
// - Otherwise, DoUntilQuorum returns all results from all replicas in the first zones for which f succeeds
|
|
// for every instance in that zone (eg. if there are 3 zones and r.MaxUnavailableZones is 1, DoUntilQuorum will
|
|
// return the results from all instances in 2 zones, even if all calls to f succeed).
|
|
//
|
|
// Otherwise, DoUntilQuorum operates in non-zone-aware mode:
|
|
// - DoUntilQuorum returns an error if more than r.MaxErrors calls to f return errors
|
|
// - Otherwise, DoUntilQuorum returns all results from the first len(r.Instances) - r.MaxErrors instances
|
|
// (eg. if there are 6 replicas and r.MaxErrors is 2, DoUntilQuorum will return the results from the first 4
|
|
// successful calls to f, even if all 6 calls to f succeed).
|
|
//
|
|
// # Request minimization
|
|
//
|
|
// cfg.MinimizeRequests enables or disables request minimization.
|
|
//
|
|
// Regardless of the value of cfg.MinimizeRequests, if one of the termination conditions above is satisfied or ctx is
|
|
// cancelled before f is called for an instance, f may not be called for that instance at all.
|
|
//
|
|
// ## When disabled
|
|
//
|
|
// If request minimization is disabled, DoUntilQuorum will call f for each instance in r. The value of cfg.HedgingDelay
|
|
// is ignored.
|
|
//
|
|
// ## When enabled
|
|
//
|
|
// If request minimization is enabled, DoUntilQuorum will initially call f for the minimum number of instances needed to
|
|
// reach the termination conditions above, and later call f for further instances if required. For example, if
|
|
// r.MaxUnavailableZones is 1 and there are three zones, DoUntilQuorum will initially only call f for instances in two
|
|
// zones, and only call f for instances in the remaining zone if a request in the initial two zones fails.
|
|
//
|
|
// If cfg.ZoneSorter is non-nil and DoUntilQuorum is operating in zone-aware mode, DoUntilQuorum will initiate requests
|
|
// to zones in the order returned by the sorter.
|
|
//
|
|
// If cfg.ZoneSorter is nil, or DoUntilQuorum is operating in non-zone-aware mode, DoUntilQuorum will randomly select
|
|
// available zones / instances such that calling DoUntilQuorum multiple times with the same ReplicationSet should evenly
|
|
// distribute requests across all zones / instances.
|
|
//
|
|
// If cfg.HedgingDelay is non-zero, DoUntilQuorum will call f for an additional zone's instances (if zone-aware) / an
|
|
// additional instance (if not zone-aware) every cfg.HedgingDelay until one of the termination conditions above is
|
|
// reached. For example, if r.MaxUnavailableZones is 2, cfg.HedgingDelay is 4 seconds and there are fives zones,
|
|
// DoUntilQuorum will initially only call f for instances in three zones, and unless one of the termination conditions
|
|
// is reached earlier, will then call f for instances in a fourth zone approximately 4 seconds later, and then call f
|
|
// for instances in the final zone approximately 4 seconds after that (ie. roughly 8 seconds since the call to
|
|
// DoUntilQuorum began).
|
|
//
|
|
// # Cleanup
|
|
//
|
|
// Any results from successful calls to f that are not returned by DoUntilQuorum will be passed to cleanupFunc,
|
|
// including when DoUntilQuorum returns an error or only returns a subset of successful results. cleanupFunc may
|
|
// be called both before and after DoUntilQuorum returns.
|
|
//
|
|
// A call to f is considered successful if it returns a nil error.
|
|
//
|
|
// # Contexts
|
|
//
|
|
// The context.Context passed to an invocation of f may be cancelled at any time if the result of that invocation of
|
|
// f will not be used.
|
|
//
|
|
// DoUntilQuorum cancels the context.Context passed to each invocation of f before DoUntilQuorum returns.
|
|
func DoUntilQuorum[T any](ctx context.Context, r ReplicationSet, cfg DoUntilQuorumConfig, f func(context.Context, *InstanceDesc) (T, error), cleanupFunc func(T)) ([]T, error) {
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
|
|
wrappedF := func(ctx context.Context, desc *InstanceDesc, _ context.CancelCauseFunc) (T, error) {
|
|
return f(ctx, desc)
|
|
}
|
|
|
|
return DoUntilQuorumWithoutSuccessfulContextCancellation(ctx, r, cfg, wrappedF, cleanupFunc)
|
|
}
|
|
|
|
// DoUntilQuorumWithoutSuccessfulContextCancellation behaves the same as DoUntilQuorum, except it does not cancel
|
|
// the context.Context passed to invocations of f whose results are returned.
|
|
//
|
|
// For example, this is useful in situations where DoUntilQuorumWithoutSuccessfulContextCancellation is used
|
|
// to establish a set of streams that will be used after DoUntilQuorumWithoutSuccessfulContextCancellation returns.
|
|
//
|
|
// It is the caller's responsibility to ensure that either of the following are eventually true:
|
|
// - ctx is cancelled, or
|
|
// - the corresponding context.CancelFunc is called for all invocations of f whose results are returned by
|
|
// DoUntilQuorumWithoutSuccessfulContextCancellation
|
|
//
|
|
// Failing to do this may result in a memory leak.
|
|
func DoUntilQuorumWithoutSuccessfulContextCancellation[T any](ctx context.Context, r ReplicationSet, cfg DoUntilQuorumConfig, f func(context.Context, *InstanceDesc, context.CancelCauseFunc) (T, error), cleanupFunc func(T)) ([]T, error) {
|
|
if err := cfg.Validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if r.ZoneAwarenessEnabled && r.MaxErrors > 0 {
|
|
return nil, fmt.Errorf("invalid ReplicationSet: MaxErrors is non-zero (is %v) and ZoneAwarenessEnabled is true", r.MaxErrors)
|
|
}
|
|
|
|
var logger kitlog.Logger = cfg.Logger
|
|
if cfg.Logger == nil {
|
|
logger = kitlog.NewNopLogger()
|
|
}
|
|
|
|
if cfg.IncludeReplicaCount {
|
|
ctx = ContextWithAvailableReplicas(ctx, len(r.Instances))
|
|
}
|
|
|
|
resultsChan := make(chan instanceResult[T], len(r.Instances))
|
|
resultsRemaining := len(r.Instances)
|
|
|
|
defer func() {
|
|
go func() {
|
|
for resultsRemaining > 0 {
|
|
result := <-resultsChan
|
|
resultsRemaining--
|
|
|
|
if result.err == nil {
|
|
cleanupFunc(result.result)
|
|
}
|
|
}
|
|
}()
|
|
}()
|
|
|
|
var resultTracker replicationSetResultTracker
|
|
var contextTracker replicationSetContextTracker
|
|
if r.MaxUnavailableZones > 0 || r.ZoneAwarenessEnabled {
|
|
resultTracker = newZoneAwareResultTracker(r.Instances, r.MaxUnavailableZones, cfg.ZoneSorter, logger)
|
|
contextTracker = newZoneAwareContextTracker(ctx, r.Instances)
|
|
} else {
|
|
resultTracker = newDefaultResultTracker(r.Instances, r.MaxErrors, logger)
|
|
contextTracker = newDefaultContextTracker(ctx, r.Instances)
|
|
}
|
|
|
|
if cfg.MinimizeRequests {
|
|
resultTracker.startMinimumRequests()
|
|
} else {
|
|
resultTracker.startAllRequests()
|
|
}
|
|
|
|
for i := range r.Instances {
|
|
instance := &r.Instances[i]
|
|
ctx, ctxCancel := contextTracker.contextFor(instance)
|
|
|
|
go func(desc *InstanceDesc) {
|
|
if err := resultTracker.awaitStart(ctx, desc); err != nil {
|
|
// Post to resultsChan so that the deferred cleanup handler above eventually terminates.
|
|
resultsChan <- instanceResult[T]{
|
|
err: err,
|
|
instance: desc,
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
result, err := f(ctx, desc, ctxCancel)
|
|
resultsChan <- instanceResult[T]{
|
|
result: result,
|
|
err: err,
|
|
instance: desc,
|
|
}
|
|
}(instance)
|
|
}
|
|
|
|
resultsMap := make(map[*InstanceDesc]T, len(r.Instances))
|
|
cleanupResultsAlreadyReceived := func() {
|
|
for _, result := range resultsMap {
|
|
cleanupFunc(result)
|
|
}
|
|
}
|
|
|
|
terminate := func(err error, cause string) ([]T, error) {
|
|
if cfg.Logger != nil && !errors.Is(err, context.Canceled) { // Cancellation is not an error.
|
|
cfg.Logger.SetError()
|
|
}
|
|
|
|
contextTracker.cancelAllContexts(cancellation.NewError(errors.New(cause)))
|
|
cleanupResultsAlreadyReceived()
|
|
return nil, err
|
|
}
|
|
|
|
var hedgingTrigger <-chan time.Time
|
|
|
|
if cfg.HedgingDelay > 0 {
|
|
ticker := time.NewTicker(cfg.HedgingDelay)
|
|
defer ticker.Stop()
|
|
hedgingTrigger = ticker.C
|
|
}
|
|
|
|
for !resultTracker.succeeded() {
|
|
select {
|
|
case <-ctx.Done():
|
|
err := context.Cause(ctx)
|
|
level.Debug(logger).Log("msg", "parent context done, returning", "err", err)
|
|
|
|
// No need to cancel individual instance contexts, as they inherit the cancellation from ctx.
|
|
cleanupResultsAlreadyReceived()
|
|
|
|
return nil, err
|
|
case <-hedgingTrigger:
|
|
resultTracker.startAdditionalRequests()
|
|
case result := <-resultsChan:
|
|
resultsRemaining--
|
|
|
|
if result.err != nil && cfg.IsTerminalError != nil && cfg.IsTerminalError(result.err) {
|
|
level.Warn(logger).Log("msg", "cancelling all outstanding requests because a terminal error occurred", "err", result.err)
|
|
// We must return before calling resultTracker.done() below, otherwise done() might start further requests if request minimisation is enabled.
|
|
return terminate(result.err, "a terminal error occurred")
|
|
}
|
|
|
|
resultTracker.done(result.instance, result.err)
|
|
|
|
if result.err == nil {
|
|
resultsMap[result.instance] = result.result
|
|
} else {
|
|
contextTracker.cancelContextFor(result.instance, cancellation.NewErrorf("this instance returned an error: %w", result.err))
|
|
|
|
if resultTracker.failed() {
|
|
level.Error(logger).Log("msg", "cancelling all outstanding requests because quorum cannot be reached")
|
|
return terminate(result.err, "quorum cannot be reached")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
level.Debug(logger).Log("msg", "quorum reached")
|
|
|
|
results := make([]T, 0, len(r.Instances))
|
|
|
|
for i := range r.Instances {
|
|
instance := &r.Instances[i]
|
|
result, haveResult := resultsMap[instance]
|
|
|
|
if haveResult {
|
|
if resultTracker.shouldIncludeResultFrom(instance) {
|
|
results = append(results, result)
|
|
} else {
|
|
contextTracker.cancelContextFor(instance, cancellation.NewErrorf("quorum reached, result not required from this instance"))
|
|
cleanupFunc(result)
|
|
}
|
|
} else {
|
|
// Nothing to clean up (yet) - this will be handled by deferred call above.
|
|
contextTracker.cancelContextFor(instance, cancellation.NewErrorf("quorum reached, result not required from this instance"))
|
|
}
|
|
}
|
|
|
|
return results, nil
|
|
}
|
|
|
|
// DoMultiUntilQuorumWithoutSuccessfulContextCancellation behaves similar to DoUntilQuorumWithoutSuccessfulContextCancellation
|
|
// with the following exceptions:
|
|
//
|
|
// - This function calls DoUntilQuorumWithoutSuccessfulContextCancellation for each input ReplicationSet and requires
|
|
// DoUntilQuorumWithoutSuccessfulContextCancellation to successfully run for each of them. Execution breaks on the
|
|
// first error returned by DoUntilQuorumWithoutSuccessfulContextCancellation on any ReplicationSet.
|
|
//
|
|
// - This function requires that the callback function f always call context.CancelCauseFunc once done. Failing to
|
|
// cancel the context will leak resources.
|
|
func DoMultiUntilQuorumWithoutSuccessfulContextCancellation[T any](ctx context.Context, sets []ReplicationSet, cfg DoUntilQuorumConfig, f func(context.Context, *InstanceDesc, context.CancelCauseFunc) (T, error), cleanupFunc func(T)) ([]T, error) {
|
|
if len(sets) == 0 {
|
|
return nil, errors.New("no replication sets")
|
|
}
|
|
if len(sets) == 1 {
|
|
return DoUntilQuorumWithoutSuccessfulContextCancellation[T](ctx, sets[0], cfg, f, cleanupFunc)
|
|
}
|
|
|
|
results, _, err := doMultiUntilQuorumWithoutSuccessfulContextCancellation[T](ctx, sets, cfg, f, cleanupFunc)
|
|
return results, err
|
|
}
|
|
|
|
// See DoMultiUntilQuorumWithoutSuccessfulContextCancellation().
|
|
//
|
|
// The returned context.Context is the internal context used by workers and it's used for testing purposes.
|
|
func doMultiUntilQuorumWithoutSuccessfulContextCancellation[T any](ctx context.Context, sets []ReplicationSet, cfg DoUntilQuorumConfig, f func(context.Context, *InstanceDesc, context.CancelCauseFunc) (T, error), cleanupFunc func(T)) ([]T, context.Context, error) {
|
|
var (
|
|
returnResultsMx = sync.Mutex{}
|
|
returnResults = make([]T, 0, len(sets)*len(sets[0].Instances)) // Assume all replication sets have the same number of instances.
|
|
|
|
returnErrOnce sync.Once
|
|
returnErr error // The first error occurred.
|
|
|
|
workersGroup = sync.WaitGroup{}
|
|
workersCtx, cancelWorkersCtx = context.WithCancelCause(ctx)
|
|
|
|
inflightTracker = newInflightInstanceTracker(sets)
|
|
)
|
|
|
|
cancelWorkersCtxIfSafe := func() {
|
|
if inflightTracker.allInstancesCompleted() {
|
|
cancelWorkersCtx(errors.New("all requests completed"))
|
|
}
|
|
}
|
|
|
|
// Start a worker for each set. A worker is responsible to call DoUntilQuorumWithoutSuccessfulContextCancellation()
|
|
// for the given replication set and handle the result.
|
|
workersGroup.Add(len(sets))
|
|
|
|
for idx, set := range sets {
|
|
go func(idx int, set ReplicationSet) {
|
|
defer workersGroup.Done()
|
|
|
|
wrappedFn := func(ctx context.Context, instance *InstanceDesc, cancelCtx context.CancelCauseFunc) (T, error) {
|
|
// The callback function has been called, so we need to track it.
|
|
inflightTracker.addInstance(idx, instance)
|
|
|
|
// Inject custom logic in the context.CancelCauseFunc.
|
|
return f(ctx, instance, func(cause error) {
|
|
// Call the original one.
|
|
cancelCtx(cause)
|
|
|
|
// The callback has done, so we can remove it from tracker and then check if it's safe
|
|
// to cancel the workers context.
|
|
inflightTracker.removeInstance(idx, instance)
|
|
cancelWorkersCtxIfSafe()
|
|
})
|
|
}
|
|
|
|
setResults, setErr := DoUntilQuorumWithoutSuccessfulContextCancellation[T](workersCtx, set, cfg, wrappedFn, cleanupFunc)
|
|
|
|
if setErr != nil {
|
|
returnErrOnce.Do(func() {
|
|
returnErr = setErr
|
|
|
|
// Interrupt the execution of all workers.
|
|
cancelWorkersCtx(setErr)
|
|
})
|
|
|
|
return
|
|
}
|
|
|
|
// Keep track of the results.
|
|
returnResultsMx.Lock()
|
|
returnResults = append(returnResults, setResults...)
|
|
returnResultsMx.Unlock()
|
|
}(idx, set)
|
|
}
|
|
|
|
// Wait until all goroutines have terminated.
|
|
workersGroup.Wait()
|
|
|
|
// All workers completed, so it's guaranteed returnResults and returnErr won't be accessed by workers anymore,
|
|
// and it's safe to read them with no locking.
|
|
if returnErr != nil {
|
|
return nil, workersCtx, returnErr
|
|
}
|
|
|
|
// No error occurred. It means workers context hasn't been canceled yet, and we don't expect more callbacks
|
|
// to get tracked, so we can check if the cancelling condition has already been reached and eventually do it.
|
|
inflightTracker.allInstancesAdded()
|
|
cancelWorkersCtxIfSafe()
|
|
|
|
return returnResults, workersCtx, nil
|
|
}
|
|
|
|
type instanceResult[T any] struct {
|
|
result T
|
|
err error
|
|
instance *InstanceDesc
|
|
}
|
|
|
|
// Includes returns whether the replication set includes the replica with the provided addr.
|
|
func (r ReplicationSet) Includes(addr string) bool {
|
|
for _, instance := range r.Instances {
|
|
if instance.GetAddr() == addr {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// GetIDs returns the IDs of all instances within the replication set. Returned slice
|
|
// order is not guaranteed.
|
|
func (r ReplicationSet) GetIDs() []string {
|
|
ids := make([]string, 0, len(r.Instances))
|
|
for _, desc := range r.Instances {
|
|
ids = append(ids, desc.Id)
|
|
}
|
|
return ids
|
|
}
|
|
|
|
// GetAddresses returns the addresses of all instances within the replication set. Returned slice
|
|
// order is not guaranteed.
|
|
func (r ReplicationSet) GetAddresses() []string {
|
|
addrs := make([]string, 0, len(r.Instances))
|
|
for _, desc := range r.Instances {
|
|
addrs = append(addrs, desc.Addr)
|
|
}
|
|
return addrs
|
|
}
|
|
|
|
// GetAddressesWithout returns the addresses of all instances within the replication set while
|
|
// excluding the specified address. Returned slice order is not guaranteed.
|
|
func (r ReplicationSet) GetAddressesWithout(exclude string) []string {
|
|
addrs := make([]string, 0, len(r.Instances))
|
|
for _, desc := range r.Instances {
|
|
if desc.Addr != exclude {
|
|
addrs = append(addrs, desc.Addr)
|
|
}
|
|
}
|
|
return addrs
|
|
}
|
|
|
|
// ZoneCount returns the number of unique zones represented by instances within this replication set.
|
|
func (r *ReplicationSet) ZoneCount() int {
|
|
// Why not use a map here? Using a slice is faster for the small number of zones we expect to typically use.
|
|
zones := []string{}
|
|
|
|
for _, i := range r.Instances {
|
|
sawZone := false
|
|
|
|
for _, z := range zones {
|
|
if z == i.Zone {
|
|
sawZone = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !sawZone {
|
|
zones = append(zones, i.Zone)
|
|
}
|
|
}
|
|
|
|
return len(zones)
|
|
}
|
|
|
|
// HasReplicationSetChanged returns false if two replications sets are the same (with possibly different timestamps),
|
|
// true if they differ in any way (number of instances, instance states, tokens, zones, ...).
|
|
func HasReplicationSetChanged(before, after ReplicationSet) bool {
|
|
return hasReplicationSetChangedExcluding(before, after, func(i *InstanceDesc) {
|
|
i.Timestamp = 0
|
|
})
|
|
}
|
|
|
|
// HasReplicationSetChangedWithoutState returns false if two replications sets
|
|
// are the same (with possibly different timestamps and instance states),
|
|
// true if they differ in any other way (number of instances, tokens, zones, ...).
|
|
func HasReplicationSetChangedWithoutState(before, after ReplicationSet) bool {
|
|
return hasReplicationSetChangedExcluding(before, after, func(i *InstanceDesc) {
|
|
i.Timestamp = 0
|
|
i.State = PENDING
|
|
})
|
|
}
|
|
|
|
// Has HasReplicationSetChangedWithoutStateOrAddr returns false if two replications sets
|
|
// are the same (with possibly different timestamps, instance states, and ip addresses),
|
|
// true if they differ in any other way (number of instances, tokens, zones, ...).
|
|
func HasReplicationSetChangedWithoutStateOrAddr(before, after ReplicationSet) bool {
|
|
return hasReplicationSetChangedExcluding(before, after, func(i *InstanceDesc) {
|
|
i.Timestamp = 0
|
|
i.State = PENDING
|
|
i.Addr = ""
|
|
})
|
|
}
|
|
|
|
// Do comparison of replicasets, but apply a function first
|
|
// to be able to exclude (reset) some values
|
|
func hasReplicationSetChangedExcluding(before, after ReplicationSet, exclude func(*InstanceDesc)) bool {
|
|
beforeInstances := before.Instances
|
|
afterInstances := after.Instances
|
|
|
|
if len(beforeInstances) != len(afterInstances) {
|
|
return true
|
|
}
|
|
|
|
sort.Sort(ByID(beforeInstances))
|
|
sort.Sort(ByID(afterInstances))
|
|
|
|
for i := 0; i < len(beforeInstances); i++ {
|
|
b := beforeInstances[i]
|
|
a := afterInstances[i]
|
|
|
|
exclude(&a)
|
|
exclude(&b)
|
|
|
|
if !b.Equal(a) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// ContextWithAvailableReplicas returns a new context that can be used as part of a DoUntilQuorum call, with the number
|
|
// of availableReplicas set. This can be later checked via GetAvailableReplicas.
|
|
func ContextWithAvailableReplicas(ctx context.Context, availableReplicas int) context.Context {
|
|
return context.WithValue(ctx, availableReplicasContextKey{}, availableReplicas)
|
|
}
|
|
|
|
// GetAvailableReplicas returns the available replicas count from the context, if any, along with an ok flag indicating
|
|
// if the availableReplicas was set.
|
|
func GetAvailableReplicas(ctx context.Context) (int, bool) {
|
|
result, ok := ctx.Value(availableReplicasContextKey{}).(int)
|
|
return result, ok
|
|
}
|