Skip to content

Commit 40ac028

Browse files
authored
Warn on dynamic config default values with shared structure (#8176)
## What changed? Log softassert warnings if dynamic config settings are registered with default values with shared structure. ## Why? This is very likely unintended and may lead to unexpected behavior of settings (values will be parsed on top of a copy of the default). ## How did you test it? - [x] run locally and tested manually - [x] added new unit test(s)
1 parent 9c75cd6 commit 40ac028

File tree

5 files changed

+144
-3
lines changed

5 files changed

+144
-3
lines changed

cmd/tools/gendynamicconfig/dynamic_config.tmpl

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ type {{$P.Name}}TypedConstrainedDefaultSetting[T any] constrainedDefaultSetting[
2828
// values. The value from dynamic config will be _merged_ over a deep copy of 'def'. Be very careful
2929
// when using non-empty maps or slices as defaults, the result may not be what you want.
3030
func New{{$P.Name}}TypedSetting[T any](key Key, def T, description string) {{$P.Name}}TypedSetting[T] {
31+
// Warn on any shared structure used with ConvertStructure, even though we handle it by deep copying.
32+
warnDefaultSharedStructure(key, def)
33+
// If even deep copy won't even work, we should panic early. Do that by calling deep copy once here.
34+
_ = deepCopyForMapstructure(def)
35+
3136
s := {{$P.Name}}TypedSetting[T]{
3237
key: key,
3338
def: def,
@@ -40,6 +45,7 @@ func New{{$P.Name}}TypedSetting[T any](key Key, def T, description string) {{$P.
4045

4146
// New{{$P.Name}}TypedSettingWithConverter creates a setting with a custom converter function.
4247
func New{{$P.Name}}TypedSettingWithConverter[T any](key Key, convert func(any) (T, error), def T, description string) {{$P.Name}}TypedSetting[T] {
48+
{{/* Do not warn on shared structure here, it's the converter's concern. */ -}}
4349
s := {{$P.Name}}TypedSetting[T]{
4450
key: key,
4551
def: def,
@@ -52,6 +58,7 @@ func New{{$P.Name}}TypedSettingWithConverter[T any](key Key, convert func(any) (
5258

5359
// New{{$P.Name}}TypedSettingWithConstrainedDefault creates a setting with a compound default value.
5460
func New{{$P.Name}}TypedSettingWithConstrainedDefault[T any](key Key, convert func(any) (T, error), cdef []TypedConstrainedValue[T], description string) {{$P.Name}}TypedConstrainedDefaultSetting[T] {
61+
{{/* Do not warn on shared structure here, it's the converter's concern. */ -}}
5562
s := {{$P.Name}}TypedConstrainedDefaultSetting[T]{
5663
key: key,
5764
cdef: cdef,

common/dynamicconfig/collection.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,9 @@ var (
104104
// NewCollection creates a new collection. For subscriptions to work, you must call Start/Stop.
105105
// Get will work without Start/Stop.
106106
func NewCollection(client Client, logger log.Logger) *Collection {
107+
// Do this at the first convenient place we have a logger:
108+
logSharedStructureWarnings(logger)
109+
107110
return &Collection{
108111
client: client,
109112
logger: logger,
@@ -576,9 +579,6 @@ func convertMap(val any) (map[string]any, error) {
576579
// treat the fields independently), or the zero value of its type (if you want to treat the fields
577580
// as a group and default unset fields to zero).
578581
func ConvertStructure[T any](def T) func(v any) (T, error) {
579-
// call deepCopyForMapstructure once to surface any potential panic at static init time.
580-
_ = deepCopyForMapstructure(def)
581-
582582
return func(v any) (T, error) {
583583
// if we already have the right type, no conversion is necessary
584584
if typedV, ok := v.(T); ok {

common/dynamicconfig/setting_gen.go

Lines changed: 35 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package dynamicconfig
2+
3+
import (
4+
"fmt"
5+
"reflect"
6+
"sync"
7+
8+
"go.temporal.io/server/common/log"
9+
"go.temporal.io/server/common/softassert"
10+
)
11+
12+
var (
13+
// These should all run at static init time, but use synchronization just in case.
14+
sharedStructureWarnings sync.Map
15+
logSharedStructureWarningsOnce sync.Once
16+
)
17+
18+
func warnDefaultSharedStructure(key Key, def any) {
19+
if path := hasSharedStructure(reflect.ValueOf(def), "root"); path != "" {
20+
sharedStructureWarnings.Store(key, path)
21+
}
22+
}
23+
24+
func logSharedStructureWarnings(logger log.Logger) {
25+
logSharedStructureWarningsOnce.Do(func() {
26+
sharedStructureWarnings.Range(func(key, path any) bool {
27+
softassert.Fail(logger, fmt.Sprintf("default value for %v contains shared structure at %v", key, path))
28+
return true
29+
})
30+
})
31+
}
32+
33+
func hasSharedStructure(v reflect.Value, path string) string {
34+
// nolint:exhaustive // deliberately not exhaustive
35+
switch v.Kind() {
36+
case reflect.Map, reflect.Slice, reflect.Pointer:
37+
if !v.IsNil() {
38+
return path
39+
}
40+
case reflect.Interface:
41+
if !v.IsNil() {
42+
return hasSharedStructure(v.Elem(), path)
43+
}
44+
case reflect.Struct:
45+
for i := range v.NumField() {
46+
if p := hasSharedStructure(v.Field(i), path+"."+v.Type().Field(i).Name); p != "" {
47+
return p
48+
}
49+
}
50+
case reflect.Array:
51+
for i := range v.Len() {
52+
if p := hasSharedStructure(v.Index(i), path+"."+fmt.Sprintf("[%d]", i)); p != "" {
53+
return p
54+
}
55+
}
56+
}
57+
return ""
58+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package dynamicconfig
2+
3+
import (
4+
"reflect"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
)
9+
10+
func TestHasSharedStructure(t *testing.T) {
11+
var v any = 3
12+
assert.Empty(t, hasSharedStructure(reflect.ValueOf(v), "root"))
13+
14+
v = struct {
15+
i int
16+
s string
17+
c struct{ p *string }
18+
}{i: 5, s: "hi"}
19+
assert.Empty(t, hasSharedStructure(reflect.ValueOf(v), "root"))
20+
21+
v = [5]*float64{nil, nil, nil, nil, nil}
22+
assert.Empty(t, hasSharedStructure(reflect.ValueOf(v), "root"))
23+
24+
f := float64(35.124)
25+
v = [5]*float64{nil, nil, &f, nil, nil}
26+
assert.Equal(t, "root.[2]", hasSharedStructure(reflect.ValueOf(v), "root"))
27+
28+
v = []byte(nil)
29+
assert.Empty(t, hasSharedStructure(reflect.ValueOf(v), "root"))
30+
31+
v = []byte{}
32+
assert.Equal(t, "root", hasSharedStructure(reflect.ValueOf(v), "root"))
33+
34+
str := "test"
35+
v = struct {
36+
i int
37+
s string
38+
c struct{ p *string }
39+
}{c: struct{ p *string }{p: &str}}
40+
assert.Equal(t, "root.c.p", hasSharedStructure(reflect.ValueOf(v), "root"))
41+
}

0 commit comments

Comments
 (0)