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
426 lines
14 KiB
Go
426 lines
14 KiB
Go
package ring
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
|
|
"github.com/go-kit/log"
|
|
"github.com/go-kit/log/level"
|
|
"github.com/pkg/errors"
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
"github.com/prometheus/client_golang/prometheus/promauto"
|
|
"go.uber.org/atomic"
|
|
|
|
"github.com/grafana/dskit/kv"
|
|
"github.com/grafana/dskit/services"
|
|
)
|
|
|
|
var (
|
|
ErrPartitionDoesNotExist = errors.New("the partition does not exist")
|
|
ErrPartitionStateMismatch = errors.New("the partition state does not match the expected one")
|
|
ErrPartitionStateChangeNotAllowed = errors.New("partition state change not allowed")
|
|
|
|
allowedPartitionStateChanges = map[PartitionState][]PartitionState{
|
|
PartitionPending: {PartitionActive, PartitionInactive},
|
|
PartitionActive: {PartitionInactive},
|
|
PartitionInactive: {PartitionActive},
|
|
}
|
|
)
|
|
|
|
type PartitionInstanceLifecyclerConfig struct {
|
|
// PartitionID is the ID of the partition managed by the lifecycler.
|
|
PartitionID int32
|
|
|
|
// InstanceID is the ID of the instance managed by the lifecycler.
|
|
InstanceID string
|
|
|
|
// MultiPartitionOwnership is the flag that indicates whether the lifecycler should allow owning multiple partitions.
|
|
// This changes the InstanceID used to register the owner in the ring.
|
|
MultiPartitionOwnership bool
|
|
|
|
// WaitOwnersCountOnPending is the minimum number of owners to wait before switching a
|
|
// PENDING partition to ACTIVE.
|
|
WaitOwnersCountOnPending int
|
|
|
|
// WaitOwnersDurationOnPending is how long each owner should have been added to the
|
|
// partition before it's considered eligible for the WaitOwnersCountOnPending count.
|
|
WaitOwnersDurationOnPending time.Duration
|
|
|
|
// DeleteInactivePartitionAfterDuration is how long the lifecycler should wait before
|
|
// deleting inactive partitions with no owners. Inactive partitions are never removed
|
|
// if this value is 0.
|
|
DeleteInactivePartitionAfterDuration time.Duration
|
|
|
|
// PollingInterval is the internal polling interval. This setting is useful to let
|
|
// upstream projects to lower it in unit tests.
|
|
PollingInterval time.Duration
|
|
}
|
|
|
|
// PartitionInstanceLifecycler is responsible to manage the lifecycle of a single
|
|
// partition and partition owner in the ring.
|
|
type PartitionInstanceLifecycler struct {
|
|
*services.BasicService
|
|
|
|
// These values are initialised at startup, and never change.
|
|
cfg PartitionInstanceLifecyclerConfig
|
|
ringName string
|
|
ringKey string
|
|
store kv.Client
|
|
logger log.Logger
|
|
|
|
// Channel used to execute logic within the lifecycler loop.
|
|
actorChan chan func()
|
|
|
|
// Whether the partitions should be created on startup if it doesn't exist yet.
|
|
createPartitionOnStartup *atomic.Bool
|
|
|
|
// Whether the lifecycler should remove the partition owner (identified by instance ID) on shutdown.
|
|
removeOwnerOnShutdown *atomic.Bool
|
|
|
|
// Metrics.
|
|
reconcilesTotal *prometheus.CounterVec
|
|
reconcilesFailedTotal *prometheus.CounterVec
|
|
}
|
|
|
|
func NewPartitionInstanceLifecycler(cfg PartitionInstanceLifecyclerConfig, ringName, ringKey string, store kv.Client, logger log.Logger, reg prometheus.Registerer) *PartitionInstanceLifecycler {
|
|
if cfg.PollingInterval == 0 {
|
|
cfg.PollingInterval = 5 * time.Second
|
|
}
|
|
|
|
l := &PartitionInstanceLifecycler{
|
|
cfg: cfg,
|
|
ringName: ringName,
|
|
ringKey: ringKey,
|
|
store: store,
|
|
logger: log.With(logger, "ring", ringName),
|
|
actorChan: make(chan func()),
|
|
createPartitionOnStartup: atomic.NewBool(true),
|
|
removeOwnerOnShutdown: atomic.NewBool(false),
|
|
reconcilesTotal: promauto.With(reg).NewCounterVec(prometheus.CounterOpts{
|
|
Name: "partition_ring_lifecycler_reconciles_total",
|
|
Help: "Total number of reconciliations started.",
|
|
ConstLabels: map[string]string{"name": ringName},
|
|
}, []string{"type"}),
|
|
reconcilesFailedTotal: promauto.With(reg).NewCounterVec(prometheus.CounterOpts{
|
|
Name: "partition_ring_lifecycler_reconciles_failed_total",
|
|
Help: "Total number of reconciliations failed.",
|
|
ConstLabels: map[string]string{"name": ringName},
|
|
}, []string{"type"}),
|
|
}
|
|
|
|
l.BasicService = services.NewBasicService(l.starting, l.running, l.stopping)
|
|
|
|
return l
|
|
}
|
|
|
|
// CreatePartitionOnStartup returns whether the lifecycle creates the partition on startup
|
|
// if it doesn't exist.
|
|
func (l *PartitionInstanceLifecycler) CreatePartitionOnStartup() bool {
|
|
return l.createPartitionOnStartup.Load()
|
|
}
|
|
|
|
// SetCreatePartitionOnStartup sets whether the lifecycler should create the partition on
|
|
// startup if it doesn't exist.
|
|
func (l *PartitionInstanceLifecycler) SetCreatePartitionOnStartup(create bool) {
|
|
l.createPartitionOnStartup.Store(create)
|
|
}
|
|
|
|
// RemoveOwnerOnShutdown returns whether the lifecycler has been configured to remove the partition
|
|
// owner on shutdown.
|
|
func (l *PartitionInstanceLifecycler) RemoveOwnerOnShutdown() bool {
|
|
return l.removeOwnerOnShutdown.Load()
|
|
}
|
|
|
|
// SetRemoveOwnerOnShutdown sets whether the lifecycler should remove the partition owner on shutdown.
|
|
func (l *PartitionInstanceLifecycler) SetRemoveOwnerOnShutdown(remove bool) {
|
|
l.removeOwnerOnShutdown.Store(remove)
|
|
}
|
|
|
|
// GetPartitionState returns the current state of the partition, and the timestamp when the state was
|
|
// changed the last time.
|
|
func (l *PartitionInstanceLifecycler) GetPartitionState(ctx context.Context) (PartitionState, time.Time, error) {
|
|
ring, err := l.getRing(ctx)
|
|
if err != nil {
|
|
return PartitionUnknown, time.Time{}, err
|
|
}
|
|
|
|
partition, exists := ring.Partitions[l.cfg.PartitionID]
|
|
if !exists {
|
|
return PartitionUnknown, time.Time{}, ErrPartitionDoesNotExist
|
|
}
|
|
|
|
return partition.GetState(), partition.GetStateTime(), nil
|
|
}
|
|
|
|
// ChangePartitionState changes the partition state to toState.
|
|
// This function returns ErrPartitionDoesNotExist if the partition doesn't exist,
|
|
// and ErrPartitionStateChangeNotAllowed if the state change is not allowed.
|
|
func (l *PartitionInstanceLifecycler) ChangePartitionState(ctx context.Context, toState PartitionState) error {
|
|
return l.runOnLifecyclerLoop(func() error {
|
|
err := l.updateRing(ctx, func(ring *PartitionRingDesc) (bool, error) {
|
|
return changePartitionState(ring, l.cfg.PartitionID, toState)
|
|
})
|
|
|
|
if err != nil {
|
|
level.Warn(l.logger).Log("msg", "failed to change partition state", "partition", l.cfg.PartitionID, "to_state", toState, "err", err)
|
|
}
|
|
|
|
return err
|
|
})
|
|
}
|
|
|
|
func (l *PartitionInstanceLifecycler) starting(ctx context.Context) error {
|
|
if l.CreatePartitionOnStartup() {
|
|
return errors.Wrap(l.createPartitionAndRegisterOwner(ctx), "create partition and register owner")
|
|
}
|
|
|
|
return errors.Wrap(l.waitPartitionAndRegisterOwner(ctx), "wait partition and register owner")
|
|
}
|
|
|
|
func (l *PartitionInstanceLifecycler) running(ctx context.Context) error {
|
|
reconcile := func() {
|
|
l.reconcileOwnedPartition(ctx, time.Now())
|
|
l.reconcileOtherPartitions(ctx, time.Now())
|
|
}
|
|
|
|
// Run a reconciliation as soon as the lifecycler, in order to not having to wait for the 1st timer tick.
|
|
reconcile()
|
|
|
|
reconcileTicker := time.NewTicker(l.cfg.PollingInterval)
|
|
defer reconcileTicker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-reconcileTicker.C:
|
|
reconcile()
|
|
|
|
case f := <-l.actorChan:
|
|
f()
|
|
|
|
case <-ctx.Done():
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
func (l *PartitionInstanceLifecycler) stopping(_ error) error {
|
|
level.Info(l.logger).Log("msg", "partition ring lifecycler is shutting down", "ring", l.ringName)
|
|
|
|
// Remove the instance from partition owners, if configured to do so.
|
|
if l.RemoveOwnerOnShutdown() {
|
|
err := l.updateRing(context.Background(), func(ring *PartitionRingDesc) (bool, error) {
|
|
return ring.RemoveOwner(l.partitionOwnerID()), nil
|
|
})
|
|
|
|
if err != nil {
|
|
level.Error(l.logger).Log("msg", "failed to remove instance from partition owners on shutdown", "instance", l.cfg.InstanceID, "partition_owner_id", l.partitionOwnerID(), "partition", l.cfg.PartitionID, "err", err)
|
|
} else {
|
|
level.Info(l.logger).Log("msg", "instance removed from partition owners", "instance", l.cfg.InstanceID, "partition_owner_id", l.partitionOwnerID(), "partition", l.cfg.PartitionID)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// runOnLifecyclerLoop runs fn within the lifecycler loop.
|
|
func (l *PartitionInstanceLifecycler) runOnLifecyclerLoop(fn func() error) error {
|
|
sc := l.ServiceContext()
|
|
if sc == nil {
|
|
return errors.New("lifecycler not running")
|
|
}
|
|
|
|
errCh := make(chan error)
|
|
wrappedFn := func() {
|
|
errCh <- fn()
|
|
}
|
|
|
|
select {
|
|
case <-sc.Done():
|
|
return errors.New("lifecycler not running")
|
|
case l.actorChan <- wrappedFn:
|
|
return <-errCh
|
|
}
|
|
}
|
|
|
|
func (l *PartitionInstanceLifecycler) getRing(ctx context.Context) (*PartitionRingDesc, error) {
|
|
in, err := l.store.Get(ctx, l.ringKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return GetOrCreatePartitionRingDesc(in), nil
|
|
}
|
|
|
|
func (l *PartitionInstanceLifecycler) updateRing(ctx context.Context, update func(ring *PartitionRingDesc) (bool, error)) error {
|
|
return l.store.CAS(ctx, l.ringKey, func(in interface{}) (out interface{}, retry bool, err error) {
|
|
ringDesc := GetOrCreatePartitionRingDesc(in)
|
|
|
|
if changed, err := update(ringDesc); err != nil {
|
|
return nil, false, err
|
|
} else if !changed {
|
|
return nil, false, nil
|
|
}
|
|
|
|
return ringDesc, true, nil
|
|
})
|
|
}
|
|
|
|
// partitionOwnerID returns the instance ID used to register the owner in the ring.
|
|
func (l *PartitionInstanceLifecycler) partitionOwnerID() string {
|
|
if l.cfg.MultiPartitionOwnership {
|
|
return multiPartitionOwnerInstanceID(l.cfg.InstanceID, l.cfg.PartitionID)
|
|
}
|
|
|
|
return l.cfg.InstanceID
|
|
}
|
|
|
|
func (l *PartitionInstanceLifecycler) createPartitionAndRegisterOwner(ctx context.Context) error {
|
|
return l.updateRing(ctx, func(ring *PartitionRingDesc) (bool, error) {
|
|
now := time.Now()
|
|
changed := false
|
|
|
|
partitionDesc, exists := ring.Partitions[l.cfg.PartitionID]
|
|
if exists {
|
|
level.Info(l.logger).Log("msg", "partition found in the ring", "partition", l.cfg.PartitionID, "state", partitionDesc.GetState(), "state_timestamp", partitionDesc.GetState().String(), "tokens", len(partitionDesc.GetTokens()))
|
|
} else {
|
|
level.Info(l.logger).Log("msg", "partition not found in the ring", "partition", l.cfg.PartitionID)
|
|
}
|
|
|
|
if !exists {
|
|
// The partition doesn't exist, so we create a new one. A new partition should always be created
|
|
// in PENDING state.
|
|
ring.AddPartition(l.cfg.PartitionID, PartitionPending, now)
|
|
changed = true
|
|
}
|
|
|
|
// Ensure the instance is added as partition owner.
|
|
if ring.AddOrUpdateOwner(l.partitionOwnerID(), OwnerActive, l.cfg.PartitionID, now) {
|
|
changed = true
|
|
}
|
|
|
|
return changed, nil
|
|
})
|
|
}
|
|
|
|
func (l *PartitionInstanceLifecycler) waitPartitionAndRegisterOwner(ctx context.Context) error {
|
|
pollTicker := time.NewTicker(l.cfg.PollingInterval)
|
|
defer pollTicker.Stop()
|
|
|
|
// Wait until the partition exists.
|
|
checkPartitionExist := func() (bool, error) {
|
|
level.Info(l.logger).Log("msg", "checking if the partition exist in the ring", "partition", l.cfg.PartitionID)
|
|
|
|
ring, err := l.getRing(ctx)
|
|
if err != nil {
|
|
return false, errors.Wrap(err, "read partition ring")
|
|
}
|
|
|
|
if ring.HasPartition(l.cfg.PartitionID) {
|
|
level.Info(l.logger).Log("msg", "partition found in the ring", "partition", l.cfg.PartitionID)
|
|
return true, nil
|
|
}
|
|
|
|
level.Info(l.logger).Log("msg", "partition not found in the ring", "partition", l.cfg.PartitionID)
|
|
return false, nil
|
|
}
|
|
|
|
for {
|
|
if exists, err := checkPartitionExist(); err != nil {
|
|
return err
|
|
} else if exists {
|
|
break
|
|
}
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
|
|
case <-pollTicker.C:
|
|
// Throttle.
|
|
}
|
|
}
|
|
|
|
// Ensure the instance is added as partition owner.
|
|
return l.updateRing(ctx, func(ring *PartitionRingDesc) (bool, error) {
|
|
return ring.AddOrUpdateOwner(l.partitionOwnerID(), OwnerActive, l.cfg.PartitionID, time.Now()), nil
|
|
})
|
|
}
|
|
|
|
// reconcileOwnedPartition reconciles the owned partition.
|
|
// This function should be called periodically.
|
|
func (l *PartitionInstanceLifecycler) reconcileOwnedPartition(ctx context.Context, now time.Time) {
|
|
const reconcileType = "owned-partition"
|
|
l.reconcilesTotal.WithLabelValues(reconcileType).Inc()
|
|
|
|
err := l.updateRing(ctx, func(ring *PartitionRingDesc) (bool, error) {
|
|
partitionID := l.cfg.PartitionID
|
|
|
|
partition, exists := ring.Partitions[partitionID]
|
|
if !exists {
|
|
return false, ErrPartitionDoesNotExist
|
|
}
|
|
|
|
// A pending partition should be switched to active if there are enough owners that
|
|
// have been added since more than the waiting period.
|
|
if partition.IsPending() && ring.PartitionOwnersCountUpdatedBefore(partitionID, now.Add(-l.cfg.WaitOwnersDurationOnPending)) >= l.cfg.WaitOwnersCountOnPending {
|
|
level.Info(l.logger).Log("msg", "switching partition state because enough owners have been registered and minimum waiting time has elapsed", "partition", l.cfg.PartitionID, "from_state", PartitionPending, "to_state", PartitionActive)
|
|
return ring.UpdatePartitionState(partitionID, PartitionActive, now), nil
|
|
}
|
|
|
|
return false, nil
|
|
})
|
|
|
|
if err != nil {
|
|
l.reconcilesFailedTotal.WithLabelValues(reconcileType).Inc()
|
|
level.Warn(l.logger).Log("msg", "failed to reconcile owned partition", "partition", l.cfg.PartitionID, "err", err)
|
|
}
|
|
}
|
|
|
|
// reconcileOtherPartitions reconciles other partitions.
|
|
// This function should be called periodically.
|
|
func (l *PartitionInstanceLifecycler) reconcileOtherPartitions(ctx context.Context, now time.Time) {
|
|
const reconcileType = "other-partitions"
|
|
l.reconcilesTotal.WithLabelValues(reconcileType).Inc()
|
|
|
|
err := l.updateRing(ctx, func(ring *PartitionRingDesc) (bool, error) {
|
|
changed := false
|
|
|
|
if l.cfg.DeleteInactivePartitionAfterDuration > 0 {
|
|
deleteBefore := now.Add(-l.cfg.DeleteInactivePartitionAfterDuration)
|
|
|
|
for partitionID, partition := range ring.Partitions {
|
|
// Never delete the partition owned by this lifecycler, since it's expected to have at least
|
|
// this instance as owner.
|
|
if partitionID == l.cfg.PartitionID {
|
|
continue
|
|
}
|
|
|
|
// A partition is safe to be removed only if it's inactive since longer than the wait period
|
|
// and it has no owners registered.
|
|
if partition.IsInactiveSince(deleteBefore) && ring.PartitionOwnersCount(partitionID) == 0 {
|
|
level.Info(l.logger).Log("msg", "removing inactive partition with no owners from ring", "partition", partitionID, "state", partition.State.CleanName(), "state_timestamp", partition.GetStateTime().String())
|
|
ring.RemovePartition(partitionID)
|
|
changed = true
|
|
}
|
|
}
|
|
}
|
|
|
|
return changed, nil
|
|
})
|
|
|
|
if err != nil {
|
|
l.reconcilesFailedTotal.WithLabelValues(reconcileType).Inc()
|
|
level.Warn(l.logger).Log("msg", "failed to reconcile other partitions", "err", err)
|
|
}
|
|
}
|
|
|
|
func isPartitionStateChangeAllowed(from, to PartitionState) bool {
|
|
for _, allowed := range allowedPartitionStateChanges[from] {
|
|
if to == allowed {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|