Skip to content

Commit

Permalink
feat: add Config.ReloadFiles for reload has been load config files
Browse files Browse the repository at this point in the history
- add new event: reload.data
- add example use fsnotify watch files
  • Loading branch information
inhere committed Oct 9, 2022
1 parent 3e8aeb5 commit 02cd35d
Show file tree
Hide file tree
Showing 5 changed files with 216 additions and 41 deletions.
98 changes: 98 additions & 0 deletions _examples/watch_file.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package main

import (
"github.com/fsnotify/fsnotify"
"github.com/gookit/config/v2"
"github.com/gookit/config/v2/yamlv3"
"github.com/gookit/goutil"
"github.com/gookit/goutil/cliutil"
)

func main() {
config.AddDriver(yamlv3.Driver)
config.WithOptions(
config.ParseEnv,
config.WithHookFunc(func(event string, c *config.Config) {
if event == config.OnReloadData {
cliutil.Cyanln("config reloaded, you can do something ....")
}
}),
)

// load app config files
err := config.LoadFiles(
"testdata/json_base.json",
"testdata/yml_base.yml",
"testdata/yml_other.yml",
)
if err != nil {
panic(err)
}

// mock server running
done := make(chan bool)

// watch loaded config files
err = watchConfigFiles(config.Default())
goutil.PanicErr(err)

cliutil.Infoln("loaded config files is watching ...")
<-done
}

func watchConfigFiles(cfg *config.Config) error {
watcher, err := fsnotify.NewWatcher()
if err != nil {
return err
}
//noinspection GoUnhandledErrorResult
defer watcher.Close()

// get loaded files
files := cfg.LoadedFiles()
if len(files) == 0 {
return nil
}

go func() {
for {
select {
case event, ok := <-watcher.Events:
if !ok { // 'Events' channel is closed
cliutil.Infoln("'Events' channel is closed ...", event)
return
}

// if event.Op > 0 {
cliutil.Infof("file event: %s\n", event)

if event.Op&fsnotify.Write == fsnotify.Write {
cliutil.Infof("modified file: %s\n", event.Name)

err := cfg.ReloadFiles()
if err != nil {
cliutil.Errorf("reload config error: %s\n", err.Error())
}
}
// }

case err, ok := <-watcher.Errors:
if ok { // 'Errors' channel is not closed
cliutil.Errorf("watch file error: %s\n", err.Error())
}
if err != nil {
cliutil.Errorf("watch file error2: %s\n", err.Error())
}
return
}
}
}()

for _, path := range files {
cliutil.Infof("add watch file: %s\n", path)
if err := watcher.Add(path); err != nil {
return err
}
}
return nil
}
1 change: 1 addition & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ type Config struct {
// loaded config files records
loadedFiles []string
driverNames []string
reloading bool

// TODO Deprecated decoder and encoder, use driver instead
// drivers map[string]Driver
Expand Down
116 changes: 79 additions & 37 deletions load.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,14 @@ import (
"github.com/imdario/mergo"
)

// LoadFiles load one or multi files
// LoadFiles load one or multi files, will fire OnLoadData event
//
// Usage:
//
// config.LoadFiles(file1, file2, ...)
func LoadFiles(sourceFiles ...string) error { return dc.LoadFiles(sourceFiles...) }

// LoadFiles load and parse config files
// LoadFiles load and parse config files, will fire OnLoadData event
func (c *Config) LoadFiles(sourceFiles ...string) (err error) {
for _, file := range sourceFiles {
if err = c.loadFile(file, false, ""); err != nil {
Expand All @@ -28,6 +32,10 @@ func (c *Config) LoadFiles(sourceFiles ...string) (err error) {
}

// LoadExists load one or multi files, will ignore not exist
//
// Usage:
//
// config.LoadExists(file1, file2, ...)
func LoadExists(sourceFiles ...string) error { return dc.LoadExists(sourceFiles...) }

// LoadExists load and parse config files, but will ignore not exists file.
Expand Down Expand Up @@ -58,7 +66,6 @@ func (c *Config) LoadRemote(format, url string) (err error) {

//noinspection GoUnhandledErrorResult
defer resp.Body.Close()

if resp.StatusCode != 200 {
return fmt.Errorf("fetch remote config error, reply status code is %d", resp.StatusCode)
}
Expand All @@ -69,7 +76,7 @@ func (c *Config) LoadRemote(format, url string) (err error) {
if err = c.parseSourceCode(format, bts); err != nil {
return
}
c.loadedFiles = append(c.loadedFiles, url)
// c.loadedFiles = append(c.loadedFiles, url)
}
return
}
Expand Down Expand Up @@ -130,47 +137,46 @@ func LoadFlags(keys []string) error { return dc.LoadFlags(keys) }
//
// Usage:
//
// // debug flag is bool type
// // 'debug' flag is bool type
// c.LoadFlags([]string{"env", "debug:bool"})
func (c *Config) LoadFlags(keys []string) (err error) {
hash := map[string]interface{}{}
hash := map[string]int8{}

// bind vars
for _, key := range keys {
key, typ := parseVarNameAndType(key)
desc := "config flag " + key

switch typ {
case "int":
ptr := new(int)
flag.IntVar(ptr, key, c.Int(key), "")
hash[key] = ptr
flag.IntVar(ptr, key, c.Int(key), desc)
hash[key] = 0
case "uint":
ptr := new(uint)
flag.UintVar(ptr, key, c.Uint(key), "")
hash[key] = ptr
flag.UintVar(ptr, key, c.Uint(key), desc)
hash[key] = 0
case "bool":
ptr := new(bool)
flag.BoolVar(ptr, key, c.Bool(key), "")
hash[key] = ptr
flag.BoolVar(ptr, key, c.Bool(key), desc)
hash[key] = 0
default: // as string
ptr := new(string)
flag.StringVar(ptr, key, c.String(key), "")
hash[key] = ptr
flag.StringVar(ptr, key, c.String(key), desc)
hash[key] = 0
}
}

// parse and collect
flag.Parse()
flag.Visit(func(f *flag.Flag) {
name := f.Name
// name := strings.Replace(f.Name, "-", ".", -1)
// only get name in the keys.
if _, ok := hash[name]; !ok {
return
}

// ignore error
_ = c.Set(name, f.Value.String())
_ = c.Set(name, f.Value.String()) // ignore error
})

c.fireHook(OnLoadData)
Expand Down Expand Up @@ -209,7 +215,7 @@ func LoadSources(format string, src []byte, more ...[]byte) error {
//
// Usage:
//
// config.LoadSources(config.Yml, []byte(`
// config.LoadSources(config.Yaml, []byte(`
// name: blog
// arr:
// key: val
Expand Down Expand Up @@ -251,37 +257,72 @@ func (c *Config) LoadStrings(format string, str string, more ...string) (err err
return
}

// LoadFilesByFormat load one or multi files by give format
func LoadFilesByFormat(format string, sourceFiles ...string) error {
return dc.LoadFilesByFormat(format, sourceFiles...)
// LoadFilesByFormat load one or multi config files by give format, will fire OnLoadData event
func LoadFilesByFormat(format string, configFiles ...string) error {
return dc.LoadFilesByFormat(format, configFiles...)
}

// LoadFilesByFormat load one or multi files by give format
func (c *Config) LoadFilesByFormat(format string, sourceFiles ...string) (err error) {
for _, file := range sourceFiles {
// LoadFilesByFormat load one or multi files by give format, will fire OnLoadData event
func (c *Config) LoadFilesByFormat(format string, configFiles ...string) (err error) {
for _, file := range configFiles {
if err = c.loadFile(file, false, format); err != nil {
return
}
}
return
}

// LoadExistsByFormat load one or multi files by give format
func LoadExistsByFormat(format string, sourceFiles ...string) error {
return dc.LoadExistsByFormat(format, sourceFiles...)
// LoadExistsByFormat load one or multi files by give format, will fire OnLoadData event
func LoadExistsByFormat(format string, configFiles ...string) error {
return dc.LoadExistsByFormat(format, configFiles...)
}

// LoadExistsByFormat load one or multi files by give format
func (c *Config) LoadExistsByFormat(format string, sourceFiles ...string) (err error) {
for _, file := range sourceFiles {
// LoadExistsByFormat load one or multi files by give format, will fire OnLoadData event
func (c *Config) LoadExistsByFormat(format string, configFiles ...string) (err error) {
for _, file := range configFiles {
if err = c.loadFile(file, true, format); err != nil {
return
}
}
return
}

// load config file
// ReloadFiles reload config data use loaded files
func ReloadFiles() error { return dc.ReloadFiles() }

// ReloadFiles reload config data use loaded files. use on watching loaded files change
func (c *Config) ReloadFiles() (err error) {
files := c.loadedFiles
if len(files) == 0 {
return
}

data := c.Data()
c.reloading = true
c.ClearCaches()

defer func() {
// revert to back up data on error
if err != nil {
c.data = data
}

c.lock.Unlock()
c.reloading = false

if err == nil {
c.fireHook(OnReloadData)
}
}()

// with lock
c.lock.Lock()

// reload config files
return c.LoadFiles(files...)
}

// load config file, will fire OnLoadData event
func (c *Config) loadFile(file string, loadExist bool, format string) (err error) {
fd, err := os.Open(file)
if err != nil {
Expand All @@ -307,7 +348,9 @@ func (c *Config) loadFile(file string, loadExist bool, format string) (err error
return
}

c.loadedFiles = append(c.loadedFiles, file)
if !c.reloading {
c.loadedFiles = append(c.loadedFiles, file)
}
}
return
}
Expand All @@ -317,16 +360,15 @@ func (c *Config) parseSourceCode(format string, blob []byte) (err error) {
format = fixFormat(format)
decode := c.decoders[format]
if decode == nil {
return errors.New("not exists or not register decoder for the format: " + format)
return errors.New("not register decoder for the format: " + format)
}

if c.opts.Delimiter == 0 {
c.opts.Delimiter = defaultDelimiter
}

data := make(map[string]interface{})

// decode content to data
data := make(map[string]interface{})
if err = decode(blob, &data); err != nil {
return
}
Expand All @@ -336,13 +378,13 @@ func (c *Config) parseSourceCode(format string, blob []byte) (err error) {
c.data = data
} else {
// again ... will merge data
// err = mergo.Map(&c.data, data, mergo.WithOverride)
err = mergo.Merge(&c.data, data, mergo.WithOverride, mergo.WithTypeCheck)
}

if err == nil {
if !c.reloading && err == nil {
c.fireHook(OnLoadData)
}

data = nil
return
}
33 changes: 33 additions & 0 deletions load_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ func TestLoadRemote(t *testing.T) {
is.Error(err)

if runtime.GOOS == "windows" {
t.Skip("skip test load remote on Windows")
return
}

Expand Down Expand Up @@ -239,3 +240,35 @@ func TestLoadOSEnvs(t *testing.T) {

ClearAll()
}

func TestReloadFiles(t *testing.T) {
ClearAll()
c := Default()
// no loaded files
assert.NoError(t, ReloadFiles())

var eventName string
c.WithOptions(WithHookFunc(func(event string, c *Config) {
eventName = event
}))

// load files
err := LoadFiles("testdata/json_base.json", "testdata/json_other.json")
assert.NoError(t, err)
assert.Equal(t, OnLoadData, eventName)
assert.NotEmpty(t, c.LoadedFiles())
assert.Equal(t, "app2", c.String("name"))

// set value
assert.NoError(t, c.Set("name", "new value"))
assert.Equal(t, OnSetValue, eventName)
assert.Equal(t, "new value", c.String("name"))

// reload files
assert.NoError(t, ReloadFiles())
assert.Equal(t, OnReloadData, eventName)

// value is reverted
assert.Equal(t, "app2", c.String("name"))
ClearAll()
}
Loading

0 comments on commit 02cd35d

Please sign in to comment.