diff --git a/config/config.go b/config/config.go index c3cda421..ca0b2112 100644 --- a/config/config.go +++ b/config/config.go @@ -14,6 +14,23 @@ // 1. camelCase is converted to snake_case, e.g. someVariable -> some_variable // 2. dots (.) are replaced with underscores (_), e.g. some.variable -> some_variable // 3. the resulting string is uppercased and prefixed with ${PREFIX}_ (default RSERVER_), e.g. some_variable -> RSERVER_SOME_VARIABLE +// +// Order of keys: +// +// When registering a variable with multiple keys, the order of the keys is important as it determines the +// hierarchical order of the keys. +// The first key is the most important one, and the last key is the least important one. +// Example: +// config.RegisterDurationConfigVariable(90, &cmdTimeout, true, time.Second, +// "JobsDB.Router.CommandRequestTimeout", +// "JobsDB.CommandRequestTimeout", +// ) +// +// In the above example "JobsDB.Router.CommandRequestTimeout" is checked first. If it doesn't exist then +// JobsDB.CommandRequestTimeout is checked. +// +// WARNING: for this reason, registering with the same keys but in a different order is going to return two +// different variables. package config import ( @@ -35,7 +52,7 @@ var camelCaseMatch = regexp.MustCompile("([a-z0-9])([A-Z])") // regular expression matching uppercase letters contained in environment variable names var upperCaseMatch = regexp.MustCompile("^[A-Z0-9_]+$") -// default, singleton config instance +// Default is the singleton config instance var Default *Config func init() { @@ -60,7 +77,9 @@ func WithEnvPrefix(prefix string) Opt { // New creates a new config instance func New(opts ...Opt) *Config { c := &Config{ - envPrefix: DefaultEnvPrefix, + envPrefix: DefaultEnvPrefix, + reloadableVars: make(map[string]any), + reloadableVarsMisuses: make(map[string]string), } for _, opt := range opts { opt(c) @@ -78,6 +97,9 @@ type Config struct { envsLock sync.RWMutex // protects the envs map below envs map[string]string envPrefix string // prefix for environment variables + reloadableVars map[string]any + reloadableVarsMisuses map[string]string + reloadableVarsLock sync.RWMutex // used to protect both the reloadableVars and reloadableVarsMisuses maps } // GetBool gets bool value from config @@ -270,6 +292,41 @@ func (c *Config) Set(key string, value interface{}) { c.onConfigChange() } +func getReloadableMapKeys[T configTypes](v T, orderedKeys ...string) (string, string) { + k := fmt.Sprintf("%T:%s", v, strings.Join(orderedKeys, ",")) + return k, fmt.Sprintf("%s:%v", k, v) +} + +func getOrCreatePointer[T configTypes]( + m map[string]any, dvs map[string]string, // this function MUST receive maps that are already initialized + lock *sync.RWMutex, defaultValue T, orderedKeys ...string, +) *Reloadable[T] { + key, dvKey := getReloadableMapKeys(defaultValue, orderedKeys...) + + lock.Lock() + defer lock.Unlock() + + defer func() { + if _, ok := dvs[key]; !ok { + dvs[key] = dvKey + } + if dvs[key] != dvKey { + panic(fmt.Errorf( + "Detected misuse of config variable registered with different default values %+v - %+v\n", + dvs[key], dvKey, + )) + } + }() + + if p, ok := m[key]; ok { + return p.(*Reloadable[T]) + } + + p := &Reloadable[T]{} + m[key] = p + return p +} + // bindEnv handles rudder server's unique snake case replacement by registering // the environment variables to viper, that would otherwise be ignored. // Viper uppercases keys before sending them to its EnvKeyReplacer, thus diff --git a/config/config_test.go b/config/config_test.go index f144def0..4edab8c9 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -1,6 +1,7 @@ package config import ( + "sync" "testing" "time" @@ -213,6 +214,271 @@ func TestCheckAndHotReloadConfig(t *testing.T) { }) } +func TestNewReloadableAPI(t *testing.T) { + t.Run("non reloadable", func(t *testing.T) { + t.Run("int", func(t *testing.T) { + c := New() + v := c.GetIntVar(5, 1, t.Name()) + require.Equal(t, 5, v) + }) + t.Run("int64", func(t *testing.T) { + c := New() + v := c.GetInt64Var(5, 1, t.Name()) + require.EqualValues(t, 5, v) + }) + t.Run("bool", func(t *testing.T) { + c := New() + v := c.GetBoolVar(true, t.Name()) + require.True(t, v) + }) + t.Run("float64", func(t *testing.T) { + c := New() + v := c.GetFloat64Var(0.123, t.Name()) + require.EqualValues(t, 0.123, v) + }) + t.Run("string", func(t *testing.T) { + c := New() + v := c.GetStringVar("foo", t.Name()) + require.Equal(t, "foo", v) + }) + t.Run("duration", func(t *testing.T) { + c := New() + v := c.GetDurationVar(123, time.Second, t.Name()) + require.Equal(t, 123*time.Second, v) + }) + t.Run("[]string", func(t *testing.T) { + c := New() + v := c.GetStringSliceVar([]string{"a", "b"}, t.Name()) + require.NotNil(t, v) + require.Equal(t, []string{"a", "b"}, v) + + c.Set(t.Name(), []string{"c", "d"}) + require.Equal(t, []string{"a", "b"}, v, "variable is not reloadable") + }) + t.Run("map[string]interface{}", func(t *testing.T) { + c := New() + v := c.GetStringMapVar(map[string]interface{}{"a": 1, "b": 2}, t.Name()) + require.NotNil(t, v) + require.Equal(t, map[string]interface{}{"a": 1, "b": 2}, v) + + c.Set(t.Name(), map[string]interface{}{"c": 3, "d": 4}) + require.Equal(t, map[string]interface{}{"a": 1, "b": 2}, v, "variable is not reloadable") + }) + }) + t.Run("reloadable", func(t *testing.T) { + t.Run("int", func(t *testing.T) { + c := New() + v := c.GetReloadableIntVar(5, 1, t.Name()) + require.Equal(t, 5, v.Load()) + + c.Set(t.Name(), 10) + require.Equal(t, 10, v.Load()) + + c.Set(t.Name(), 10) + require.Equal(t, 10, v.Load(), "value should not change") + + require.PanicsWithError(t, + "Detected misuse of config variable registered with different default values "+ + "int:TestNewReloadableAPI/reloadable/int:5 - "+ + "int:TestNewReloadableAPI/reloadable/int:10\n", + func() { + // changing just the valueScale also changes the default value + _ = c.GetReloadableIntVar(5, 2, t.Name()) + }, + ) + }) + t.Run("int64", func(t *testing.T) { + c := New() + v := c.GetReloadableInt64Var(5, 1, t.Name()) + require.EqualValues(t, 5, v.Load()) + + c.Set(t.Name(), 10) + require.EqualValues(t, 10, v.Load()) + + c.Set(t.Name(), 10) + require.EqualValues(t, 10, v.Load(), "value should not change") + + require.PanicsWithError(t, + "Detected misuse of config variable registered with different default values "+ + "int64:TestNewReloadableAPI/reloadable/int64:5 - "+ + "int64:TestNewReloadableAPI/reloadable/int64:10\n", + func() { + // changing just the valueScale also changes the default value + _ = c.GetReloadableInt64Var(5, 2, t.Name()) + }, + ) + }) + t.Run("bool", func(t *testing.T) { + c := New() + v := c.GetReloadableBoolVar(true, t.Name()) + require.True(t, v.Load()) + + c.Set(t.Name(), false) + require.False(t, v.Load()) + + c.Set(t.Name(), false) + require.False(t, v.Load(), "value should not change") + + require.PanicsWithError(t, + "Detected misuse of config variable registered with different default values "+ + "bool:TestNewReloadableAPI/reloadable/bool:true - "+ + "bool:TestNewReloadableAPI/reloadable/bool:false\n", + func() { + _ = c.GetReloadableBoolVar(false, t.Name()) + }, + ) + }) + t.Run("float64", func(t *testing.T) { + c := New() + v := c.GetReloadableFloat64Var(0.123, t.Name()) + require.EqualValues(t, 0.123, v.Load()) + + c.Set(t.Name(), 4.567) + require.EqualValues(t, 4.567, v.Load()) + + c.Set(t.Name(), 4.567) + require.EqualValues(t, 4.567, v.Load(), "value should not change") + + require.PanicsWithError(t, + "Detected misuse of config variable registered with different default values "+ + "float64:TestNewReloadableAPI/reloadable/float64:0.123 - "+ + "float64:TestNewReloadableAPI/reloadable/float64:0.1234\n", + func() { + _ = c.GetReloadableFloat64Var(0.1234, t.Name()) + }, + ) + }) + t.Run("string", func(t *testing.T) { + c := New() + v := c.GetReloadableStringVar("foo", t.Name()) + require.Equal(t, "foo", v.Load()) + + c.Set(t.Name(), "bar") + require.EqualValues(t, "bar", v.Load()) + + c.Set(t.Name(), "bar") + require.EqualValues(t, "bar", v.Load(), "value should not change") + + require.PanicsWithError(t, + "Detected misuse of config variable registered with different default values "+ + "string:TestNewReloadableAPI/reloadable/string:foo - "+ + "string:TestNewReloadableAPI/reloadable/string:qux\n", + func() { + _ = c.GetReloadableStringVar("qux", t.Name()) + }, + ) + }) + t.Run("duration", func(t *testing.T) { + c := New() + v := c.GetReloadableDurationVar(123, time.Nanosecond, t.Name()) + require.Equal(t, 123*time.Nanosecond, v.Load()) + + c.Set(t.Name(), 456*time.Millisecond) + require.Equal(t, 456*time.Millisecond, v.Load()) + + c.Set(t.Name(), 456*time.Millisecond) + require.Equal(t, 456*time.Millisecond, v.Load(), "value should not change") + + require.PanicsWithError(t, + "Detected misuse of config variable registered with different default values "+ + "time.Duration:TestNewReloadableAPI/reloadable/duration:123ns - "+ + "time.Duration:TestNewReloadableAPI/reloadable/duration:2m3s\n", + func() { + _ = c.GetReloadableDurationVar(123, time.Second, t.Name()) + }, + ) + }) + t.Run("[]string", func(t *testing.T) { + c := New() + v := c.GetReloadableStringSliceVar([]string{"a", "b"}, t.Name()) + require.Equal(t, []string{"a", "b"}, v.Load()) + + c.Set(t.Name(), []string{"c", "d"}) + require.Equal(t, []string{"c", "d"}, v.Load()) + + c.Set(t.Name(), []string{"c", "d"}) + require.Equal(t, []string{"c", "d"}, v.Load(), "value should not change") + + require.PanicsWithError(t, + "Detected misuse of config variable registered with different default values "+ + "[]string:TestNewReloadableAPI/reloadable/[]string:[a b] - "+ + "[]string:TestNewReloadableAPI/reloadable/[]string:[a b c]\n", + func() { + _ = c.GetReloadableStringSliceVar([]string{"a", "b", "c"}, t.Name()) + }, + ) + }) + t.Run("map[string]interface{}", func(t *testing.T) { + c := New() + v := c.GetReloadableStringMapVar(map[string]interface{}{"a": 1, "b": 2}, t.Name()) + require.Equal(t, map[string]interface{}{"a": 1, "b": 2}, v.Load()) + + c.Set(t.Name(), map[string]interface{}{"c": 3, "d": 4}) + require.Equal(t, map[string]interface{}{"c": 3, "d": 4}, v.Load()) + + c.Set(t.Name(), map[string]interface{}{"c": 3, "d": 4}) + require.Equal(t, map[string]interface{}{"c": 3, "d": 4}, v.Load(), "value should not change") + + require.PanicsWithError(t, + "Detected misuse of config variable registered with different default values "+ + "map[string]interface {}:TestNewReloadableAPI/reloadable/map[string]interface{}:map[a:1 b:2] - "+ + "map[string]interface {}:TestNewReloadableAPI/reloadable/map[string]interface{}:map[a:2 b:1]\n", + func() { + _ = c.GetReloadableStringMapVar(map[string]interface{}{"a": 2, "b": 1}, t.Name()) + }, + ) + }) + }) +} + +func TestGetOrCreatePointer(t *testing.T) { + var ( + m = make(map[string]any) + dvs = make(map[string]string) + rwm sync.RWMutex + ) + p1 := getOrCreatePointer(m, dvs, &rwm, 123, "foo", "bar") + require.NotNil(t, p1) + + p2 := getOrCreatePointer(m, dvs, &rwm, 123, "foo", "bar") + require.True(t, p1 == p2) + + p3 := getOrCreatePointer(m, dvs, &rwm, 123, "bar", "foo") + require.True(t, p1 != p3) + + p4 := getOrCreatePointer(m, dvs, &rwm, 123, "bar", "foo", "qux") + require.True(t, p1 != p4) + + require.PanicsWithError(t, + "Detected misuse of config variable registered with different default values "+ + "int:bar,foo,qux:123 - int:bar,foo,qux:456\n", + func() { + getOrCreatePointer(m, dvs, &rwm, 456, "bar", "foo", "qux") + }, + ) +} + +func TestReloadable(t *testing.T) { + t.Run("scalar", func(t *testing.T) { + var v Reloadable[int] + v.store(123) + require.Equal(t, 123, v.Load()) + }) + t.Run("nullable", func(t *testing.T) { + var v Reloadable[[]string] + require.Nil(t, v.Load()) + + v.store(nil) + require.Nil(t, v.Load()) + + v.store([]string{"foo", "bar"}) + require.Equal(t, v.Load(), []string{"foo", "bar"}) + + v.store(nil) + require.Nil(t, v.Load()) + }) +} + func TestConfigKeyToEnv(t *testing.T) { expected := "RSERVER_KEY_VAR1_VAR2" require.Equal(t, expected, ConfigKeyToEnv(DefaultEnvPrefix, "Key.Var1.Var2")) diff --git a/config/hotreloadable.go b/config/hotreloadable.go index 46981848..578d7133 100644 --- a/config/hotreloadable.go +++ b/config/hotreloadable.go @@ -1,14 +1,80 @@ package config -import "time" +import ( + "strings" + "sync" + "time" +) // RegisterIntConfigVariable registers int config variable -func RegisterIntConfigVariable(defaultValue int, ptr *int, isHotReloadable bool, valueScale int, keys ...string) { - Default.RegisterIntConfigVariable(defaultValue, ptr, isHotReloadable, valueScale, keys...) +// +// WARNING: keys are being looked up in requested order and the value of the first found key is returned, +// e.g. asking for the same keys but in a different order can result in a different value to be returned +// +// Deprecated: use GetIntVar or GetReloadableIntVar instead +func RegisterIntConfigVariable(defaultValue int, ptr *int, isHotReloadable bool, valueScale int, orderedKeys ...string) { + Default.RegisterIntConfigVariable(defaultValue, ptr, isHotReloadable, valueScale, orderedKeys...) +} + +// GetIntVar registers a not hot-reloadable int config variable +// +// WARNING: keys are being looked up in requested order and the value of the first found key is returned, +// e.g. asking for the same keys but in a different order can result in a different value to be returned +func GetIntVar(defaultValue, valueScale int, orderedKeys ...string) int { + return Default.GetIntVar(defaultValue, valueScale, orderedKeys...) +} + +// GetReloadableIntVar registers a hot-reloadable int config variable +// +// WARNING: keys are being looked up in requested order and the value of the first found key is returned, +// e.g. asking for the same keys but in a different order can result in a different value to be returned +func GetReloadableIntVar(defaultValue, valueScale int, orderedKeys ...string) *Reloadable[int] { + return Default.GetReloadableIntVar(defaultValue, valueScale, orderedKeys...) } // RegisterIntConfigVariable registers int config variable -func (c *Config) RegisterIntConfigVariable(defaultValue int, ptr *int, isHotReloadable bool, valueScale int, keys ...string) { +// +// WARNING: keys are being looked up in requested order and the value of the first found key is returned, +// e.g. asking for the same keys but in a different order can result in a different value to be returned +// +// Deprecated: use GetIntVar or GetReloadableIntVar instead +func (c *Config) RegisterIntConfigVariable( + defaultValue int, ptr *int, isHotReloadable bool, valueScale int, orderedKeys ...string, +) { + c.registerIntVar(defaultValue, ptr, isHotReloadable, valueScale, func(v int) { + *ptr = v + }, orderedKeys...) +} + +// GetIntVar registers a not hot-reloadable int config variable +// +// WARNING: keys are being looked up in requested order and the value of the first found key is returned, +// e.g. asking for the same keys but in a different order can result in a different value to be returned +func (c *Config) GetIntVar(defaultValue, valueScale int, orderedKeys ...string) int { + var ret int + c.registerIntVar(defaultValue, nil, false, valueScale, func(v int) { + ret = v + }, orderedKeys...) + return ret +} + +// GetReloadableIntVar registers a hot-reloadable int config variable +// +// WARNING: keys are being looked up in requested order and the value of the first found key is returned, +// e.g. asking for the same keys but in a different order can result in a different value to be returned +func (c *Config) GetReloadableIntVar(defaultValue, valueScale int, orderedKeys ...string) *Reloadable[int] { + ptr := getOrCreatePointer( + c.reloadableVars, c.reloadableVarsMisuses, &c.reloadableVarsLock, defaultValue*valueScale, orderedKeys..., + ) + c.registerIntVar(defaultValue, ptr, true, valueScale, func(v int) { + ptr.store(v) + }, orderedKeys...) + return ptr +} + +func (c *Config) registerIntVar( + defaultValue int, ptr any, isHotReloadable bool, valueScale int, store func(int), orderedKeys ...string, +) { c.vLock.RLock() defer c.vLock.RUnlock() c.hotReloadableConfigLock.Lock() @@ -17,29 +83,87 @@ func (c *Config) RegisterIntConfigVariable(defaultValue int, ptr *int, isHotRelo value: ptr, multiplier: valueScale, defaultValue: defaultValue, - keys: keys, + keys: orderedKeys, } if isHotReloadable { - c.appendVarToConfigMaps(keys[0], &configVar) + c.appendVarToConfigMaps(orderedKeys, &configVar) } - for _, key := range keys { + for _, key := range orderedKeys { if c.IsSet(key) { - *ptr = c.GetInt(key, defaultValue) * valueScale + store(c.GetInt(key, defaultValue) * valueScale) return } } - *ptr = defaultValue * valueScale + store(defaultValue * valueScale) } // RegisterBoolConfigVariable registers bool config variable -func RegisterBoolConfigVariable(defaultValue bool, ptr *bool, isHotReloadable bool, keys ...string) { - Default.RegisterBoolConfigVariable(defaultValue, ptr, isHotReloadable, keys...) +// +// WARNING: keys are being looked up in requested order and the value of the first found key is returned, +// e.g. asking for the same keys but in a different order can result in a different value to be returned +// +// Deprecated: use GetBoolVar or GetReloadableBoolVar instead +func RegisterBoolConfigVariable(defaultValue bool, ptr *bool, isHotReloadable bool, orderedKeys ...string) { + Default.RegisterBoolConfigVariable(defaultValue, ptr, isHotReloadable, orderedKeys...) +} + +// GetBoolVar registers a not hot-reloadable bool config variable +// +// WARNING: keys are being looked up in requested order and the value of the first found key is returned, +// e.g. asking for the same keys but in a different order can result in a different value to be returned +func GetBoolVar(defaultValue bool, orderedKeys ...string) bool { + return Default.GetBoolVar(defaultValue, orderedKeys...) +} + +// GetReloadableBoolVar registers a hot-reloadable bool config variable +// +// WARNING: keys are being looked up in requested order and the value of the first found key is returned, +// e.g. asking for the same keys but in a different order can result in a different value to be returned +func GetReloadableBoolVar(defaultValue bool, orderedKeys ...string) *Reloadable[bool] { + return Default.GetReloadableBoolVar(defaultValue, orderedKeys...) } // RegisterBoolConfigVariable registers bool config variable -func (c *Config) RegisterBoolConfigVariable(defaultValue bool, ptr *bool, isHotReloadable bool, keys ...string) { +// +// WARNING: keys are being looked up in requested order and the value of the first found key is returned, +// e.g. asking for the same keys but in a different order can result in a different value to be returned +// +// Deprecated: use GetBoolVar or GetReloadableBoolVar instead +func (c *Config) RegisterBoolConfigVariable(defaultValue bool, ptr *bool, isHotReloadable bool, orderedKeys ...string) { + c.registerBoolVar(defaultValue, ptr, isHotReloadable, func(v bool) { + *ptr = v + }, orderedKeys...) +} + +// GetBoolVar registers a not hot-reloadable bool config variable +// +// WARNING: keys are being looked up in requested order and the value of the first found key is returned, +// e.g. asking for the same keys but in a different order can result in a different value to be returned +func (c *Config) GetBoolVar(defaultValue bool, orderedKeys ...string) bool { + var ret bool + c.registerBoolVar(defaultValue, nil, false, func(v bool) { + ret = v + }, orderedKeys...) + return ret +} + +// GetReloadableBoolVar registers a hot-reloadable bool config variable +// +// WARNING: keys are being looked up in requested order and the value of the first found key is returned, +// e.g. asking for the same keys but in a different order can result in a different value to be returned +func (c *Config) GetReloadableBoolVar(defaultValue bool, orderedKeys ...string) *Reloadable[bool] { + ptr := getOrCreatePointer( + c.reloadableVars, c.reloadableVarsMisuses, &c.reloadableVarsLock, defaultValue, orderedKeys..., + ) + c.registerBoolVar(defaultValue, ptr, true, func(v bool) { + ptr.store(v) + }, orderedKeys...) + return ptr +} + +func (c *Config) registerBoolVar(defaultValue bool, ptr any, isHotReloadable bool, store func(bool), orderedKeys ...string) { c.vLock.RLock() defer c.vLock.RUnlock() c.hotReloadableConfigLock.Lock() @@ -47,30 +171,92 @@ func (c *Config) RegisterBoolConfigVariable(defaultValue bool, ptr *bool, isHotR configVar := configValue{ value: ptr, defaultValue: defaultValue, - keys: keys, + keys: orderedKeys, } if isHotReloadable { - c.appendVarToConfigMaps(keys[0], &configVar) + c.appendVarToConfigMaps(orderedKeys, &configVar) } - for _, key := range keys { + for _, key := range orderedKeys { c.bindEnv(key) if c.IsSet(key) { - *ptr = c.GetBool(key, defaultValue) + store(c.GetBool(key, defaultValue)) return } } - *ptr = defaultValue + store(defaultValue) } // RegisterFloat64ConfigVariable registers float64 config variable -func RegisterFloat64ConfigVariable(defaultValue float64, ptr *float64, isHotReloadable bool, keys ...string) { - Default.RegisterFloat64ConfigVariable(defaultValue, ptr, isHotReloadable, keys...) +// +// WARNING: keys are being looked up in requested order and the value of the first found key is returned, +// e.g. asking for the same keys but in a different order can result in a different value to be returned +// +// Deprecated: use GetFloat64Var or GetReloadableFloat64Var instead +func RegisterFloat64ConfigVariable(defaultValue float64, ptr *float64, isHotReloadable bool, orderedKeys ...string) { + Default.RegisterFloat64ConfigVariable(defaultValue, ptr, isHotReloadable, orderedKeys...) +} + +// GetFloat64Var registers a not hot-reloadable float64 config variable +// +// WARNING: keys are being looked up in requested order and the value of the first found key is returned, +// e.g. asking for the same keys but in a different order can result in a different value to be returned +func GetFloat64Var(defaultValue float64, orderedKeys ...string) float64 { + return Default.GetFloat64Var(defaultValue, orderedKeys...) +} + +// GetReloadableFloat64Var registers a hot-reloadable float64 config variable +// +// WARNING: keys are being looked up in requested order and the value of the first found key is returned, +// e.g. asking for the same keys but in a different order can result in a different value to be returned +func GetReloadableFloat64Var(defaultValue float64, orderedKeys ...string) *Reloadable[float64] { + return Default.GetReloadableFloat64Var(defaultValue, orderedKeys...) } // RegisterFloat64ConfigVariable registers float64 config variable -func (c *Config) RegisterFloat64ConfigVariable(defaultValue float64, ptr *float64, isHotReloadable bool, keys ...string) { +// +// WARNING: keys are being looked up in requested order and the value of the first found key is returned, +// e.g. asking for the same keys but in a different order can result in a different value to be returned +// +// Deprecated: use GetFloat64Var or GetReloadableFloat64Var instead +func (c *Config) RegisterFloat64ConfigVariable( + defaultValue float64, ptr *float64, isHotReloadable bool, orderedKeys ...string, +) { + c.registerFloat64Var(defaultValue, ptr, isHotReloadable, func(v float64) { + *ptr = v + }, orderedKeys...) +} + +// GetFloat64Var registers a not hot-reloadable float64 config variable +// +// WARNING: keys are being looked up in requested order and the value of the first found key is returned, +// e.g. asking for the same keys but in a different order can result in a different value to be returned +func (c *Config) GetFloat64Var(defaultValue float64, orderedKeys ...string) float64 { + var ret float64 + c.registerFloat64Var(defaultValue, nil, false, func(v float64) { + ret = v + }, orderedKeys...) + return ret +} + +// GetReloadableFloat64Var registers a hot-reloadable float64 config variable +// +// WARNING: keys are being looked up in requested order and the value of the first found key is returned, +// e.g. asking for the same keys but in a different order can result in a different value to be returned +func (c *Config) GetReloadableFloat64Var(defaultValue float64, orderedKeys ...string) *Reloadable[float64] { + ptr := getOrCreatePointer( + c.reloadableVars, c.reloadableVarsMisuses, &c.reloadableVarsLock, defaultValue, orderedKeys..., + ) + c.registerFloat64Var(defaultValue, ptr, true, func(v float64) { + ptr.store(v) + }, orderedKeys...) + return ptr +} + +func (c *Config) registerFloat64Var( + defaultValue float64, ptr any, isHotReloadable bool, store func(float64), orderedKeys ...string, +) { c.vLock.RLock() defer c.vLock.RUnlock() c.hotReloadableConfigLock.Lock() @@ -79,30 +265,92 @@ func (c *Config) RegisterFloat64ConfigVariable(defaultValue float64, ptr *float6 value: ptr, multiplier: 1.0, defaultValue: defaultValue, - keys: keys, + keys: orderedKeys, } if isHotReloadable { - c.appendVarToConfigMaps(keys[0], &configVar) + c.appendVarToConfigMaps(orderedKeys, &configVar) } - for _, key := range keys { + for _, key := range orderedKeys { c.bindEnv(key) if c.IsSet(key) { - *ptr = c.GetFloat64(key, defaultValue) + store(c.GetFloat64(key, defaultValue)) return } } - *ptr = defaultValue + store(defaultValue) } // RegisterInt64ConfigVariable registers int64 config variable -func RegisterInt64ConfigVariable(defaultValue int64, ptr *int64, isHotReloadable bool, valueScale int64, keys ...string) { - Default.RegisterInt64ConfigVariable(defaultValue, ptr, isHotReloadable, valueScale, keys...) +// +// WARNING: keys are being looked up in requested order and the value of the first found key is returned, +// e.g. asking for the same keys but in a different order can result in a different value to be returned +// +// Deprecated: use GetInt64Var or GetReloadableInt64Var instead +func RegisterInt64ConfigVariable(defaultValue int64, ptr *int64, isHotReloadable bool, valueScale int64, orderedKeys ...string) { + Default.RegisterInt64ConfigVariable(defaultValue, ptr, isHotReloadable, valueScale, orderedKeys...) +} + +// GetInt64Var registers a not hot-reloadable int64 config variable +// +// WARNING: keys are being looked up in requested order and the value of the first found key is returned, +// e.g. asking for the same keys but in a different order can result in a different value to be returned +func GetInt64Var(defaultValue, valueScale int64, orderedKeys ...string) int64 { + return Default.GetInt64Var(defaultValue, valueScale, orderedKeys...) +} + +// GetReloadableInt64Var registers a hot-reloadable int64 config variable +// +// WARNING: keys are being looked up in requested order and the value of the first found key is returned, +// e.g. asking for the same keys but in a different order can result in a different value to be returned +func GetReloadableInt64Var(defaultValue, valueScale int64, orderedKeys ...string) *Reloadable[int64] { + return Default.GetReloadableInt64Var(defaultValue, valueScale, orderedKeys...) } // RegisterInt64ConfigVariable registers int64 config variable -func (c *Config) RegisterInt64ConfigVariable(defaultValue int64, ptr *int64, isHotReloadable bool, valueScale int64, keys ...string) { +// +// WARNING: keys are being looked up in requested order and the value of the first found key is returned, +// e.g. asking for the same keys but in a different order can result in a different value to be returned +// +// Deprecated: use GetInt64Var or GetReloadableInt64Var instead +func (c *Config) RegisterInt64ConfigVariable( + defaultValue int64, ptr *int64, isHotReloadable bool, valueScale int64, orderedKeys ...string, +) { + c.registerInt64Var(defaultValue, ptr, isHotReloadable, valueScale, func(v int64) { + *ptr = v + }, orderedKeys...) +} + +// GetInt64Var registers a not hot-reloadable int64 config variable +// +// WARNING: keys are being looked up in requested order and the value of the first found key is returned, +// e.g. asking for the same keys but in a different order can result in a different value to be returned +func (c *Config) GetInt64Var(defaultValue, valueScale int64, orderedKeys ...string) int64 { + var ret int64 + c.registerInt64Var(defaultValue, nil, false, valueScale, func(v int64) { + ret = v + }, orderedKeys...) + return ret +} + +// GetReloadableInt64Var registers a not hot-reloadable int64 config variable +// +// WARNING: keys are being looked up in requested order and the value of the first found key is returned, +// e.g. asking for the same keys but in a different order can result in a different value to be returned +func (c *Config) GetReloadableInt64Var(defaultValue, valueScale int64, orderedKeys ...string) *Reloadable[int64] { + ptr := getOrCreatePointer( + c.reloadableVars, c.reloadableVarsMisuses, &c.reloadableVarsLock, defaultValue*valueScale, orderedKeys..., + ) + c.registerInt64Var(defaultValue, ptr, true, valueScale, func(v int64) { + ptr.store(v) + }, orderedKeys...) + return ptr +} + +func (c *Config) registerInt64Var( + defaultValue int64, ptr any, isHotReloadable bool, valueScale int64, store func(int64), orderedKeys ...string, +) { c.vLock.RLock() defer c.vLock.RUnlock() c.hotReloadableConfigLock.Lock() @@ -111,30 +359,102 @@ func (c *Config) RegisterInt64ConfigVariable(defaultValue int64, ptr *int64, isH value: ptr, multiplier: valueScale, defaultValue: defaultValue, - keys: keys, + keys: orderedKeys, } if isHotReloadable { - c.appendVarToConfigMaps(keys[0], &configVar) + c.appendVarToConfigMaps(orderedKeys, &configVar) } - for _, key := range keys { + for _, key := range orderedKeys { c.bindEnv(key) if c.IsSet(key) { - *ptr = c.GetInt64(key, defaultValue) * valueScale + store(c.GetInt64(key, defaultValue) * valueScale) return } } - *ptr = defaultValue * valueScale + store(defaultValue * valueScale) } // RegisterDurationConfigVariable registers duration config variable -func RegisterDurationConfigVariable(defaultValueInTimescaleUnits int64, ptr *time.Duration, isHotReloadable bool, timeScale time.Duration, keys ...string) { - Default.RegisterDurationConfigVariable(defaultValueInTimescaleUnits, ptr, isHotReloadable, timeScale, keys...) +// +// WARNING: keys are being looked up in requested order and the value of the first found key is returned, +// e.g. asking for the same keys but in a different order can result in a different value to be returned +// +// Deprecated: use GetDurationVar or GetReloadableDurationVar instead +func RegisterDurationConfigVariable( + defaultValueInTimescaleUnits int64, ptr *time.Duration, isHotReloadable bool, timeScale time.Duration, orderedKeys ...string, +) { + Default.RegisterDurationConfigVariable(defaultValueInTimescaleUnits, ptr, isHotReloadable, timeScale, orderedKeys...) +} + +// GetDurationVar registers a not hot-reloadable duration config variable +// +// WARNING: keys are being looked up in requested order and the value of the first found key is returned, +// e.g. asking for the same keys but in a different order can result in a different value to be returned +func GetDurationVar( + defaultValueInTimescaleUnits int64, timeScale time.Duration, orderedKeys ...string, +) time.Duration { + return Default.GetDurationVar(defaultValueInTimescaleUnits, timeScale, orderedKeys...) +} + +// GetReloadableDurationVar registers a not hot-reloadable duration config variable +// +// WARNING: keys are being looked up in requested order and the value of the first found key is returned, +// e.g. asking for the same keys but in a different order can result in a different value to be returned +func GetReloadableDurationVar(defaultValueInTimescaleUnits int64, timeScale time.Duration, orderedKeys ...string) *Reloadable[time.Duration] { + return Default.GetReloadableDurationVar(defaultValueInTimescaleUnits, timeScale, orderedKeys...) } // RegisterDurationConfigVariable registers duration config variable -func (c *Config) RegisterDurationConfigVariable(defaultValueInTimescaleUnits int64, ptr *time.Duration, isHotReloadable bool, timeScale time.Duration, keys ...string) { +// +// WARNING: keys are being looked up in requested order and the value of the first found key is returned, +// e.g. asking for the same keys but in a different order can result in a different value to be returned +// +// Deprecated: use GetDurationVar or GetReloadableDurationVar instead +func (c *Config) RegisterDurationConfigVariable( + defaultValueInTimescaleUnits int64, ptr *time.Duration, isHotReloadable bool, timeScale time.Duration, orderedKeys ...string, +) { + c.registerDurationVar(defaultValueInTimescaleUnits, ptr, isHotReloadable, timeScale, func(v time.Duration) { + *ptr = v + }, orderedKeys...) +} + +// GetDurationVar registers a not hot-reloadable duration config variable +// +// WARNING: keys are being looked up in requested order and the value of the first found key is returned, +// e.g. asking for the same keys but in a different order can result in a different value to be returned +func (c *Config) GetDurationVar( + defaultValueInTimescaleUnits int64, timeScale time.Duration, orderedKeys ...string, +) time.Duration { + var ret time.Duration + c.registerDurationVar(defaultValueInTimescaleUnits, nil, false, timeScale, func(v time.Duration) { + ret = v + }, orderedKeys...) + return ret +} + +// GetReloadableDurationVar registers a hot-reloadable duration config variable +// +// WARNING: keys are being looked up in requested order and the value of the first found key is returned, +// e.g. asking for the same keys but in a different order can result in a different value to be returned +func (c *Config) GetReloadableDurationVar( + defaultValueInTimescaleUnits int64, timeScale time.Duration, orderedKeys ...string, +) *Reloadable[time.Duration] { + ptr := getOrCreatePointer( + c.reloadableVars, c.reloadableVarsMisuses, &c.reloadableVarsLock, + time.Duration(defaultValueInTimescaleUnits)*timeScale, orderedKeys..., + ) + c.registerDurationVar(defaultValueInTimescaleUnits, ptr, true, timeScale, func(v time.Duration) { + ptr.store(v) + }, orderedKeys...) + return ptr +} + +func (c *Config) registerDurationVar( + defaultValueInTimescaleUnits int64, ptr any, isHotReloadable bool, timeScale time.Duration, + store func(time.Duration), orderedKeys ...string, +) { c.vLock.RLock() defer c.vLock.RUnlock() c.hotReloadableConfigLock.Lock() @@ -143,29 +463,91 @@ func (c *Config) RegisterDurationConfigVariable(defaultValueInTimescaleUnits int value: ptr, multiplier: timeScale, defaultValue: defaultValueInTimescaleUnits, - keys: keys, + keys: orderedKeys, } if isHotReloadable { - c.appendVarToConfigMaps(keys[0], &configVar) + c.appendVarToConfigMaps(orderedKeys, &configVar) } - for _, key := range keys { + for _, key := range orderedKeys { if c.IsSet(key) { - *ptr = c.GetDuration(key, defaultValueInTimescaleUnits, timeScale) + store(c.GetDuration(key, defaultValueInTimescaleUnits, timeScale)) return } } - *ptr = time.Duration(defaultValueInTimescaleUnits) * timeScale + store(time.Duration(defaultValueInTimescaleUnits) * timeScale) } // RegisterStringConfigVariable registers string config variable -func RegisterStringConfigVariable(defaultValue string, ptr *string, isHotReloadable bool, keys ...string) { - Default.RegisterStringConfigVariable(defaultValue, ptr, isHotReloadable, keys...) +// +// WARNING: keys are being looked up in requested order and the value of the first found key is returned, +// e.g. asking for the same keys but in a different order can result in a different value to be returned +// +// Deprecated: use GetStringVar or GetReloadableStringVar instead +func RegisterStringConfigVariable(defaultValue string, ptr *string, isHotReloadable bool, orderedKeys ...string) { + Default.RegisterStringConfigVariable(defaultValue, ptr, isHotReloadable, orderedKeys...) +} + +// GetStringVar registers a not hot-reloadable string config variable +// +// WARNING: keys are being looked up in requested order and the value of the first found key is returned, +// e.g. asking for the same keys but in a different order can result in a different value to be returned +func GetStringVar(defaultValue string, orderedKeys ...string) string { + return Default.GetStringVar(defaultValue, orderedKeys...) +} + +// GetReloadableStringVar registers a hot-reloadable string config variable +// +// WARNING: keys are being looked up in requested order and the value of the first found key is returned, +// e.g. asking for the same keys but in a different order can result in a different value to be returned +func GetReloadableStringVar(defaultValue string, orderedKeys ...string) *Reloadable[string] { + return Default.GetReloadableStringVar(defaultValue, orderedKeys...) } // RegisterStringConfigVariable registers string config variable -func (c *Config) RegisterStringConfigVariable(defaultValue string, ptr *string, isHotReloadable bool, keys ...string) { +// +// WARNING: keys are being looked up in requested order and the value of the first found key is returned, +// e.g. asking for the same keys but in a different order can result in a different value to be returned +// +// Deprecated: use GetStringVar or GetReloadableStringVar instead +func (c *Config) RegisterStringConfigVariable( + defaultValue string, ptr *string, isHotReloadable bool, orderedKeys ...string, +) { + c.registerStringVar(defaultValue, ptr, isHotReloadable, func(v string) { + *ptr = v + }, orderedKeys...) +} + +// GetStringVar registers a not hot-reloadable string config variable +// +// WARNING: keys are being looked up in requested order and the value of the first found key is returned, +// e.g. asking for the same keys but in a different order can result in a different value to be returned +func (c *Config) GetStringVar(defaultValue string, orderedKeys ...string) string { + var ret string + c.registerStringVar(defaultValue, nil, false, func(v string) { + ret = v + }, orderedKeys...) + return ret +} + +// GetReloadableStringVar registers a hot-reloadable string config variable +// +// WARNING: keys are being looked up in requested order and the value of the first found key is returned, +// e.g. asking for the same keys but in a different order can result in a different value to be returned +func (c *Config) GetReloadableStringVar(defaultValue string, orderedKeys ...string) *Reloadable[string] { + ptr := getOrCreatePointer( + c.reloadableVars, c.reloadableVarsMisuses, &c.reloadableVarsLock, defaultValue, orderedKeys..., + ) + c.registerStringVar(defaultValue, ptr, true, func(v string) { + ptr.store(v) + }, orderedKeys...) + return ptr +} + +func (c *Config) registerStringVar( + defaultValue string, ptr any, isHotReloadable bool, store func(string), orderedKeys ...string, +) { c.vLock.RLock() defer c.vLock.RUnlock() c.hotReloadableConfigLock.Lock() @@ -173,29 +555,91 @@ func (c *Config) RegisterStringConfigVariable(defaultValue string, ptr *string, configVar := configValue{ value: ptr, defaultValue: defaultValue, - keys: keys, + keys: orderedKeys, } if isHotReloadable { - c.appendVarToConfigMaps(keys[0], &configVar) + c.appendVarToConfigMaps(orderedKeys, &configVar) } - for _, key := range keys { + for _, key := range orderedKeys { if c.IsSet(key) { - *ptr = c.GetString(key, defaultValue) + store(c.GetString(key, defaultValue)) return } } - *ptr = defaultValue + store(defaultValue) } // RegisterStringSliceConfigVariable registers string slice config variable -func RegisterStringSliceConfigVariable(defaultValue []string, ptr *[]string, isHotReloadable bool, keys ...string) { - Default.RegisterStringSliceConfigVariable(defaultValue, ptr, isHotReloadable, keys...) +// +// WARNING: keys are being looked up in requested order and the value of the first found key is returned, +// e.g. asking for the same keys but in a different order can result in a different value to be returned +// +// Deprecated: use GetStringSliceVar or GetReloadableStringSliceVar instead +func RegisterStringSliceConfigVariable(defaultValue []string, ptr *[]string, isHotReloadable bool, orderedKeys ...string) { + Default.RegisterStringSliceConfigVariable(defaultValue, ptr, isHotReloadable, orderedKeys...) +} + +// GetStringSliceVar registers a not hot-reloadable string slice config variable +// +// WARNING: keys are being looked up in requested order and the value of the first found key is returned, +// e.g. asking for the same keys but in a different order can result in a different value to be returned +func GetStringSliceVar(defaultValue []string, orderedKeys ...string) []string { + return Default.GetStringSliceVar(defaultValue, orderedKeys...) +} + +// GetReloadableStringSliceVar registers a hot-reloadable string slice config variable +// +// WARNING: keys are being looked up in requested order and the value of the first found key is returned, +// e.g. asking for the same keys but in a different order can result in a different value to be returned +func GetReloadableStringSliceVar(defaultValue []string, orderedKeys ...string) *Reloadable[[]string] { + return Default.GetReloadableStringSliceVar(defaultValue, orderedKeys...) } // RegisterStringSliceConfigVariable registers string slice config variable -func (c *Config) RegisterStringSliceConfigVariable(defaultValue []string, ptr *[]string, isHotReloadable bool, keys ...string) { +// +// WARNING: keys are being looked up in requested order and the value of the first found key is returned, +// e.g. asking for the same keys but in a different order can result in a different value to be returned +// +// Deprecated: use GetStringSliceVar or GetReloadableStringSliceVar instead +func (c *Config) RegisterStringSliceConfigVariable( + defaultValue []string, ptr *[]string, isHotReloadable bool, orderedKeys ...string, +) { + c.registerStringSliceVar(defaultValue, ptr, isHotReloadable, func(v []string) { + *ptr = v + }, orderedKeys...) +} + +// GetStringSliceVar registers a not hot-reloadable string slice config variable +// +// WARNING: keys are being looked up in requested order and the value of the first found key is returned, +// e.g. asking for the same keys but in a different order can result in a different value to be returned +func (c *Config) GetStringSliceVar(defaultValue []string, orderedKeys ...string) []string { + var ret []string + c.registerStringSliceVar(defaultValue, ret, false, func(v []string) { + ret = v + }, orderedKeys...) + return ret +} + +// GetReloadableStringSliceVar registers a hot-reloadable string slice config variable +// +// WARNING: keys are being looked up in requested order and the value of the first found key is returned, +// e.g. asking for the same keys but in a different order can result in a different value to be returned +func (c *Config) GetReloadableStringSliceVar(defaultValue []string, orderedKeys ...string) *Reloadable[[]string] { + ptr := getOrCreatePointer( + c.reloadableVars, c.reloadableVarsMisuses, &c.reloadableVarsLock, defaultValue, orderedKeys..., + ) + c.registerStringSliceVar(defaultValue, ptr, true, func(v []string) { + ptr.store(v) + }, orderedKeys...) + return ptr +} + +func (c *Config) registerStringSliceVar( + defaultValue []string, ptr any, isHotReloadable bool, store func([]string), orderedKeys ...string, +) { c.vLock.RLock() defer c.vLock.RUnlock() c.hotReloadableConfigLock.Lock() @@ -203,29 +647,99 @@ func (c *Config) RegisterStringSliceConfigVariable(defaultValue []string, ptr *[ configVar := configValue{ value: ptr, defaultValue: defaultValue, - keys: keys, + keys: orderedKeys, } if isHotReloadable { - c.appendVarToConfigMaps(keys[0], &configVar) + c.appendVarToConfigMaps(orderedKeys, &configVar) } - for _, key := range keys { + for _, key := range orderedKeys { if c.IsSet(key) { - *ptr = c.GetStringSlice(key, defaultValue) + store(c.GetStringSlice(key, defaultValue)) return } } - *ptr = defaultValue + store(defaultValue) } // RegisterStringMapConfigVariable registers string map config variable -func RegisterStringMapConfigVariable(defaultValue map[string]interface{}, ptr *map[string]interface{}, isHotReloadable bool, keys ...string) { - Default.RegisterStringMapConfigVariable(defaultValue, ptr, isHotReloadable, keys...) +// +// WARNING: keys are being looked up in requested order and the value of the first found key is returned, +// e.g. asking for the same keys but in a different order can result in a different value to be returned +// +// Deprecated: use GetStringMapVar or GetReloadableStringMapVar instead +func RegisterStringMapConfigVariable( + defaultValue map[string]interface{}, ptr *map[string]interface{}, isHotReloadable bool, orderedKeys ...string, +) { + Default.RegisterStringMapConfigVariable(defaultValue, ptr, isHotReloadable, orderedKeys...) +} + +// GetStringMapVar registers a not hot-reloadable string map config variable +// +// WARNING: keys are being looked up in requested order and the value of the first found key is returned, +// e.g. asking for the same keys but in a different order can result in a different value to be returned +func GetStringMapVar(defaultValue map[string]interface{}, orderedKeys ...string) map[string]interface{} { + return Default.GetStringMapVar(defaultValue, orderedKeys...) +} + +// GetReloadableStringMapVar registers a hot-reloadable string map config variable +// +// WARNING: keys are being looked up in requested order and the value of the first found key is returned, +// e.g. asking for the same keys but in a different order can result in a different value to be returned +func GetReloadableStringMapVar( + defaultValue map[string]interface{}, orderedKeys ...string, +) *Reloadable[map[string]interface{}] { + return Default.GetReloadableStringMapVar(defaultValue, orderedKeys...) } // RegisterStringMapConfigVariable registers string map config variable -func (c *Config) RegisterStringMapConfigVariable(defaultValue map[string]interface{}, ptr *map[string]interface{}, isHotReloadable bool, keys ...string) { +// +// WARNING: keys are being looked up in requested order and the value of the first found key is returned, +// e.g. asking for the same keys but in a different order can result in a different value to be returned +// +// Deprecated: use GetStringMapVar or GetReloadableStringMapVar instead +func (c *Config) RegisterStringMapConfigVariable( + defaultValue map[string]interface{}, ptr *map[string]interface{}, isHotReloadable bool, orderedKeys ...string, +) { + c.registerStringMapVar(defaultValue, ptr, isHotReloadable, func(v map[string]interface{}) { + *ptr = v + }, orderedKeys...) +} + +// GetStringMapVar registers a not hot-reloadable string map config variable +// +// WARNING: keys are being looked up in requested order and the value of the first found key is returned, +// e.g. asking for the same keys but in a different order can result in a different value to be returned +func (c *Config) GetStringMapVar( + defaultValue map[string]interface{}, orderedKeys ...string, +) map[string]interface{} { + var ret map[string]interface{} + c.registerStringMapVar(defaultValue, nil, false, func(v map[string]interface{}) { + ret = v + }, orderedKeys...) + return ret +} + +// GetReloadableStringMapVar registers a hot-reloadable string map config variable +// +// WARNING: keys are being looked up in requested order and the value of the first found key is returned, +// e.g. asking for the same keys but in a different order can result in a different value to be returned +func (c *Config) GetReloadableStringMapVar( + defaultValue map[string]interface{}, orderedKeys ...string, +) *Reloadable[map[string]interface{}] { + ptr := getOrCreatePointer( + c.reloadableVars, c.reloadableVarsMisuses, &c.reloadableVarsLock, defaultValue, orderedKeys..., + ) + c.registerStringMapVar(defaultValue, ptr, true, func(v map[string]interface{}) { + ptr.store(v) + }, orderedKeys...) + return ptr +} + +func (c *Config) registerStringMapVar( + defaultValue map[string]interface{}, ptr any, isHotReloadable bool, store func(map[string]interface{}), orderedKeys ...string, +) { c.vLock.RLock() defer c.vLock.RUnlock() c.hotReloadableConfigLock.Lock() @@ -233,25 +747,69 @@ func (c *Config) RegisterStringMapConfigVariable(defaultValue map[string]interfa configVar := configValue{ value: ptr, defaultValue: defaultValue, - keys: keys, + keys: orderedKeys, } if isHotReloadable { - c.appendVarToConfigMaps(keys[0], &configVar) + c.appendVarToConfigMaps(orderedKeys, &configVar) } - for _, key := range keys { + for _, key := range orderedKeys { if c.IsSet(key) { - *ptr = c.GetStringMap(key, defaultValue) + store(c.GetStringMap(key, defaultValue)) return } } - *ptr = defaultValue + store(defaultValue) } -func (c *Config) appendVarToConfigMaps(key string, configVar *configValue) { +func (c *Config) appendVarToConfigMaps(keys []string, configVar *configValue) { + key := strings.Join(keys, ",") if _, ok := c.hotReloadableConfig[key]; !ok { c.hotReloadableConfig[key] = make([]*configValue, 0) } c.hotReloadableConfig[key] = append(c.hotReloadableConfig[key], configVar) } + +type configTypes interface { + int | int64 | string | time.Duration | bool | float64 | []string | map[string]interface{} +} + +// Reloadable is used as a wrapper for hot-reloadable config variables +type Reloadable[T configTypes] struct { + value T + lock sync.RWMutex +} + +// Load should be used to read the underlying value without worrying about data races +func (a *Reloadable[T]) Load() T { + a.lock.RLock() + v := a.value + a.lock.RUnlock() + return v +} + +func (a *Reloadable[T]) store(v T) { + a.lock.Lock() + a.value = v + a.lock.Unlock() +} + +// swapIfNotEqual is used internally to swap the value of a hot-reloadable config variable +// if the new value is not equal to the old value +func (a *Reloadable[T]) swapIfNotEqual(new T, compare func(old, new T) bool) (old T, swapped bool) { + a.lock.Lock() + defer a.lock.Unlock() + if !compare(a.value, new) { + old := a.value + a.value = new + return old, true + } + return a.value, false +} + +func compare[T comparable]() func(a, b T) bool { + return func(a, b T) bool { + return a == b + } +} diff --git a/config/load.go b/config/load.go index a8ffc883..99574ed3 100644 --- a/config/load.go +++ b/config/load.go @@ -60,7 +60,7 @@ func (c *Config) checkAndHotReloadConfig(configMap map[string][]*configValue) { for _, configVal := range configValArr { value := configVal.value switch value := value.(type) { - case *int: + case *int, *Reloadable[int]: var _value int var isSet bool for _, key := range configVal.keys { @@ -74,11 +74,8 @@ func (c *Config) checkAndHotReloadConfig(configMap map[string][]*configValue) { _value = configVal.defaultValue.(int) } _value = _value * configVal.multiplier.(int) - if _value != *value { - fmt.Printf("The value of key:%s & variable:%p changed from %d to %d\n", key, configVal, *value, _value) - *value = _value - } - case *int64: + swapHotReloadableConfig(key, "%d", configVal, value, _value, compare[int]()) + case *int64, *Reloadable[int64]: var _value int64 var isSet bool for _, key := range configVal.keys { @@ -92,11 +89,8 @@ func (c *Config) checkAndHotReloadConfig(configMap map[string][]*configValue) { _value = configVal.defaultValue.(int64) } _value = _value * configVal.multiplier.(int64) - if _value != *value { - fmt.Printf("The value of key:%s & variable:%p changed from %d to %d\n", key, configVal, *value, _value) - *value = _value - } - case *string: + swapHotReloadableConfig(key, "%d", configVal, value, _value, compare[int64]()) + case *string, *Reloadable[string]: var _value string var isSet bool for _, key := range configVal.keys { @@ -109,11 +103,8 @@ func (c *Config) checkAndHotReloadConfig(configMap map[string][]*configValue) { if !isSet { _value = configVal.defaultValue.(string) } - if _value != *value { - fmt.Printf("The value of key:%s & variable:%p changed from %v to %v\n", key, configVal, *value, _value) - *value = _value - } - case *time.Duration: + swapHotReloadableConfig(key, "%q", configVal, value, _value, compare[string]()) + case *time.Duration, *Reloadable[time.Duration]: var _value time.Duration var isSet bool for _, key := range configVal.keys { @@ -126,11 +117,8 @@ func (c *Config) checkAndHotReloadConfig(configMap map[string][]*configValue) { if !isSet { _value = time.Duration(configVal.defaultValue.(int64)) * configVal.multiplier.(time.Duration) } - if _value != *value { - fmt.Printf("The value of key:%s & variable:%p changed from %v to %v\n", key, configVal, *value, _value) - *value = _value - } - case *bool: + swapHotReloadableConfig(key, "%d", configVal, value, _value, compare[time.Duration]()) + case *bool, *Reloadable[bool]: var _value bool var isSet bool for _, key := range configVal.keys { @@ -143,11 +131,8 @@ func (c *Config) checkAndHotReloadConfig(configMap map[string][]*configValue) { if !isSet { _value = configVal.defaultValue.(bool) } - if _value != *value { - fmt.Printf("The value of key:%s & variable:%p changed from %v to %v\n", key, configVal, *value, _value) - *value = _value - } - case *float64: + swapHotReloadableConfig(key, "%v", configVal, value, _value, compare[bool]()) + case *float64, *Reloadable[float64]: var _value float64 var isSet bool for _, key := range configVal.keys { @@ -161,11 +146,8 @@ func (c *Config) checkAndHotReloadConfig(configMap map[string][]*configValue) { _value = configVal.defaultValue.(float64) } _value = _value * configVal.multiplier.(float64) - if _value != *value { - fmt.Printf("The value of key:%s & variable:%p changed from %v to %v\n", key, configVal, *value, _value) - *value = _value - } - case *[]string: + swapHotReloadableConfig(key, "%v", configVal, value, _value, compare[float64]()) + case *[]string, *Reloadable[[]string]: var _value []string var isSet bool for _, key := range configVal.keys { @@ -178,11 +160,10 @@ func (c *Config) checkAndHotReloadConfig(configMap map[string][]*configValue) { if !isSet { _value = configVal.defaultValue.([]string) } - if slices.Compare(_value, *value) != 0 { - fmt.Printf("The value of key:%s & variable:%p changed from %v to %v\n", key, configVal, *value, _value) - *value = _value - } - case *map[string]interface{}: + swapHotReloadableConfig(key, "%v", configVal, value, _value, func(a, b []string) bool { + return slices.Compare(a, b) == 0 + }) + case *map[string]interface{}, *Reloadable[map[string]interface{}]: var _value map[string]interface{} var isSet bool for _, key := range configVal.keys { @@ -195,16 +176,35 @@ func (c *Config) checkAndHotReloadConfig(configMap map[string][]*configValue) { if !isSet { _value = configVal.defaultValue.(map[string]interface{}) } - - if !mapDeepEqual(_value, *value) { - fmt.Printf("The value of key:%s & variable:%p changed from %v to %v\n", key, configVal, *value, _value) - *value = _value - } + swapHotReloadableConfig(key, "%v", configVal, value, _value, func(a, b map[string]interface{}) bool { + return mapDeepEqual(a, b) + }) } } } } +func swapHotReloadableConfig[T configTypes]( + key, placeholder string, configVal *configValue, ptr any, newValue T, + compare func(T, T) bool, +) { + if value, ok := ptr.(*T); ok { + if !compare(*value, newValue) { + fmt.Printf("The value of key %q & variable %p changed from "+placeholder+" to "+placeholder+"\n", + key, configVal, *value, newValue, + ) + *value = newValue + } + return + } + reloadableValue, _ := configVal.value.(*Reloadable[T]) + if oldValue, swapped := reloadableValue.swapIfNotEqual(newValue, compare); swapped { + fmt.Printf("The value of key %q & variable %p changed from "+placeholder+" to "+placeholder+"\n", + key, configVal, oldValue, newValue, + ) + } +} + type configValue struct { value interface{} multiplier interface{} @@ -225,13 +225,11 @@ func mapDeepEqual[K comparable, V any](a, b map[K]V) bool { if len(a) != len(b) { return false } - for k, v := range a { if w, ok := b[k]; !ok || !reflect.DeepEqual(v, w) { return false } } - return true } diff --git a/config/reloadable_benchmark_test.go b/config/reloadable_benchmark_test.go new file mode 100644 index 00000000..7af61a07 --- /dev/null +++ b/config/reloadable_benchmark_test.go @@ -0,0 +1,145 @@ +package config + +import ( + "sync" + "sync/atomic" + "testing" + "time" +) + +// BenchmarkReloadable/mutex-24 135314967 8.845 ns/op +// BenchmarkReloadable/rw_mutex-24 132994274 8.715 ns/op +// BenchmarkReloadable/reloadable_value-24 1000000000 0.6007 ns/op +// BenchmarkReloadable/reloadable_custom_mutex-24 77852116 15.19 ns/op +func BenchmarkReloadable(b *testing.B) { + b.Run("mutex", func(b *testing.B) { + var v reloadableMutex[int] + go func() { + for { + v.Store(1) + time.Sleep(time.Millisecond) + } + }() + for i := 0; i < b.N; i++ { + _ = v.Load() + } + }) + b.Run("rw mutex", func(b *testing.B) { + var v reloadableRWMutex[int] + go func() { + for { + v.Store(1) + time.Sleep(time.Millisecond) + } + }() + for i := 0; i < b.N; i++ { + _ = v.Load() + } + }) + b.Run("reloadable value", func(b *testing.B) { + var v reloadableValue[int] + go func() { + for { + v.Store(1) + time.Sleep(time.Millisecond) + } + }() + for i := 0; i < b.N; i++ { + _ = v.Load() + } + }) + b.Run("reloadable custom mutex", func(b *testing.B) { + var v reloadableCustomMutex[int] + go func() { + for { + v.Store(1) + time.Sleep(time.Millisecond) + } + }() + for i := 0; i < b.N; i++ { + _ = v.Load() + } + }) +} + +type reloadableMutex[T comparable] struct { + value T + lock sync.Mutex +} + +func (a *reloadableMutex[T]) Load() T { + a.lock.Lock() + v := a.value + a.lock.Unlock() + return v +} + +func (a *reloadableMutex[T]) Store(v T) { + a.lock.Lock() + a.value = v + a.lock.Unlock() +} + +type reloadableRWMutex[T comparable] struct { + value T + lock sync.RWMutex +} + +func (a *reloadableRWMutex[T]) Load() T { + a.lock.RLock() + v := a.value + a.lock.RUnlock() + return v +} + +func (a *reloadableRWMutex[T]) Store(v T) { + a.lock.Lock() + a.value = v + a.lock.Unlock() +} + +type reloadableValue[T comparable] struct { + // Note: it would also be possible to use reloadable.Pointer to avoid the panics from + // atomic.Value but we won't be able to do the "swapIfNotEqual" as a single transaction anyway + atomic.Value +} + +func (a *reloadableValue[T]) Load() (zero T) { + v := a.Value.Load() + if v == nil { + return zero + } + return v.(T) +} + +func (a *reloadableValue[T]) Store(v T) { + a.Value.Store(v) +} + +type reloadableCustomMutex[T comparable] struct { + value T + mutex int32 +} + +func (a *reloadableCustomMutex[T]) lock() { + for atomic.CompareAndSwapInt32(&a.mutex, 0, 1) { + } +} + +func (a *reloadableCustomMutex[T]) unlock() { + for atomic.CompareAndSwapInt32(&a.mutex, 1, 0) { + } +} + +func (a *reloadableCustomMutex[T]) Load() T { + a.lock() + v := a.value + a.unlock() + return v +} + +func (a *reloadableCustomMutex[T]) Store(v T) { + a.lock() + a.value = v + a.unlock() +} diff --git a/logger/config.go b/logger/config.go index c76603dd..3d3a6f2a 100644 --- a/logger/config.go +++ b/logger/config.go @@ -5,13 +5,15 @@ import ( "sync" "go.uber.org/zap/zapcore" + + "github.com/rudderlabs/rudder-go-kit/config" ) // factoryConfig is the configuration for the logger type factoryConfig struct { - rootLevel int // the level for the root logger - enableNameInLog bool // whether to include the logger name in the log message - enableStackTrace bool // for fatal logs + rootLevel int // the level for the root logger + enableNameInLog bool // whether to include the logger name in the log message + enableStackTrace *config.Reloadable[bool] // for fatal logs levelConfig *syncMap[string, int] // preconfigured log levels for loggers levelConfigCache *syncMap[string, int] // cache of all calculated log levels for loggers diff --git a/logger/factory.go b/logger/factory.go index 987d5689..01cad737 100644 --- a/logger/factory.go +++ b/logger/factory.go @@ -95,7 +95,7 @@ func newConfig(config *config.Config) *factoryConfig { } fc.rootLevel = levelMap[config.GetString("LOG_LEVEL", "INFO")] fc.enableNameInLog = config.GetBool("Logger.enableLoggerNameInLog", true) - config.RegisterBoolConfigVariable(false, &fc.enableStackTrace, true, "Logger.enableStackTrace") + fc.enableStackTrace = config.GetReloadableBoolVar(false, "Logger.enableStackTrace") config.GetBool("Logger.enableLoggerNameInLog", true) // colon separated key value pairs diff --git a/logger/logger.go b/logger/logger.go index 4f06e196..877a97b6 100644 --- a/logger/logger.go +++ b/logger/logger.go @@ -181,7 +181,7 @@ func (l *logger) Fatal(args ...interface{}) { // If enableStackTrace is true, Zaplogger will take care of writing stacktrace to the file. // Else, we are force writing the stacktrace to the file. - if !l.logConfig.enableStackTrace { + if !l.logConfig.enableStackTrace.Load() { byteArr := make([]byte, 2048) n := runtime.Stack(byteArr, false) stackTrace := string(byteArr[:n]) @@ -229,7 +229,7 @@ func (l *logger) Fatalf(format string, args ...interface{}) { // If enableStackTrace is true, Zaplogger will take care of writing stacktrace to the file. // Else, we are force writing the stacktrace to the file. - if !l.logConfig.enableStackTrace { + if !l.logConfig.enableStackTrace.Load() { byteArr := make([]byte, 2048) n := runtime.Stack(byteArr, false) stackTrace := string(byteArr[:n]) @@ -277,7 +277,7 @@ func (l *logger) Fatalw(msg string, keysAndValues ...interface{}) { // If enableStackTrace is true, Zaplogger will take care of writing stacktrace to the file. // Else, we are force writing the stacktrace to the file. - if !l.logConfig.enableStackTrace { + if !l.logConfig.enableStackTrace.Load() { byteArr := make([]byte, 2048) n := runtime.Stack(byteArr, false) stackTrace := string(byteArr[:n])