github.com/thanos-io/thanos@v0.32.5/pkg/extkingpin/path_content_reloader.go (about) 1 // Copyright (c) The Thanos Authors. 2 // Licensed under the Apache License 2.0. 3 4 package extkingpin 5 6 import ( 7 "context" 8 "crypto/sha256" 9 "os" 10 "path/filepath" 11 "time" 12 13 "github.com/go-kit/log" 14 "github.com/go-kit/log/level" 15 "github.com/pkg/errors" 16 ) 17 18 type fileContent interface { 19 Content() ([]byte, error) 20 Path() string 21 } 22 23 // PathContentReloader runs the reloadFunc when it detects that the contents of fileContent have changed. 24 func PathContentReloader(ctx context.Context, fileContent fileContent, logger log.Logger, reloadFunc func(), debounceTime time.Duration) error { 25 filePath, err := filepath.Abs(fileContent.Path()) 26 if err != nil { 27 return errors.Wrap(err, "getting absolute file path") 28 } 29 30 engine := &pollingEngine{ 31 filePath: filePath, 32 logger: logger, 33 debounce: debounceTime, 34 reloadFunc: reloadFunc, 35 } 36 return engine.start(ctx) 37 } 38 39 // pollingEngine keeps rereading the contents at filePath and when its checksum changes it runs the reloadFunc. 40 type pollingEngine struct { 41 filePath string 42 logger log.Logger 43 debounce time.Duration 44 reloadFunc func() 45 previousChecksum [sha256.Size]byte 46 } 47 48 func (p *pollingEngine) start(ctx context.Context) error { 49 configReader := func() { 50 // check if file still exists 51 if _, err := os.Stat(p.filePath); os.IsNotExist(err) { 52 level.Error(p.logger).Log("msg", "file does not exist", "error", err) 53 return 54 } 55 file, err := os.ReadFile(p.filePath) 56 if err != nil { 57 level.Error(p.logger).Log("msg", "error opening file", "error", err) 58 return 59 } 60 checksum := sha256.Sum256(file) 61 if checksum == p.previousChecksum { 62 return 63 } 64 p.reloadFunc() 65 p.previousChecksum = checksum 66 level.Debug(p.logger).Log("msg", "configuration reloaded", "path", p.filePath) 67 } 68 go func() { 69 for { 70 select { 71 case <-ctx.Done(): 72 return 73 case <-time.After(p.debounce): 74 configReader() 75 } 76 } 77 }() 78 return nil 79 } 80 81 type staticPathContent struct { 82 content []byte 83 path string 84 } 85 86 var _ fileContent = (*staticPathContent)(nil) 87 88 // Content returns the cached content. 89 func (t *staticPathContent) Content() ([]byte, error) { 90 return t.content, nil 91 } 92 93 // Path returns the path to the file that contains the content. 94 func (t *staticPathContent) Path() string { 95 return t.path 96 } 97 98 // NewStaticPathContent creates a new content that can be used to serve a static configuration. It copies the 99 // configuration from `fromPath` into `destPath` to avoid confusion with file watchers. 100 func NewStaticPathContent(fromPath string) (*staticPathContent, error) { 101 content, err := os.ReadFile(fromPath) 102 if err != nil { 103 return nil, errors.Wrapf(err, "could not load test content: %s", fromPath) 104 } 105 return &staticPathContent{content, fromPath}, nil 106 } 107 108 // Rewrite rewrites the file backing this staticPathContent and swaps the local content cache. The file writing 109 // is needed to trigger the file system monitor. 110 func (t *staticPathContent) Rewrite(newContent []byte) error { 111 t.content = newContent 112 // Write the file to ensure possible file watcher reloaders get triggered. 113 return os.WriteFile(t.path, newContent, 0666) 114 }