chatdesk-ui/auth_v2.169.0/internal/reloader/reloader.go

142 lines
3.3 KiB
Go

// Package reloader provides support for live configuration reloading.
package reloader
import (
"context"
"log"
"strings"
"time"
"github.com/fsnotify/fsnotify"
"github.com/sirupsen/logrus"
"github.com/supabase/auth/internal/conf"
)
const (
// reloadInterval is the interval between configuration reloading. At most
// one configuration change may be made between this duration.
reloadInterval = time.Second * 10
// tickerInterval is the maximum latency between configuration reloads.
tickerInterval = reloadInterval / 10
)
type ConfigFunc func(*conf.GlobalConfiguration)
type Reloader struct {
watchDir string
reloadIval time.Duration
tickerIval time.Duration
}
func NewReloader(watchDir string) *Reloader {
return &Reloader{
watchDir: watchDir,
reloadIval: reloadInterval,
tickerIval: tickerInterval,
}
}
// reload attempts to create a new *conf.GlobalConfiguration after loading the
// currently configured watchDir.
func (rl *Reloader) reload() (*conf.GlobalConfiguration, error) {
if err := conf.LoadDirectory(rl.watchDir); err != nil {
return nil, err
}
cfg, err := conf.LoadGlobalFromEnv()
if err != nil {
return nil, err
}
return cfg, nil
}
// reloadCheckAt checks if reloadConfig should be called, returns true if config
// should be reloaded or false otherwise.
func (rl *Reloader) reloadCheckAt(at, lastUpdate time.Time) bool {
if lastUpdate.IsZero() {
return false // no pending updates
}
if at.Sub(lastUpdate) < rl.reloadIval {
return false // waiting for reload interval
}
// Update is pending.
return true
}
func (rl *Reloader) Watch(ctx context.Context, fn ConfigFunc) error {
wr, err := fsnotify.NewWatcher()
if err != nil {
log.Fatal(err)
}
defer wr.Close()
tr := time.NewTicker(rl.tickerIval)
defer tr.Stop()
// Ignore errors, if watch dir doesn't exist we can add it later.
if err := wr.Add(rl.watchDir); err != nil {
logrus.WithError(err).Error("watch dir failed")
}
var lastUpdate time.Time
for {
select {
case <-ctx.Done():
return ctx.Err()
case <-tr.C:
// This is a simple way to solve watch dir being added later or
// being moved and then recreated. I've tested all of these basic
// scenarios and wr.WatchList() does not grow which aligns with
// the documented behavior.
if err := wr.Add(rl.watchDir); err != nil {
logrus.WithError(err).Error("watch dir failed")
}
// Check to see if the config is ready to be relaoded.
if !rl.reloadCheckAt(time.Now(), lastUpdate) {
continue
}
// Reset the last update time before we try to reload the config.
lastUpdate = time.Time{}
cfg, err := rl.reload()
if err != nil {
logrus.WithError(err).Error("config reload failed")
continue
}
// Call the callback function with the latest cfg.
fn(cfg)
case evt, ok := <-wr.Events:
if !ok {
logrus.WithError(err).Error("fsnotify has exited")
return nil
}
// We only read files ending in .env
if !strings.HasSuffix(evt.Name, ".env") {
continue
}
switch {
case evt.Op.Has(fsnotify.Create),
evt.Op.Has(fsnotify.Remove),
evt.Op.Has(fsnotify.Rename),
evt.Op.Has(fsnotify.Write):
lastUpdate = time.Now()
}
case err, ok := <-wr.Errors:
if !ok {
logrus.Error("fsnotify has exited")
return nil
}
logrus.WithError(err).Error("fsnotify has reported an error")
}
}
}