kra-new/internal/data/config_watch.go

120 lines
2.9 KiB
Go

package data
import (
"path/filepath"
"sync"
"time"
"github.com/fsnotify/fsnotify"
)
// watchConfig refreshes the in-memory config snapshot on file changes, while
// /system/reloadSystem remains responsible
// for rebuilding database, Redis, storage, and scheduled tasks.
func (d *Data) watchConfig() func() {
logger := d.logger()
configPath := d.runtime.ConfigPath()
if configPath == "" {
return func() {}
}
absolute, err := filepath.Abs(configPath)
if err != nil {
logger.Error("resolve config watch path", "mod", "system", "error", err)
return func() {}
}
watcher, err := fsnotify.NewWatcher()
if err != nil {
logger.Error("create config watcher", "mod", "system", "error", err)
return func() {}
}
if err = watcher.Add(filepath.Dir(absolute)); err != nil {
logger.Error("watch config directory", "mod", "system", "error", err)
_ = watcher.Close()
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 := <-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 {
if !timer.Stop() {
select {
case <-timer.C:
default:
}
}
}
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)
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 {
if !timer.Stop() {
select {
case <-timer.C:
default:
}
}
}
return
}
}
}()
return func() {
once.Do(func() {
close(done)
_ = watcher.Close()
<-finished
})
}
}