Skip to content

Commit 09959f3

Browse files
authored
Dynamic Config (#664)
* dyncfg 90% done * tests, docs, and clean-up * updates * checkers * allow cmp.Equal * fixed dynamic * avoid unsettable * change-notification * go mod tidy * dbg basetext * fix bad import * test logger * dbgFail: is it in baseTest * found 1 blocker for test failure: base was not included right * rm dbg * lint * no pq, fix unmarshal * complex equal * mod tidy & naming * fix Fil cmp panic
1 parent 547a713 commit 09959f3

File tree

17 files changed

+604
-89
lines changed

17 files changed

+604
-89
lines changed

cmd/curio/guidedsetup/guidedsetup.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ type MigrationData struct {
202202
selectTemplates *promptui.SelectTemplates
203203
MinerConfigPath string
204204
DB *harmonydb.DB
205-
HarmonyCfg config.HarmonyDB
205+
HarmonyCfg harmonydb.Config
206206
MinerID address.Address
207207
full api.Chain
208208
cctx *cli.Context

deps/config/cfgdocgen/gen.go

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,10 @@ func run() error {
2525
state := stGlobal
2626

2727
type field struct {
28-
Name string
29-
Type string
30-
Comment string
28+
Name string
29+
Type string
30+
Comment string
31+
IsDynamic bool
3132
}
3233

3334
var currentType string
@@ -73,16 +74,24 @@ func run() error {
7374

7475
name := f[0]
7576
typ := f[1]
77+
isDynamic := false
78+
if strings.HasPrefix(typ, "*Dynamic[") {
79+
isDynamic = true
80+
typ = strings.TrimPrefix(typ, "*Dynamic[")
81+
typ = strings.TrimSuffix(typ, "]")
82+
comment = append(comment, "Updates will affect running instances.")
83+
}
7684

7785
if len(comment) > 0 && strings.HasPrefix(comment[0], fmt.Sprintf("%s is DEPRECATED", name)) {
7886
// don't document deprecated fields
7987
continue
8088
}
8189

8290
out[currentType] = append(out[currentType], field{
83-
Name: name,
84-
Type: typ,
85-
Comment: strings.Join(comment, "\n"),
91+
Name: name,
92+
Type: typ,
93+
Comment: strings.Join(comment, "\n"),
94+
IsDynamic: isDynamic,
8695
})
8796
}
8897
}

deps/config/doc_gen.go

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

deps/config/dynamic.go

Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
package config
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"math/big"
7+
"reflect"
8+
"strings"
9+
"sync"
10+
"time"
11+
12+
"github.com/BurntSushi/toml"
13+
"github.com/google/go-cmp/cmp"
14+
logging "github.com/ipfs/go-log/v2"
15+
16+
"github.com/filecoin-project/curio/harmony/harmonydb"
17+
)
18+
19+
var logger = logging.Logger("config-dynamic")
20+
21+
// bigIntComparer is used to compare big.Int values properly
22+
var bigIntComparer = cmp.Comparer(func(x, y big.Int) bool {
23+
return x.Cmp(&y) == 0
24+
})
25+
26+
type Dynamic[T any] struct {
27+
value T
28+
}
29+
30+
func NewDynamic[T any](value T) *Dynamic[T] {
31+
d := &Dynamic[T]{value: value}
32+
dynamicLocker.notifier[reflect.ValueOf(d).Pointer()] = nil
33+
return d
34+
}
35+
36+
// OnChange registers a function to be called in a goroutine when the dynamic value changes to a new final-layered value.
37+
// The function is called in a goroutine to avoid blocking the main thread; it should not panic.
38+
func (d *Dynamic[T]) OnChange(fn func()) {
39+
p := reflect.ValueOf(d).Pointer()
40+
prev := dynamicLocker.notifier[p]
41+
if prev == nil {
42+
dynamicLocker.notifier[p] = fn
43+
return
44+
}
45+
dynamicLocker.notifier[p] = func() {
46+
prev()
47+
fn()
48+
}
49+
}
50+
51+
func (d *Dynamic[T]) Set(value T) {
52+
dynamicLocker.Lock()
53+
defer dynamicLocker.Unlock()
54+
dynamicLocker.inform(reflect.ValueOf(d).Pointer(), d.value, value)
55+
d.value = value
56+
}
57+
58+
func (d *Dynamic[T]) Get() T {
59+
dynamicLocker.RLock()
60+
defer dynamicLocker.RUnlock()
61+
return d.value
62+
}
63+
64+
// UnmarshalText unmarshals the text into the dynamic value.
65+
// After initial setting, future updates require a lock on the DynamicMx mutex before calling toml.Decode.
66+
func (d *Dynamic[T]) UnmarshalText(text []byte) error {
67+
return toml.Unmarshal(text, &d.value)
68+
}
69+
70+
// MarshalTOML marshals the dynamic value to TOML format.
71+
// If used from deps, requires a lock.
72+
func (d *Dynamic[T]) MarshalTOML() ([]byte, error) {
73+
return toml.Marshal(d.value)
74+
}
75+
76+
// Equal is used by cmp.Equal for custom comparison.
77+
// If used from deps, requires a lock.
78+
func (d *Dynamic[T]) Equal(other *Dynamic[T]) bool {
79+
return cmp.Equal(d.value, other.value, bigIntComparer)
80+
}
81+
82+
type cfgRoot[T any] struct {
83+
db *harmonydb.DB
84+
layers []string
85+
treeCopy T
86+
fixupFn func(string, T) error
87+
}
88+
89+
func EnableChangeDetection[T any](db *harmonydb.DB, obj T, layers []string, fixupFn func(string, T) error) error {
90+
var err error
91+
r := &cfgRoot[T]{db: db, treeCopy: obj, layers: layers, fixupFn: fixupFn}
92+
r.treeCopy, err = CopyWithOriginalDynamics(obj)
93+
if err != nil {
94+
return err
95+
}
96+
go r.changeMonitor()
97+
return nil
98+
}
99+
100+
// copyWithOriginalDynamics copies the original dynamics from the original object to the new object.
101+
func CopyWithOriginalDynamics[T any](orig T) (T, error) {
102+
typ := reflect.TypeOf(orig)
103+
val := reflect.ValueOf(orig)
104+
105+
// Handle pointer to struct
106+
if typ.Kind() == reflect.Ptr {
107+
if typ.Elem().Kind() != reflect.Struct {
108+
var zero T
109+
return zero, fmt.Errorf("expected pointer to struct, got pointer to %s", typ.Elem().Kind())
110+
}
111+
// Create a new instance of the struct
112+
result := reflect.New(typ.Elem())
113+
walker(val.Elem(), result.Elem())
114+
return result.Interface().(T), nil
115+
}
116+
117+
// Handle direct struct
118+
if typ.Kind() != reflect.Struct {
119+
var zero T
120+
return zero, fmt.Errorf("expected struct or pointer to struct, got %s", typ.Kind())
121+
}
122+
123+
result := reflect.New(typ).Elem()
124+
walker(val, result)
125+
return result.Interface().(T), nil
126+
}
127+
128+
// walker recursively walks the struct tree, copying fields and preserving Dynamic pointers
129+
func walker(orig, result reflect.Value) {
130+
for i := 0; i < orig.NumField(); i++ {
131+
field := orig.Field(i)
132+
resultField := result.Field(i)
133+
134+
// Skip unexported fields - they can't be set via reflection
135+
if !resultField.CanSet() {
136+
continue
137+
}
138+
139+
switch field.Kind() {
140+
case reflect.Struct:
141+
// Check if this struct is a Dynamic[T] - if so, copy by value
142+
if isDynamicType(field.Type()) {
143+
resultField.Set(field)
144+
} else {
145+
walker(field, resultField)
146+
}
147+
case reflect.Ptr:
148+
if !field.IsNil() {
149+
// Check if the pointed-to type is Dynamic[T]
150+
elemType := field.Type().Elem()
151+
if isDynamicType(elemType) {
152+
// This is *Dynamic[T] - copy the pointer to preserve sharing
153+
resultField.Set(field)
154+
} else if elemType.Kind() == reflect.Struct {
155+
// Regular struct pointer - recursively copy
156+
newPtr := reflect.New(elemType)
157+
walker(field.Elem(), newPtr.Elem())
158+
resultField.Set(newPtr)
159+
} else {
160+
// Other pointer types - shallow copy
161+
resultField.Set(field)
162+
}
163+
}
164+
default:
165+
resultField.Set(field)
166+
}
167+
}
168+
}
169+
170+
// isDynamicType checks if a type is Dynamic[T] by checking if the name starts with "Dynamic"
171+
func isDynamicType(t reflect.Type) bool {
172+
name := t.Name()
173+
return strings.HasPrefix(name, "Dynamic[")
174+
}
175+
176+
func (r *cfgRoot[T]) changeMonitor() {
177+
lastTimestamp := time.Time{} // lets do a read at startup
178+
179+
for {
180+
configCount := 0
181+
err := r.db.QueryRow(context.Background(), `SELECT COUNT(*) FROM harmony_config WHERE timestamp > $1 AND title IN ($2)`, lastTimestamp, strings.Join(r.layers, ",")).Scan(&configCount)
182+
if err != nil {
183+
logger.Errorf("error selecting configs: %s", err)
184+
continue
185+
}
186+
if configCount == 0 {
187+
continue
188+
}
189+
lastTimestamp = time.Now()
190+
191+
// 1. get all configs
192+
configs, err := GetConfigs(context.Background(), r.db, r.layers)
193+
if err != nil {
194+
logger.Errorf("error getting configs: %s", err)
195+
continue
196+
}
197+
198+
// 2. lock "dynamic" mutex
199+
func() {
200+
dynamicLocker.Lock()
201+
defer dynamicLocker.Unlock()
202+
err := ApplyLayers(context.Background(), r.treeCopy, configs, r.fixupFn)
203+
if err != nil {
204+
logger.Errorf("dynamic config failed to ApplyLayers: %s", err)
205+
return
206+
}
207+
}()
208+
time.Sleep(30 * time.Second)
209+
}
210+
}
211+
212+
var dynamicLocker = changeNotifier{diff: diff{
213+
originally: make(map[uintptr]any),
214+
latest: make(map[uintptr]any),
215+
},
216+
notifier: make(map[uintptr]func()),
217+
}
218+
219+
type changeNotifier struct {
220+
sync.RWMutex // this protects the dynamic[T] reads from getting a race with the updating
221+
updating bool // determines which mode we are in: updating or querying
222+
223+
diff
224+
225+
notifier map[uintptr]func()
226+
}
227+
type diff struct {
228+
cdmx sync.Mutex //
229+
originally map[uintptr]any
230+
latest map[uintptr]any
231+
}
232+
233+
func (c *changeNotifier) Lock() {
234+
c.RWMutex.Lock()
235+
c.updating = true
236+
}
237+
func (c *changeNotifier) Unlock() {
238+
c.cdmx.Lock()
239+
c.RWMutex.Unlock()
240+
defer c.cdmx.Unlock()
241+
242+
c.updating = false
243+
for k, v := range c.latest {
244+
if !cmp.Equal(v, c.originally[k], bigIntComparer) {
245+
if notifier := c.notifier[k]; notifier != nil {
246+
go notifier()
247+
}
248+
}
249+
}
250+
c.originally = make(map[uintptr]any)
251+
c.latest = make(map[uintptr]any)
252+
}
253+
func (c *changeNotifier) inform(ptr uintptr, oldValue any, newValue any) {
254+
if !c.updating {
255+
return
256+
}
257+
c.cdmx.Lock()
258+
defer c.cdmx.Unlock()
259+
if _, ok := c.originally[ptr]; !ok {
260+
c.originally[ptr] = oldValue
261+
}
262+
c.latest[ptr] = newValue
263+
}

0 commit comments

Comments
 (0)