Files
traces/vendor/github.com/grafana/dskit/ring/partition_instance_lifecycler.go
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

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
}