Files
godoxy-yusing/internal/config/events.go

160 lines
4.1 KiB
Go

package config
import (
"errors"
"fmt"
"sync"
"time"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/yusing/godoxy/internal/common"
config "github.com/yusing/godoxy/internal/config/types"
"github.com/yusing/godoxy/internal/notif"
"github.com/yusing/godoxy/internal/watcher"
watcherEvents "github.com/yusing/godoxy/internal/watcher/events"
gperr "github.com/yusing/goutils/errs"
"github.com/yusing/goutils/eventqueue"
"github.com/yusing/goutils/events"
"github.com/yusing/goutils/strings/ansi"
"github.com/yusing/goutils/task"
)
var (
cfgWatcher watcher.Watcher
reloadMu sync.Mutex
)
const configEventFlushInterval = 500 * time.Millisecond
var (
errCfgRenameWarn = errors.New("config file renamed, not reloading; Make sure you rename it back before next time you start")
errCfgDeleteWarn = errors.New(`config file deleted, not reloading; You may run "ls-config" to show or dump the current config`)
)
func logNotifyError(action string, err error) {
log.Error().Err(err).Msg("config " + action + " error")
notif.Notify(&notif.LogMessage{
Level: zerolog.ErrorLevel,
Title: fmt.Sprintf("Config %s error", action),
Body: notif.ErrorBody(err),
})
events.Global.Add(events.NewEvent(events.LevelError, "config", action, err))
}
func logNotifyWarn(action string, err error) {
log.Warn().Err(err).Msg("config " + action + " warning")
notif.Notify(&notif.LogMessage{
Level: zerolog.WarnLevel,
Title: fmt.Sprintf("Config %s warning", action),
Body: notif.ErrorBody(err),
})
events.Global.Add(events.NewEvent(events.LevelWarn, "config", action, err))
}
func Load() error {
if HasState() {
panic(errors.New("config already loaded"))
}
state := NewState()
config.WorkingState.Store(state)
cfgWatcher = watcher.NewConfigFileWatcher(common.ConfigFileName)
initErr := state.InitFromFile(common.ConfigPath)
if initErr != nil {
// if error is critical, notify and return it without starting providers
if criticalErr, ok := errors.AsType[CriticalError](initErr); ok {
logNotifyError("init", criticalErr.err)
return criticalErr
}
}
// disable pool logging temporary since we already have pretty logging
state.Entrypoint().DisablePoolsLog(true)
defer func() {
state.Entrypoint().DisablePoolsLog(false)
}()
err := errors.Join(initErr, state.StartProviders())
if err != nil {
logNotifyError("init", err)
}
state.StartAPIServers()
state.StartMetrics()
SetState(state)
// flush temporary log
state.FlushTmpLog()
return nil
}
func Reload() error {
events.Global.Add(events.NewEvent(events.LevelInfo, "config", "reload", nil))
// avoid race between config change and API reload request
reloadMu.Lock()
defer reloadMu.Unlock()
newState := NewState()
config.WorkingState.Store(newState)
err := newState.InitFromFile(common.ConfigPath)
if err != nil {
newState.Task().FinishAndWait(err)
config.WorkingState.Store(GetState())
return gperr.Wrap(err, ansi.Warning("using last config"))
}
// flush temporary log
newState.FlushTmpLog()
// cancel all current subtasks -> wait
// -> replace config -> start new subtasks
GetState().Task().FinishAndWait(config.ErrConfigChanged)
SetState(newState)
if err := newState.StartProviders(); err != nil {
logNotifyError("start providers", err)
return nil // continue
}
newState.StartAPIServers()
newState.StartMetrics()
return nil
}
func WatchChanges() {
opts := eventqueue.Options[watcherEvents.Event]{
FlushInterval: configEventFlushInterval,
OnFlush: OnConfigChange,
OnError: func(err error) {
logNotifyError("reload", err)
},
Debug: common.IsDebug,
}
t := task.RootTask("config_watcher", true)
eventQueue := eventqueue.New(t, opts)
eventQueue.Start(cfgWatcher.Events(t.Context()))
}
func OnConfigChange(ev []watcherEvents.Event) {
// no matter how many events during the interval
// just reload once and check the last event
switch ev[len(ev)-1].Action {
case watcherEvents.ActionFileRenamed:
logNotifyWarn("rename", errCfgRenameWarn)
return
case watcherEvents.ActionFileDeleted:
logNotifyWarn("delete", errCfgDeleteWarn)
return
}
if err := Reload(); err != nil {
// recovered in event queue
panic(err)
}
}