github.com/SaurabhDubey-Groww/go-cloud@v0.0.0-20221124105541-b26c29285fd8/runtimevar/filevar/filevar.go (about) 1 // Copyright 2018 The Go Cloud Development Kit Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // https://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package filevar provides a runtimevar implementation with variables 16 // backed by the filesystem. Use OpenVariable to construct a *runtimevar.Variable. 17 // 18 // Configuration files can be updated using any commands (cp, mv) or 19 // tools/editors. This package does not guarantee read consistency since 20 // it does not have control over the writes. For example, some kinds of 21 // updates might result in filevar temporarily receiving an error or an 22 // empty value. 23 // 24 // Known Issues: 25 // 26 // * On macOS, if an empty file is copied into a configuration file, 27 // 28 // filevar will not detect the change. 29 // 30 // # URLs 31 // 32 // For runtimevar.OpenVariable, filevar registers for the scheme "file". 33 // To customize the URL opener, or for more details on the URL format, 34 // see URLOpener. 35 // See https://gocloud.dev/concepts/urls/ for background information. 36 // 37 // # As 38 // 39 // filevar does not support any types for As. 40 package filevar // import "gocloud.dev/runtimevar/filevar" 41 42 import ( 43 "bytes" 44 "context" 45 "errors" 46 "fmt" 47 "io/ioutil" 48 "net/url" 49 "os" 50 "path/filepath" 51 "strings" 52 "time" 53 54 "github.com/fsnotify/fsnotify" 55 "gocloud.dev/gcerrors" 56 "gocloud.dev/runtimevar" 57 "gocloud.dev/runtimevar/driver" 58 ) 59 60 func init() { 61 runtimevar.DefaultURLMux().RegisterVariable(Scheme, &URLOpener{}) 62 } 63 64 // Scheme is the URL scheme filevar registers its URLOpener under on runtimevar.DefaultMux. 65 const Scheme = "file" 66 67 // URLOpener opens filevar URLs like "file:///path/to/config.json?decoder=json". 68 // 69 // The URL's host+path is used as the path to the file to watch. 70 // If os.PathSeparator != "/", any leading "/" from the path is dropped 71 // and remaining '/' characters are converted to os.PathSeparator. 72 // 73 // The following URL parameters are supported: 74 // - decoder: The decoder to use. Defaults to URLOpener.Decoder, or 75 // runtimevar.BytesDecoder if URLOpener.Decoder is nil. 76 // See runtimevar.DecoderByName for supported values. 77 // - wait: The frequency for retries after an error, in time.ParseDuration formats. 78 // Defaults to 30s. 79 type URLOpener struct { 80 // Decoder specifies the decoder to use if one is not specified in the URL. 81 // Defaults to runtimevar.BytesDecoder. 82 Decoder *runtimevar.Decoder 83 84 // Options specifies the options to pass to OpenVariable. 85 Options Options 86 } 87 88 // OpenVariableURL opens the variable at the URL's path. See the package doc 89 // for more details. 90 func (o *URLOpener) OpenVariableURL(ctx context.Context, u *url.URL) (*runtimevar.Variable, error) { 91 q := u.Query() 92 93 decoderName := q.Get("decoder") 94 q.Del("decoder") 95 decoder, err := runtimevar.DecoderByName(ctx, decoderName, o.Decoder) 96 if err != nil { 97 return nil, fmt.Errorf("open variable %v: invalid decoder: %v", u, err) 98 } 99 opts := o.Options 100 if s := q.Get("wait"); s != "" { 101 q.Del("wait") 102 d, err := time.ParseDuration(s) 103 if err != nil { 104 return nil, fmt.Errorf("open variable %v: invalid wait %q: %v", u, s, err) 105 } 106 opts.WaitDuration = d 107 } 108 109 for param := range q { 110 return nil, fmt.Errorf("open variable %v: invalid query parameter %q", u, param) 111 } 112 path := u.Path 113 if os.PathSeparator != '/' { 114 path = strings.TrimPrefix(path, "/") 115 } 116 return OpenVariable(filepath.FromSlash(path), decoder, &opts) 117 } 118 119 // Options sets options. 120 type Options struct { 121 // WaitDuration controls the frequency of retries after an error. For example, 122 // if the file does not exist. Defaults to 30 seconds. 123 WaitDuration time.Duration 124 } 125 126 // OpenVariable constructs a *runtimevar.Variable backed by the file at path. 127 // The file holds raw bytes; provide a decoder to decode the raw bytes into the 128 // appropriate type for runtimevar.Snapshot.Value. 129 // See the runtimevar package documentation for examples of decoders. 130 func OpenVariable(path string, decoder *runtimevar.Decoder, opts *Options) (*runtimevar.Variable, error) { 131 w, err := newWatcher(path, decoder, opts) 132 if err != nil { 133 return nil, err 134 } 135 return runtimevar.New(w), nil 136 } 137 138 func newWatcher(path string, decoder *runtimevar.Decoder, opts *Options) (*watcher, error) { 139 if opts == nil { 140 opts = &Options{} 141 } 142 if path == "" { 143 return nil, errors.New("path is required") 144 } 145 if decoder == nil { 146 return nil, errors.New("decoder is required") 147 } 148 149 // Use absolute file path. 150 abspath, err := filepath.Abs(path) 151 if err != nil { 152 return nil, err 153 } 154 155 // Construct a fsnotify.Watcher. 156 notifier, err := fsnotify.NewWatcher() 157 if err != nil { 158 return nil, err 159 } 160 161 // Create a ctx for the background goroutine that does all of the reading. 162 // The cancel function will be used to shut it down during Close, with the 163 // result being passed back via closeCh. 164 ctx, cancel := context.WithCancel(context.Background()) 165 w := &watcher{ 166 path: abspath, 167 // See struct comments for why it's buffered. 168 ch: make(chan *state, 1), 169 closeCh: make(chan error), 170 shutdown: cancel, 171 } 172 go w.watch(ctx, notifier, abspath, decoder, driver.WaitDuration(opts.WaitDuration)) 173 return w, nil 174 } 175 176 // errNotExist wraps an underlying error in cases where the file likely doesn't 177 // exist. 178 type errNotExist struct { 179 err error 180 } 181 182 func (e *errNotExist) Error() string { 183 return e.err.Error() 184 } 185 186 // state implements driver.State. 187 type state struct { 188 val interface{} 189 updateTime time.Time 190 raw []byte 191 err error 192 } 193 194 func (s *state) Value() (interface{}, error) { 195 return s.val, s.err 196 } 197 198 func (s *state) UpdateTime() time.Time { 199 return s.updateTime 200 } 201 202 func (s *state) As(i interface{}) bool { 203 return false 204 } 205 206 // watcher implements driver.Watcher for configurations stored in files. 207 type watcher struct { 208 // The path for the file we're watching. 209 path string 210 // The background goroutine writes new *state values to ch. 211 // It is buffered so that the background goroutine can write without 212 // blocking; it always drains the buffer before writing so that the latest 213 // write is buffered. If writes could block, the background goroutine could be 214 // blocked indefinitely from reading fsnotify events. 215 ch chan *state 216 // closeCh is used to return any errors from closing the notifier 217 // back to watcher.Close. 218 closeCh chan error 219 // shutdown tells the background goroutine to exit. 220 shutdown func() 221 } 222 223 // WatchVariable implements driver.WatchVariable. 224 func (w *watcher) WatchVariable(ctx context.Context, _ driver.State) (driver.State, time.Duration) { 225 select { 226 case <-ctx.Done(): 227 return &state{err: ctx.Err()}, 0 228 case cur := <-w.ch: 229 return cur, 0 230 } 231 } 232 233 // updateState checks to see if s and prev both represent the same error. 234 // If not, it drains any previous state buffered in w.ch, then writes s to it. 235 // It always return s. 236 func (w *watcher) updateState(s, prev *state) *state { 237 if s.err != nil && prev != nil && prev.err != nil && (s.err == prev.err || s.err.Error() == prev.err.Error() || (os.IsNotExist(s.err) && os.IsNotExist(prev.err))) { 238 // s represents the same error as prev. 239 return s 240 } 241 // Drain any buffered value on ch; it is now stale. 242 select { 243 case <-w.ch: 244 default: 245 } 246 // This write can't block, since we're the only writer, ch has a buffer 247 // size of 1, and we just read anything that was buffered. 248 w.ch <- s 249 return s 250 } 251 252 // watch is run by a background goroutine. 253 // It watches file using notifier, and writes new states to w.ch. 254 // If it can't read or watch the file, it re-checks every wait. 255 // It exits when ctx is canceled, and writes any shutdown errors (or 256 // nil if there weren't any) to w.closeCh. 257 func (w *watcher) watch(ctx context.Context, notifier *fsnotify.Watcher, file string, decoder *runtimevar.Decoder, wait time.Duration) { 258 var cur *state 259 260 for { 261 // If the current state is an error, pause between attempts 262 // to avoid spin loops. In particular, this happens when the file 263 // doesn't exist. 264 if cur != nil && cur.err != nil { 265 select { 266 case <-ctx.Done(): 267 w.closeCh <- notifier.Close() 268 return 269 case <-time.After(wait): 270 } 271 } 272 273 // Add the file to the notifier to be watched. It's fine to be 274 // added multiple times, and fsnotifier is a bit flaky about when 275 // it's needed during renames, so just always try. 276 if err := notifier.Add(file); err != nil { 277 // File probably does not exist. Try again later. 278 cur = w.updateState(&state{err: &errNotExist{err}}, cur) 279 continue 280 } 281 282 // Read the file. 283 b, err := ioutil.ReadFile(file) 284 if err != nil { 285 // File probably does not exist. Try again later. 286 cur = w.updateState(&state{err: &errNotExist{err}}, cur) 287 continue 288 } 289 290 // If it's a new value, decode and return it. 291 if cur == nil || cur.err != nil || !bytes.Equal(cur.raw, b) { 292 if val, err := decoder.Decode(ctx, b); err != nil { 293 cur = w.updateState(&state{err: err}, cur) 294 } else { 295 cur = w.updateState(&state{val: val, updateTime: time.Now(), raw: b}, cur) 296 } 297 } 298 299 // Block until notifier tells us something relevant changed. 300 wait := true 301 for wait { 302 select { 303 case <-ctx.Done(): 304 w.closeCh <- notifier.Close() 305 return 306 307 case event := <-notifier.Events: 308 if event.Name != file { 309 continue 310 } 311 // Ignore if not one of the following operations. 312 if event.Op&(fsnotify.Create|fsnotify.Write|fsnotify.Remove|fsnotify.Rename) == 0 { 313 continue 314 } 315 wait = false 316 317 case err := <-notifier.Errors: 318 cur = w.updateState(&state{err: err}, cur) 319 } 320 } 321 } 322 } 323 324 // Close implements driver.WatchVariable. 325 func (w *watcher) Close() error { 326 // Tell the background goroutine to shut down by canceling its ctx. 327 w.shutdown() 328 // Wait for it to return the result of closing the notifier. 329 err := <-w.closeCh 330 // Cleanup our channels. 331 close(w.ch) 332 close(w.closeCh) 333 return err 334 } 335 336 // ErrorAs implements driver.ErrorAs. 337 func (w *watcher) ErrorAs(err error, i interface{}) bool { return false } 338 339 // ErrorCode implements driver.ErrorCode. 340 func (*watcher) ErrorCode(err error) gcerrors.ErrorCode { 341 if _, ok := err.(*errNotExist); ok { 342 return gcerrors.NotFound 343 } 344 return gcerrors.Unknown 345 }