优化结构
This commit is contained in:
@@ -33,43 +33,77 @@ func (d *Data) watchConfig() func() {
|
||||
return func() {}
|
||||
}
|
||||
done := make(chan struct{})
|
||||
finished := make(chan struct{})
|
||||
var once sync.Once
|
||||
go func() {
|
||||
defer close(finished)
|
||||
var timer *time.Timer
|
||||
var timerC <-chan time.Time
|
||||
events := watcher.Events
|
||||
watchErrors := watcher.Errors
|
||||
for {
|
||||
select {
|
||||
case event, ok := <-watcher.Events:
|
||||
if !ok || filepath.Clean(event.Name) != absolute || event.Op&(fsnotify.Write|fsnotify.Create|fsnotify.Rename) == 0 {
|
||||
case event, ok := <-events:
|
||||
if !ok {
|
||||
events = nil
|
||||
continue
|
||||
}
|
||||
if filepath.Clean(event.Name) != absolute || event.Op&(fsnotify.Write|fsnotify.Create|fsnotify.Rename) == 0 {
|
||||
continue
|
||||
}
|
||||
if timer != nil {
|
||||
timer.Stop()
|
||||
if !timer.Stop() {
|
||||
select {
|
||||
case <-timer.C:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
timer = time.AfterFunc(100*time.Millisecond, func() {
|
||||
next, loadErr := readBootstrap(absolute)
|
||||
if loadErr != nil {
|
||||
logger.Error("reload changed config", "mod", "system", "error", loadErr)
|
||||
return
|
||||
}
|
||||
if next.Data == nil || next.Admin == nil {
|
||||
logger.Error("reload changed config: data and admin configuration are required", "mod", "system")
|
||||
return
|
||||
}
|
||||
if current := d.runtime.Admin(); current != nil {
|
||||
next.Admin.Storage = current.Storage
|
||||
next.Admin.Email = current.Email
|
||||
}
|
||||
next.Admin.ConfigPath = absolute
|
||||
d.runtime.Replace(next.Data, next.Admin)
|
||||
logger.Info("config file changed", "mod", "system", "path", absolute)
|
||||
})
|
||||
case watchErr, ok := <-watcher.Errors:
|
||||
if ok {
|
||||
logger.Error("config watcher error", "mod", "system", "error", watchErr)
|
||||
if timer == nil {
|
||||
timer = time.NewTimer(100 * time.Millisecond)
|
||||
} else {
|
||||
timer.Reset(100 * time.Millisecond)
|
||||
}
|
||||
timerC = timer.C
|
||||
case <-timerC:
|
||||
// The debounce callback runs on this goroutine so shutdown can
|
||||
// wait for it to finish without racing runtime replacement.
|
||||
timerC = nil
|
||||
select {
|
||||
case <-done:
|
||||
continue
|
||||
default:
|
||||
}
|
||||
next, loadErr := readBootstrap(absolute)
|
||||
if loadErr != nil {
|
||||
logger.Error("reload changed config", "mod", "system", "error", loadErr)
|
||||
continue
|
||||
}
|
||||
if next.Data == nil || next.Admin == nil {
|
||||
logger.Error("reload changed config: data and admin configuration are required", "mod", "system")
|
||||
continue
|
||||
}
|
||||
if current := d.runtime.Admin(); current != nil {
|
||||
next.Admin.Storage = current.Storage
|
||||
next.Admin.Email = current.Email
|
||||
}
|
||||
next.Admin.ConfigPath = absolute
|
||||
d.runtime.Replace(next.Data, next.Admin)
|
||||
logger.Info("config file changed", "mod", "system", "path", absolute)
|
||||
case watchErr, ok := <-watchErrors:
|
||||
if !ok {
|
||||
watchErrors = nil
|
||||
continue
|
||||
}
|
||||
logger.Error("config watcher error", "mod", "system", "error", watchErr)
|
||||
case <-done:
|
||||
if timer != nil {
|
||||
timer.Stop()
|
||||
if !timer.Stop() {
|
||||
select {
|
||||
case <-timer.C:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -79,6 +113,7 @@ func (d *Data) watchConfig() func() {
|
||||
once.Do(func() {
|
||||
close(done)
|
||||
_ = watcher.Close()
|
||||
<-finished
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"kra/internal/conf"
|
||||
)
|
||||
|
||||
func TestConfigWatcherStopWaitsForDebouncedReload(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
configPath := filepath.Join(root, "config.yaml")
|
||||
config := []byte("data:\n database:\n driver: sqlite\n path: " + filepath.ToSlash(root) + "\nadmin: {}\n")
|
||||
if err := os.WriteFile(configPath, config, 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
runtime := conf.NewRuntime(&conf.Data{}, &conf.AdminBackend{ConfigPath: configPath})
|
||||
started := make(chan struct{})
|
||||
release := make(chan struct{})
|
||||
var startOnce sync.Once
|
||||
stopListener := runtime.Subscribe(func(*conf.Data, *conf.AdminBackend) {
|
||||
startOnce.Do(func() { close(started) })
|
||||
<-release
|
||||
})
|
||||
defer stopListener()
|
||||
data := &Data{runtime: runtime}
|
||||
stop := data.watchConfig()
|
||||
|
||||
// A write event schedules the 100ms debounce reload.
|
||||
if err := os.WriteFile(configPath, config, 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
select {
|
||||
case <-started:
|
||||
case <-time.After(3 * time.Second):
|
||||
stop()
|
||||
t.Fatal("debounced reload did not start")
|
||||
}
|
||||
|
||||
stopped := make(chan struct{})
|
||||
go func() {
|
||||
stop()
|
||||
close(stopped)
|
||||
}()
|
||||
select {
|
||||
case <-stopped:
|
||||
t.Fatal("watcher stop returned before reload callback finished")
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
}
|
||||
close(release)
|
||||
select {
|
||||
case <-stopped:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("watcher stop did not wait for callback completion")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user