优化结构

This commit is contained in:
Yvan
2026-08-23 07:48:02 +08:00
parent 40632c5756
commit 7a9a738c6a
15 changed files with 590 additions and 83 deletions
+60 -25
View File
@@ -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
})
}
}
+59
View File
@@ -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")
}
}