Files
renovate-sh-app[bot]GitHubrenovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com>A. Stoewer
92ce383d0e fix(deps): update github.com/grafana/dskit digest to 10fce42 (#5749)
* fix(deps): update github.com/grafana/dskit digest to 10fce42

Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com>

* make generate-manifest

---------

Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com>
Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com>
Co-authored-by: A. Stoewer <[email protected]>
2025-10-28 08:29:23 +10:00

366 lines
10 KiB
Go

package kv
import (
"context"
"flag"
"fmt"
"sync"
"time"
"github.com/go-kit/log"
"github.com/go-kit/log/level"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"go.uber.org/atomic"
)
// MultiConfig is a configuration for MultiClient.
type MultiConfig struct {
Primary string `yaml:"primary" category:"advanced"`
Secondary string `yaml:"secondary" category:"advanced"`
MirrorEnabled bool `yaml:"mirror_enabled" category:"advanced"`
MirrorTimeout time.Duration `yaml:"mirror_timeout" category:"advanced"`
// ConfigProvider returns channel with MultiRuntimeConfig updates.
ConfigProvider func() <-chan MultiRuntimeConfig `yaml:"-"`
}
// RegisterFlagsWithPrefix registers flags with prefix.
func (cfg *MultiConfig) RegisterFlagsWithPrefix(f *flag.FlagSet, prefix string) {
f.StringVar(&cfg.Primary, prefix+"multi.primary", "", "Primary backend storage used by multi-client.")
f.StringVar(&cfg.Secondary, prefix+"multi.secondary", "", "Secondary backend storage used by multi-client.")
f.BoolVar(&cfg.MirrorEnabled, prefix+"multi.mirror-enabled", false, "Mirror writes to the secondary store.")
f.DurationVar(&cfg.MirrorTimeout, prefix+"multi.mirror-timeout", 2*time.Second, "Timeout for storing a value to the secondary store.")
}
// MultiRuntimeConfig has values that can change in runtime (via overrides)
type MultiRuntimeConfig struct {
// Primary store used by MultiClient. Can be updated in runtime to switch to a different store (eg. consul -> etcd,
// or to gossip). Doing this allows nice migration between stores. Empty values are ignored.
PrimaryStore string `yaml:"primary"`
// Mirroring enabled or not. Nil = no change.
Mirroring *bool `yaml:"mirror_enabled"`
}
type kvclient struct {
client Client
name string
}
type clientInProgress struct {
client int
cancel context.CancelFunc
}
// MultiClient implements kv.Client by forwarding all API calls to primary client.
// Writes performed via CAS method are also (optionally) forwarded to secondary clients.
type MultiClient struct {
// Available KV clients
clients []kvclient
mirrorTimeout time.Duration
mirroringEnabled *atomic.Bool
// logger with "multikv" component
logger log.Logger
// The primary client used for interaction.
primaryID *atomic.Int32
cancel context.CancelFunc
inProgressMu sync.Mutex
// Cancel functions for ongoing operations. key is a value from inProgressCnt.
// What we really need is a []context.CancelFunc, but functions cannot be compared against each other using ==,
// so we use this map instead.
inProgress map[int]clientInProgress
inProgressCnt int
primaryStoreGauge *prometheus.GaugeVec
mirrorEnabledGauge prometheus.Gauge
mirrorWritesCounter prometheus.Counter
mirrorFailuresCounter prometheus.Counter
}
// NewMultiClient creates new MultiClient with given KV Clients.
// First client in the slice is the primary client.
func NewMultiClient(cfg MultiConfig, clients []kvclient, logger log.Logger, registerer prometheus.Registerer) *MultiClient {
c := &MultiClient{
clients: clients,
primaryID: atomic.NewInt32(0),
inProgress: map[int]clientInProgress{},
mirrorTimeout: cfg.MirrorTimeout,
mirroringEnabled: atomic.NewBool(cfg.MirrorEnabled),
logger: log.With(logger, "component", "multikv"),
}
c.registerMetrics(registerer)
c.updatePrimaryStoreGauge()
c.updateMirrorEnabledGauge()
ctx, cancelFn := context.WithCancel(context.Background())
c.cancel = cancelFn
if cfg.ConfigProvider != nil {
go c.watchConfigChannel(ctx, cfg.ConfigProvider())
}
return c
}
func (m *MultiClient) watchConfigChannel(ctx context.Context, configChannel <-chan MultiRuntimeConfig) {
for {
select {
case cfg, ok := <-configChannel:
if !ok {
return
}
if cfg.Mirroring != nil {
enabled := *cfg.Mirroring
old := m.mirroringEnabled.Swap(enabled)
if old != enabled {
level.Info(m.logger).Log("msg", "toggled mirroring", "enabled", enabled)
}
m.updateMirrorEnabledGauge()
}
if cfg.PrimaryStore != "" {
switched, err := m.setNewPrimaryClient(cfg.PrimaryStore)
if switched {
level.Info(m.logger).Log("msg", "switched primary KV store", "primary", cfg.PrimaryStore)
}
if err != nil {
level.Error(m.logger).Log("msg", "failed to switch primary KV store", "primary", cfg.PrimaryStore, "err", err)
}
}
case <-ctx.Done():
return
}
}
}
func (m *MultiClient) getPrimaryClient() (int, kvclient) {
v := m.primaryID.Load()
return int(v), m.clients[v]
}
// returns true, if primary client has changed
func (m *MultiClient) setNewPrimaryClient(store string) (bool, error) {
newPrimaryIx := -1
for ix, c := range m.clients {
if c.name == store {
newPrimaryIx = ix
break
}
}
if newPrimaryIx < 0 {
return false, fmt.Errorf("KV store not found")
}
prev := int(m.primaryID.Swap(int32(newPrimaryIx)))
if prev == newPrimaryIx {
return false, nil
}
defer m.updatePrimaryStoreGauge() // do as the last thing, after releasing the lock
// switching to new primary... cancel clients using previous one
m.inProgressMu.Lock()
defer m.inProgressMu.Unlock()
for _, inp := range m.inProgress {
if inp.client == prev {
inp.cancel()
}
}
return true, nil
}
func (m *MultiClient) registerMetrics(registerer prometheus.Registerer) {
m.primaryStoreGauge = promauto.With(registerer).NewGaugeVec(prometheus.GaugeOpts{
Name: "multikv_primary_store",
Help: "Selected primary KV store",
}, []string{"store"})
m.mirrorEnabledGauge = promauto.With(registerer).NewGauge(prometheus.GaugeOpts{
Name: "multikv_mirror_enabled",
Help: "Is mirroring to secondary store enabled",
})
m.mirrorWritesCounter = promauto.With(registerer).NewCounter(prometheus.CounterOpts{
Name: "multikv_mirror_writes_total",
Help: "Number of mirror-writes to secondary store",
})
m.mirrorFailuresCounter = promauto.With(registerer).NewCounter(prometheus.CounterOpts{
Name: "multikv_mirror_write_errors_total",
Help: "Number of failures to mirror-write to secondary store",
})
}
func (m *MultiClient) updatePrimaryStoreGauge() {
_, pkv := m.getPrimaryClient()
for _, kv := range m.clients {
value := float64(0)
if pkv == kv {
value = 1
}
m.primaryStoreGauge.WithLabelValues(kv.name).Set(value)
}
}
func (m *MultiClient) updateMirrorEnabledGauge() {
if m.mirroringEnabled.Load() {
m.mirrorEnabledGauge.Set(1)
} else {
m.mirrorEnabledGauge.Set(0)
}
}
func (m *MultiClient) registerCancelFn(clientID int, fn context.CancelFunc) int {
m.inProgressMu.Lock()
defer m.inProgressMu.Unlock()
m.inProgressCnt++
id := m.inProgressCnt
m.inProgress[id] = clientInProgress{client: clientID, cancel: fn}
return id
}
func (m *MultiClient) unregisterCancelFn(id int) {
m.inProgressMu.Lock()
defer m.inProgressMu.Unlock()
delete(m.inProgress, id)
}
// Runs supplied fn with current primary client. If primary client changes, fn is restarted.
// When fn finishes (with or without error), this method returns given error value.
func (m *MultiClient) runWithPrimaryClient(origCtx context.Context, fn func(newCtx context.Context, primary kvclient) error) error {
cancelFn := context.CancelFunc(nil)
cancelFnID := 0
cleanup := func() {
if cancelFn != nil {
cancelFn()
}
if cancelFnID > 0 {
m.unregisterCancelFn(cancelFnID)
}
}
defer cleanup()
// This only loops if switchover to a new primary backend happens while calling 'fn', which is very rare.
for {
cleanup()
pid, kv := m.getPrimaryClient()
var cancelCtx context.Context
cancelCtx, cancelFn = context.WithCancel(origCtx)
cancelFnID = m.registerCancelFn(pid, cancelFn)
err := fn(cancelCtx, kv)
if err == nil {
return nil
}
if cancelCtx.Err() == context.Canceled && origCtx.Err() == nil {
// our context was cancelled, but outer context is not done yet. retry
continue
}
return err
}
}
// List is a part of the kv.Client interface.
func (m *MultiClient) List(ctx context.Context, prefix string) ([]string, error) {
_, kv := m.getPrimaryClient()
return kv.client.List(ctx, prefix)
}
// Get is a part of kv.Client interface.
func (m *MultiClient) Get(ctx context.Context, key string) (interface{}, error) {
_, kv := m.getPrimaryClient()
return kv.client.Get(ctx, key)
}
// Delete is a part of the kv.Client interface.
func (m *MultiClient) Delete(ctx context.Context, key string) error {
_, kv := m.getPrimaryClient()
return kv.client.Delete(ctx, key)
}
// CAS is a part of kv.Client interface.
func (m *MultiClient) CAS(ctx context.Context, key string, f func(in interface{}) (out interface{}, retry bool, err error)) error {
_, kv := m.getPrimaryClient()
updatedValue := interface{}(nil)
err := kv.client.CAS(ctx, key, func(in interface{}) (interface{}, bool, error) {
out, retry, err := f(in)
updatedValue = out
return out, retry, err
})
if err == nil && updatedValue != nil && m.mirroringEnabled.Load() {
m.writeToSecondary(ctx, kv, key, updatedValue)
}
return err
}
// WatchKey is a part of kv.Client interface.
func (m *MultiClient) WatchKey(ctx context.Context, key string, f func(interface{}) bool) {
_ = m.runWithPrimaryClient(ctx, func(newCtx context.Context, primary kvclient) error {
primary.client.WatchKey(newCtx, key, f)
return newCtx.Err()
})
}
// WatchPrefix is a part of kv.Client interface.
func (m *MultiClient) WatchPrefix(ctx context.Context, prefix string, f func(string, interface{}) bool) {
_ = m.runWithPrimaryClient(ctx, func(newCtx context.Context, primary kvclient) error {
primary.client.WatchPrefix(newCtx, prefix, f)
return newCtx.Err()
})
}
func (m *MultiClient) writeToSecondary(ctx context.Context, primary kvclient, key string, newValue interface{}) {
if m.mirrorTimeout > 0 {
var cfn context.CancelFunc
ctx, cfn = context.WithTimeout(ctx, m.mirrorTimeout)
defer cfn()
}
// let's propagate new value to all remaining clients
for _, kvc := range m.clients {
if kvc == primary {
continue
}
m.mirrorWritesCounter.Inc()
err := kvc.client.CAS(ctx, key, func(interface{}) (out interface{}, retry bool, err error) {
// try once
return newValue, false, nil
})
if err != nil {
m.mirrorFailuresCounter.Inc()
level.Warn(m.logger).Log("msg", "failed to update value in secondary store", "key", key, "err", err, "primary", primary.name, "secondary", kvc.name)
} else {
level.Debug(m.logger).Log("msg", "stored updated value to secondary store", "key", key, "primary", primary.name, "secondary", kvc.name)
}
}
}