From 3eb3589186ad5b3bd473a48740669b4ba437854b Mon Sep 17 00:00:00 2001 From: Volker Schukai <volker.schukai@schukai.com> Date: Sun, 10 Sep 2023 20:07:09 +0200 Subject: [PATCH] feat: new watcher lib #8 --- Taskfile.yml | 3 + go.mod | 9 +-- go.sum | 12 ++++ import.go | 25 +------- settings.go | 13 +--- watch.go | 173 +++++++++++++------------------------------------- watch_test.go | 65 ++++++++++++------- 7 files changed, 109 insertions(+), 191 deletions(-) diff --git a/Taskfile.yml b/Taskfile.yml index 3465c69..8fd6c58 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -7,11 +7,14 @@ tasks: cmds: - task --list-all silent: true + test: desc: Execute unit tests in Go. cmds: - echo "Execute unit tests in Go." - go test -cover -v ./... + - go test -bench . + - go test -race . test-fuzz: desc: Conduct fuzzing tests.# diff --git a/go.mod b/go.mod index a4e546e..fe1395c 100644 --- a/go.mod +++ b/go.mod @@ -7,13 +7,14 @@ require ( github.com/imdario/mergo v0.3.16 github.com/kinbiko/jsonassert v1.1.1 github.com/magiconair/properties v1.8.7 - github.com/pelletier/go-toml/v2 v2.0.9 + github.com/pelletier/go-toml/v2 v2.1.0 github.com/r3labs/diff/v3 v3.0.1 github.com/stretchr/testify v1.8.4 gitlab.schukai.com/oss/libraries/go/application/xflags v1.9.0 - gitlab.schukai.com/oss/libraries/go/network/http-negotiation v1.3.0 + gitlab.schukai.com/oss/libraries/go/network/http-negotiation v1.3.1 gitlab.schukai.com/oss/libraries/go/utilities/pathfinder v0.5.2 - golang.org/x/exp v0.0.0-20230811145659-89c5cff77bcb + gitlab.schukai.com/oss/libraries/go/utilities/watch v0.2.0 + golang.org/x/exp v0.0.0-20230905200255-921286631fa9 gopkg.in/yaml.v3 v3.0.1 ) @@ -26,7 +27,7 @@ require ( github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect golang.org/x/net v0.8.0 // indirect - golang.org/x/sys v0.11.0 // indirect + golang.org/x/sys v0.12.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.29.0 // indirect ) diff --git a/go.sum b/go.sum index 2bcbd7a..be8d433 100644 --- a/go.sum +++ b/go.sum @@ -27,6 +27,8 @@ github.com/pelletier/go-toml/v2 v2.0.7 h1:muncTPStnKRos5dpVKULv2FVd4bMOhNePj9Cjg github.com/pelletier/go-toml/v2 v2.0.7/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek= github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0= github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= +github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/r3labs/diff/v3 v3.0.0 h1:ZhPwNxn9gW5WLPBV9GCYaVbMdLOSmJ0DeKdCiSbOLUI= @@ -56,12 +58,18 @@ gitlab.schukai.com/oss/libraries/go/application/xflags v1.9.0 h1:bSnwEV56JZQWBQC gitlab.schukai.com/oss/libraries/go/application/xflags v1.9.0/go.mod h1:KN99uofMnTNcpfKwPbskucCTgwivJa3jfP2BHM4Ac+A= gitlab.schukai.com/oss/libraries/go/network/http-negotiation v1.3.0 h1:SZG0BW5ll3WK5ZIOTogjqX8oVHCTxANTDLPxUs7Rnx8= gitlab.schukai.com/oss/libraries/go/network/http-negotiation v1.3.0/go.mod h1:RS2rKf5O+rmSBshHLOgjG7dxg5N2MhNYokZOBcuXdX8= +gitlab.schukai.com/oss/libraries/go/network/http-negotiation v1.3.1 h1:B6BZV3bURUew5u+L/QLaBjdqTlW7P3dHTO19QLkPSfI= +gitlab.schukai.com/oss/libraries/go/network/http-negotiation v1.3.1/go.mod h1:RS2rKf5O+rmSBshHLOgjG7dxg5N2MhNYokZOBcuXdX8= gitlab.schukai.com/oss/libraries/go/utilities/pathfinder v0.3.0 h1:mSxk2q/npskmHMmw1oF4moccjGav5dL6qmff2njUV7A= gitlab.schukai.com/oss/libraries/go/utilities/pathfinder v0.3.0/go.mod h1:UvdD4NAf3gLKYafabJD7e9ZCOetzM9JZ9y4GkZukPVU= gitlab.schukai.com/oss/libraries/go/utilities/pathfinder v0.3.1 h1:oyElaqEiyr2XgaE1CYwD8LoeHsuR/vQD/p6k3jYbJFs= gitlab.schukai.com/oss/libraries/go/utilities/pathfinder v0.3.1/go.mod h1:UvdD4NAf3gLKYafabJD7e9ZCOetzM9JZ9y4GkZukPVU= gitlab.schukai.com/oss/libraries/go/utilities/pathfinder v0.5.2 h1:R+dL2NJCM+AQNPK4DPDmfvx1eomi1Xb1dl0XKEFj7Ek= gitlab.schukai.com/oss/libraries/go/utilities/pathfinder v0.5.2/go.mod h1:UvdD4NAf3gLKYafabJD7e9ZCOetzM9JZ9y4GkZukPVU= +gitlab.schukai.com/oss/libraries/go/utilities/watch v0.1.0 h1:FAKHmf9p3NKyzuM0cIXYBxmhdQ7zJ+6wj5qqeoIMbGc= +gitlab.schukai.com/oss/libraries/go/utilities/watch v0.1.0/go.mod h1:tMFl68peRKHgFQLltrTN3JLredofMqvGi3C0SEAj73Y= +gitlab.schukai.com/oss/libraries/go/utilities/watch v0.2.0 h1:tLjN9Wyv+LJhtiiQDzdzaDelEq2LVCDP3Ndo7ZPIWfQ= +gitlab.schukai.com/oss/libraries/go/utilities/watch v0.2.0/go.mod h1:tMFl68peRKHgFQLltrTN3JLredofMqvGi3C0SEAj73Y= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/exp v0.0.0-20221012211006-4de253d81b95 h1:sBdrWpxhGDdTAYNqbgBLAR+ULAPPhfgncLr1X0lyWtg= golang.org/x/exp v0.0.0-20221012211006-4de253d81b95/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= @@ -75,6 +83,8 @@ golang.org/x/exp v0.0.0-20230810033253-352e893a4cad h1:g0bG7Z4uG+OgH2QDODnjp6ggk golang.org/x/exp v0.0.0-20230810033253-352e893a4cad/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= golang.org/x/exp v0.0.0-20230811145659-89c5cff77bcb h1:mIKbk8weKhSeLH2GmUTrvx8CjkyJmnU1wFmg59CUjFA= golang.org/x/exp v0.0.0-20230811145659-89c5cff77bcb/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20221014081412-f15817d10f9b h1:tvrvnPFcdzp294diPnrdZZZ8XUt2Tyj7svb7X52iDuU= golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= @@ -92,6 +102,8 @@ golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/import.go b/import.go index 2a83855..8fddd9f 100644 --- a/import.go +++ b/import.go @@ -172,32 +172,9 @@ func handleField(p string, r reflect.Value) { } } -//func forEachElem(r reflect.Value, fn func(e reflect.Value)) { -// switch r.Kind() { -// case reflect.Slice: -// for i := 0; i < r.Len(); i++ { -// fn(r.Index(i)) -// } -// case reflect.Map: -// for _, k := range r.MapKeys() { -// fn(r.MapIndex(k)) -// } -// case reflect.Ptr, reflect.Interface: -// if !r.IsNil() { -// fn(r.Elem()) -// } -// } -//} - func (s *Settings[C]) importFiles() { - s.fileWatch.Lock() - - defer func() { - s.fileWatch.Unlock() - }() - - defer func() { + defer func() { s.notifyErrorHooks() }() diff --git a/settings.go b/settings.go index 2014632..55288ec 100644 --- a/settings.go +++ b/settings.go @@ -4,21 +4,12 @@ package configuration import ( + "gitlab.schukai.com/oss/libraries/go/utilities/watch" "reflect" "strconv" "sync" - - "github.com/fsnotify/fsnotify" ) -type fileWatch struct { - sync.Mutex - watcher *fsnotify.Watcher - watchList map[string]bool - cancelWatch chan bool - onWatch bool -} - // Settings is the main struct for the configuration type Settings[C any] struct { files fileBackend @@ -34,7 +25,7 @@ type Settings[C any] struct { postprocessing []PostprocessingHook } - fileWatch fileWatch + fileWatch watch.Lighthouse } func (s *Settings[C]) initDefaults() *Settings[C] { diff --git a/watch.go b/watch.go index 5b90b6e..2dd96a2 100644 --- a/watch.go +++ b/watch.go @@ -4,72 +4,34 @@ package configuration import ( - "github.com/fsnotify/fsnotify" - "os" + "gitlab.schukai.com/oss/libraries/go/utilities/watch" "path" ) -func (s *Settings[C]) initWatch() *Settings[C] { - - var err error - - defer func() { - s.notifyErrorHooks() - }() - - if s.fileWatch.watcher != nil { - s.errors = append(s.errors, WatchAlreadyInitializedError) - return s - } - - s.fileWatch.watcher, err = fsnotify.NewWatcher() - if err != nil { - s.errors = append(s.errors, err) - return s - } - - return s - -} - func (s *Settings[C]) StopWatching() *Settings[C] { - s.fileWatch.Lock() - defer s.fileWatch.Unlock() - - defer func() { - s.notifyErrorHooks() - }() - - if s.fileWatch.watcher == nil { - s.errors = append(s.errors, WatchNotInitializedError) - return s + if s.fileWatch != nil { + err := s.fileWatch.StopWatching() + if err != nil { + s.errors = append(s.errors, err) + return s + } } - if !s.fileWatch.onWatch { - s.errors = append(s.errors, WatchNotRunningError) - return s + // remove all files from watch list + for _, f := range s.files.files { + d := path.Dir(f.path) + err := s.fileWatch.Remove(d) + if err != nil { + s.errors = append(s.errors, err) + } } - s.fileWatch.cancelWatch <- true - - return s -} - -func (s *Settings[C]) buildWatchList() *Settings[C] { - - s.fileWatch.Lock() - defer s.fileWatch.Unlock() - - s.fileWatch.watchList = make(map[string]bool) - for _, d := range s.files.directories { - fn := path.Join(d, s.files.name+s.files.format.Extension()) - s.fileWatch.watchList[fn] = true - } - - for _, f := range s.files.files { - s.fileWatch.watchList[f.path] = true + err := s.fileWatch.Remove(d) + if err != nil { + s.errors = append(s.errors, err) + } } return s @@ -78,99 +40,52 @@ func (s *Settings[C]) buildWatchList() *Settings[C] { // Watch the given file for changes func (s *Settings[C]) Watch() *Settings[C] { - s.buildWatchList() - - s.fileWatch.Lock() - defer s.fileWatch.Unlock() - - defer func() { - s.notifyErrorHooks() - }() - - if s.fileWatch.watcher == nil { - s.initWatch() - } - - if s.fileWatch.watchList == nil { - s.errors = append(s.errors, WatchListNotInitializedError) - return s + if s.fileWatch == nil { + s.fileWatch = watch.NewLighthouse() } - if s.fileWatch.onWatch == true { - s.errors = append(s.errors, WatchAlreadyRunningError) + err := s.fileWatch.StartWatching() + if err != nil { + s.errors = append(s.errors, err) return s } - s.fileWatch.onWatch = true - s.fileWatch.cancelWatch = make(chan bool) - - // remove all files from the watch list - for _, file := range s.fileWatch.watcher.WatchList() { - s.fileWatch.watcher.Remove(file) - } - - // add all files to the watch list - for filePath := range s.fileWatch.watchList { - - fileInfo, err := os.Stat(filePath) - if err != nil { - s.errors = append(s.errors, err) - continue - } + for _, d := range s.files.directories { - if fileInfo.IsDir() { - err = s.fileWatch.watcher.Add(filePath) - } else { - err = s.fileWatch.watcher.Add(path.Dir(filePath)) + w := &watch.Watch{ + Path: d, + OnChange: func(x string) { + if x == d { + s.Import() + } + }, } + err := s.fileWatch.Add(w) if err != nil { s.errors = append(s.errors, err) } } - if len(s.fileWatch.watcher.WatchList()) == 0 { - s.errors = append(s.errors, NoFilesToWatchError) - s.Unlock() - return s - } - - go func() { - finished: - - for { - select { - case event, ok := <-s.fileWatch.watcher.Events: - if !ok { - return - } + for _, f := range s.files.files { - _, exist := s.fileWatch.watchList[event.Name] - if !exist { - continue - } + p := path.Dir(f.path) - if event.Op&fsnotify.Write == fsnotify.Write { - s.Import() - } else if event.Op&fsnotify.Remove == fsnotify.Remove { - s.Import() - } else if event.Op&fsnotify.Rename == fsnotify.Rename { + w := &watch.Watch{ + Path: p, + OnChange: func(x string) { + if x == f.path { s.Import() } - case err, ok := <-s.fileWatch.watcher.Errors: - if !ok { - return - } - s.errors = append(s.errors, err) - - case <-s.fileWatch.cancelWatch: - break finished - } + }, } - s.fileWatch.onWatch = false - }() + err := s.fileWatch.Add(w) + if err != nil { + s.errors = append(s.errors, err) + } + } return s diff --git a/watch_test.go b/watch_test.go index 91e4bff..9c7a2bd 100644 --- a/watch_test.go +++ b/watch_test.go @@ -6,6 +6,7 @@ package configuration import ( "github.com/stretchr/testify/assert" "os" + "sync" "testing" "time" ) @@ -35,11 +36,10 @@ func createTestFileForWatch1() (string, error) { return "", err } - f.WriteString("Host: \"127.0.0.1\"") - f.Close() + _, _ = f.WriteString("Host: \"127.0.0.1\"") + _ = f.Close() return f.Name(), nil - } type testHostTimeout struct { @@ -55,7 +55,12 @@ func TestMultiChange(t *testing.T) { return } - defer os.Remove(tmpFn) + defer func() { + err := os.Remove(tmpFn) + if err != nil { + t.Error(err) + } + }() config := struct { Host string `yaml:"Host"` @@ -67,13 +72,16 @@ func TestMultiChange(t *testing.T) { c.SetMnemonic("my-app") assert.Equal(t, c.Config().Host, "localhost") + var mu sync.Mutex result := []string{} signal := make(chan string) var h ChangeHook h = &ChangeEventHandler{ Callback: func(event ChangeEvent) { + mu.Lock() result = append(result, event.Changelog[0].To.(string)) + mu.Unlock() signal <- event.Changelog[0].To.(string) }, } @@ -96,25 +104,27 @@ func TestMultiChange(t *testing.T) { data := []testHostTimeout{ + // important to have a timeout > 500ms, because the decoupled file watcher will only trigger every 500ms + { host: "1.org", - timeout: time.Millisecond * 100, + timeout: time.Millisecond * 550, }, { host: "2.org", - timeout: time.Millisecond * 10, + timeout: time.Millisecond * 610, }, { host: "3.org", - timeout: time.Millisecond * 2, + timeout: time.Millisecond * 602, }, { host: "4.org", - timeout: time.Millisecond * 100, + timeout: time.Millisecond * 700, }, { host: "9.org", - timeout: time.Millisecond * 100, + timeout: time.Millisecond * 700, }, } @@ -122,14 +132,18 @@ func TestMultiChange(t *testing.T) { go runTestFilesChange(tmpFn, data, t) - for loop := true; loop; { + mu.Lock() + length := len(result) + mu.Unlock() + + for length < len(data) { select { case <-signal: - if len(result) == len(data) { - loop = false - break - } - case <-time.After(time.Second * 10): + mu.Lock() + length = len(result) + mu.Unlock() + continue + case <-time.After(time.Second * 20): t.Log(result) t.Fatalf("Timeout") } @@ -201,7 +215,10 @@ func TestSettingStopWatching(t *testing.T) { return } - defer os.Remove(f.Name()) + defer func() { + _ = os.Remove(f.Name()) + _ = f.Close() + }() config := struct { Host string `yaml:"Host"` @@ -236,8 +253,8 @@ func TestSettingStopWatchingNotOnWatch(t *testing.T) { c := New(config) c.StopWatching() - if !c.HasErrors() { - t.Error("Expected to have an error") + if c.HasErrors() { + t.Error("Expected to have no error") } } @@ -250,7 +267,10 @@ func TestSettingStopWatchingTwice(t *testing.T) { return } - defer os.Remove(f.Name()) + defer func() { + _ = os.Remove(f.Name()) + _ = f.Close() + }() config := struct { Host string `yaml:"Host"` @@ -258,7 +278,7 @@ func TestSettingStopWatchingTwice(t *testing.T) { Host: "localhost", } - f.WriteString("Host: example.org") + _, _ = f.WriteString("Host: example.org") c := New(config) c.AddFile(f.Name(), Yaml) @@ -273,8 +293,7 @@ func TestSettingStopWatchingTwice(t *testing.T) { t.Error("Expected to have an error") } - if e[0] != WatchNotRunningError { - t.Error("Expected to have an error") - } + errText := e[0].Error() + assert.Equal(t, errText, "Watcher is not active") } -- GitLab