@@ -5,10 +5,11 @@ package config
55
66import (
77 "context"
8- "os "
8+ "path/filepath "
99 "sync"
1010 "time"
1111
12+ "github.com/fsnotify/fsnotify"
1213 "github.com/pingcap/tiproxy/lib/config"
1314 "github.com/pingcap/tiproxy/lib/util/errors"
1415 "github.com/pingcap/tiproxy/lib/util/waitgroup"
@@ -42,7 +43,7 @@ type ConfigManager struct {
4243
4344 kv * btree.BTreeG [KVValue ]
4445
45- lastModTime time. Time
46+ wch * fsnotify. Watcher
4647 checkFileInterval time.Duration
4748 overlay []byte
4849 sts struct {
@@ -80,22 +81,53 @@ func (e *ConfigManager) Init(ctx context.Context, logger *zap.Logger, configFile
8081 }
8182
8283 if configFile != "" {
83- if err := e .checkFileAndLoad (configFile ); err != nil {
84+ e .wch , err = fsnotify .NewWatcher ()
85+ if err != nil {
86+ return errors .WithStack (err )
87+ }
88+
89+ // Watch the parent dir, because vim/k8s or other apps may not edit files in-place:
90+ // e.g. k8s configmap is a symlink of a symlink to a file, which will only trigger
91+ // a remove event for the file.
92+ parentDir := filepath .Dir (configFile )
93+
94+ if err := e .reloadConfigFile (configFile ); err != nil {
8495 return err
8596 }
97+ if err := e .wch .Add (parentDir ); err != nil {
98+ return errors .WithStack (err )
99+ }
100+
86101 e .wg .Run (func () {
87- var lastErr error
102+ // Some apps will trigger rename/remove events, which means they will re-create/rename
103+ // the new file to the directory. Watch possibly stopped after rename/remove events.
104+ // So, we use a tick to repeatedly add the parent dir to re-watch files.
88105 ticker := time .NewTicker (e .checkFileInterval )
106+ var watchErr error
89107 for {
90108 select {
91109 case <- nctx .Done ():
92110 return
111+ case err := <- e .wch .Errors :
112+ e .logger .Warn ("failed to watch config file" , zap .Error (err ))
113+ watchErr = err
114+ case ev := <- e .wch .Events :
115+ e .handleFSEvent (ev , configFile )
93116 case <- ticker .C :
94- // Do not report the same error to avoid log flooding.
95- if err = e .checkFileAndLoad (configFile ); err != nil && errors .Is (err , lastErr ) {
96- e .logger .Warn ("reload config file failed" , zap .Error (err ))
117+ // There may be a concurrency issue:
118+ // 1. Remove the directory and the watcher removes the directory automatically
119+ // 2. Create the directory and the file again within a tick
120+ // 3. Add it to the watcher again, but the CREATE event is not sent and the file is not loaded
121+ // So if watch failed and succeeds now, reload the file.
122+ if err := e .wch .Add (parentDir ); err != nil {
123+ e .logger .Warn ("failed to rewatch config file" , zap .Error (err ))
124+ watchErr = err
125+ continue
126+ }
127+ if watchErr != nil {
128+ watchErr = e .reloadConfigFile (configFile )
129+ e .logger .Info ("config file reloaded" , zap .Error (watchErr ))
97130 }
98- lastErr = err
99131 }
100132 }
101133 })
@@ -108,25 +140,8 @@ func (e *ConfigManager) Init(ctx context.Context, logger *zap.Logger, configFile
108140 return nil
109141}
110142
111- func (e * ConfigManager ) checkFileAndLoad (filename string ) error {
112- info , err := os .Stat (filename )
113- if err != nil {
114- return errors .WithStack (err )
115- }
116- if info .IsDir () {
117- return errors .New ("config file is a directory" )
118- }
119- if info .ModTime () != e .lastModTime {
120- if err = e .reloadConfigFile (filename ); err != nil {
121- return err
122- }
123- e .logger .Info ("config file reloaded" , zap .Time ("file_modify_time" , info .ModTime ()))
124- e .lastModTime = info .ModTime ()
125- }
126- return nil
127- }
128-
129143func (e * ConfigManager ) Close () error {
144+ var wcherr error
130145 if e .cancel != nil {
131146 e .cancel ()
132147 e .cancel = nil
@@ -138,5 +153,10 @@ func (e *ConfigManager) Close() error {
138153 e .sts .listeners = nil
139154 e .sts .Unlock ()
140155 e .wg .Wait ()
141- return nil
156+ // close after all goroutines are done
157+ if e .wch != nil {
158+ wcherr = e .wch .Close ()
159+ e .wch = nil
160+ }
161+ return wcherr
142162}
0 commit comments