Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
7634a48
Add fallback config reload for symlinks
douglascamata Jul 5, 2023
67e80a7
Improve pooling config reload
douglascamata Jul 5, 2023
7eab8a3
Make tests more reliable
douglascamata Jul 5, 2023
be66dd1
Improve tests again
douglascamata Jul 5, 2023
0d4544d
Pass the debounce/reload time to each test
douglascamata Jul 5, 2023
8748e22
Add comments and ensure interfaces are implemented
douglascamata Jul 5, 2023
4ebebd6
Make the limit configuration reload time customizable
douglascamata Jul 5, 2023
ea725f2
goimports file
douglascamata Jul 5, 2023
13bb032
Fix lint warning
douglascamata Jul 5, 2023
f3e845f
Improve log for polling engine
douglascamata Jul 5, 2023
19a0b63
Fix tests
douglascamata Jul 5, 2023
3425262
Make linter happy
douglascamata Jul 5, 2023
213244c
Extract symlink identification to a separate function
douglascamata Jul 6, 2023
71d3d9c
Update changelog
douglascamata Jul 6, 2023
13152f1
Fix links to go-grpc-middleware after v2 merge into main
douglascamata Jul 6, 2023
f16d38c
Add period to make linter happy.
douglascamata Jul 6, 2023
e99835e
Update changelog entry
douglascamata Jul 7, 2023
d1d78fe
Replace the fsnotify based engine from `PathContentReloader` with the…
douglascamata Jul 7, 2023
651ed95
Rerun CI
douglascamata Jul 7, 2023
b53770e
Rerun CI
douglascamata Jul 7, 2023
905abab
Remove check for empty filePath
douglascamata Jul 10, 2023
816c1cb
Unexpose pollingEngine.Start
douglascamata Jul 10, 2023
e730722
Fix pollingEngine doc-comment
douglascamata Jul 10, 2023
ef33f1e
Log file path when config is reloaded
douglascamata Jul 10, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions cmd/thanos/receive.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ func runReceive(
return errors.Wrap(err, "parse limit configuration")
}
}
limiter, err := receive.NewLimiter(conf.writeLimitsConfig, reg, receiveMode, log.With(logger, "component", "receive-limiter"))
limiter, err := receive.NewLimiter(conf.writeLimitsConfig, reg, receiveMode, log.With(logger, "component", "receive-limiter"), conf.limitsConfigReloadTimer)
if err != nil {
return errors.Wrap(err, "creating limiter")
}
Expand Down Expand Up @@ -822,8 +822,9 @@ type receiveConfig struct {
reqLogConfig *extflag.PathOrContent
relabelConfigPath *extflag.PathOrContent

writeLimitsConfig *extflag.PathOrContent
storeRateLimits store.SeriesSelectLimits
writeLimitsConfig *extflag.PathOrContent
storeRateLimits store.SeriesSelectLimits
limitsConfigReloadTimer time.Duration
}

func (rc *receiveConfig) registerFlag(cmd extkingpin.FlagClause) {
Expand Down Expand Up @@ -953,6 +954,8 @@ func (rc *receiveConfig) registerFlag(cmd extkingpin.FlagClause) {
rc.reqLogConfig = extkingpin.RegisterRequestLoggingFlags(cmd)

rc.writeLimitsConfig = extflag.RegisterPathOrContent(cmd, "receive.limits-config", "YAML file that contains limit configuration.", extflag.WithEnvSubstitution(), extflag.WithHidden())
cmd.Flag("receive.limits-config-reload-timer", "Minimum amount of time to pass for the limit configuration to be reloaded. Helps to avoid excessive reloads.").
Default("1s").Hidden().DurationVar(&rc.limitsConfigReloadTimer)
}

// determineMode returns the ReceiverMode that this receiver is configured to run in.
Expand Down
132 changes: 113 additions & 19 deletions pkg/extkingpin/path_content_reloader.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package extkingpin

import (
"context"
"crypto/sha256"
"fmt"
"os"
"path"
Expand All @@ -22,32 +23,125 @@ type fileContent interface {
Path() string
}

// PathContentReloader starts a file watcher that monitors the file indicated by fileContent.Path() and runs
// reloadFunc whenever a change is detected.
// A debounce timer can be configured via opts to handle situations where many "write" events are received together or
// a "create" event is followed up by a "write" event, for example. Files will be effectively reloaded at the latest
// after 2 times the debounce timer. By default the debouncer timer is 1 second.
// To ensure renames and deletes are properly handled, the file watcher is put at the file's parent folder. See
// https://github.com/fsnotify/fsnotify/issues/214 for more details.
// PathContentReloader runs the reloadFunc when it detects that the contents of fileContent have changed.
func PathContentReloader(ctx context.Context, fileContent fileContent, logger log.Logger, reloadFunc func(), debounceTime time.Duration) error {
filePath, err := filepath.Abs(fileContent.Path())
if err != nil {
return errors.Wrap(err, "getting absolute file path")
}

watcher, err := fsnotify.NewWatcher()
if filePath == "" {
level.Debug(logger).Log("msg", "no path detected for config reload")
}

// Check if filePath is symlink
filePathStat, err := os.Lstat(filePath)
if err != nil {
return errors.Wrap(err, "getting file info")
}
// Check if filePath's parent folder is symlink
parentFolder := path.Dir(filePath)
parentFolderStat, err := os.Lstat(parentFolder)
if err != nil {
return errors.Wrap(err, "getting parent folder info")
}
var engine reloaderEngine
if filePathStat.Mode()&os.ModeSymlink != 0 || parentFolderStat.Mode()&os.ModeSymlink != 0 {
level.Debug(logger).Log("msg", "file is a symlink, using polling approach", "file", filePath)
engine = &pollingEngine{
filePath: filePath,
logger: logger,
debounce: debounceTime,
reloadFunc: reloadFunc,
}
} else {
engine = &fsNotifyEngine{
filePath: filePath,
logger: logger,
debounce: debounceTime,
reloadFunc: reloadFunc,
}
}
return engine.Start(ctx)
}

// reloaderEngine is an interface that abstracts different underlying logics for reloading a `fileContent`.
type reloaderEngine interface {
Start(ctx context.Context) error
}

// pollingEngine is an implementation of reloaderEngine that keeps rereading the contents at filePath and when the
// checksum changes it runs the reloadFunc.
Comment thread
douglascamata marked this conversation as resolved.
Outdated
type pollingEngine struct {
filePath string
logger log.Logger
debounce time.Duration
reloadFunc func()
previousChecksum [sha256.Size]byte
}

var _ reloaderEngine = &pollingEngine{}

func (p *pollingEngine) Start(ctx context.Context) error {
configReader := func() {
Comment thread
douglascamata marked this conversation as resolved.
// check if file still exists
if _, err := os.Stat(p.filePath); os.IsNotExist(err) {
Comment thread
douglascamata marked this conversation as resolved.
level.Error(p.logger).Log("msg", "file does not exist", "error", err)
return
}
file, err := os.ReadFile(p.filePath)
if err != nil {
level.Error(p.logger).Log("msg", "error opening file", "error", err)
return
}
checksum := sha256.Sum256(file)
if checksum == p.previousChecksum {
return
}
p.reloadFunc()
p.previousChecksum = checksum
level.Debug(p.logger).Log("msg", "configuration reloaded")
Comment thread
douglascamata marked this conversation as resolved.
Outdated
return
Comment thread
douglascamata marked this conversation as resolved.
Outdated
Comment thread
douglascamata marked this conversation as resolved.
Outdated
}
go func() {
for {
select {
case <-ctx.Done():
return
case <-time.After(p.debounce):
configReader()
}
}
}()
return nil
}

// fsNotifyEngine is an implementation of reloaderEngine that uses fsnotify to watch for changes to a file and then
// runs the reloadFunc.
// A debounce timer can be configured via opts to handle situations where many "write" events are received together or
// a "create" event is followed up by a "write" event, for example. Files will be effectively reloaded at the latest
// after 2 times the debounce timer. By default the debouncer timer is 1 second.
// To ensure renames and deletes are properly handled, the file watcher is put at the file's parent folder. See
// https://github.com/fsnotify/fsnotify/issues/214 for more details.
type fsNotifyEngine struct {
filePath string
logger log.Logger
debounce time.Duration
reloadFunc func()
}

var _ reloaderEngine = &fsNotifyEngine{}

func (f *fsNotifyEngine) Start(ctx context.Context) error {
watcher, err := fsnotify.NewWatcher()
if err != nil {
return errors.Wrap(err, "creating file watcher")
}
go func() {
var reloadTimer *time.Timer
if debounceTime != 0 {
reloadTimer = time.AfterFunc(debounceTime, func() {
reloadFunc()
level.Debug(logger).Log("msg", "configuration reloaded after debouncing")
if f.debounce != 0 {
reloadTimer = time.AfterFunc(f.debounce, func() {
f.reloadFunc()
level.Debug(f.logger).Log("msg", "configuration reloaded after debouncing")
})
reloadTimer.Stop()
}
Expand All @@ -67,7 +161,7 @@ func PathContentReloader(ctx context.Context, fileContent fileContent, logger lo
}
// We are watching the file's parent folder (more details on this is done can be found below), but are
// only interested in changed to the target file. Discard every other file as quickly as possible.
if event.Name != filePath {
if event.Name != f.filePath {
break
}
// We only react to files being written or created.
Expand All @@ -76,19 +170,19 @@ func PathContentReloader(ctx context.Context, fileContent fileContent, logger lo
if event.Op&fsnotify.Write == 0 && event.Op&fsnotify.Create == 0 {
break
}
level.Debug(logger).Log("msg", fmt.Sprintf("change detected for %s", filePath), "eventName", event.Name, "eventOp", event.Op)
level.Debug(f.logger).Log("msg", fmt.Sprintf("change detected for %s", f.filePath), "eventName", event.Name, "eventOp", event.Op)
if reloadTimer != nil {
reloadTimer.Reset(debounceTime)
reloadTimer.Reset(f.debounce)
}
case err := <-watcher.Errors:
level.Error(logger).Log("msg", "watcher error", "error", err)
level.Error(f.logger).Log("msg", "watcher error", "error", err)
}
}
}()
// We watch the file's parent folder and not the file itself to better handle DELETE and RENAME events. Check
// https://github.com/fsnotify/fsnotify/issues/214 for more details.
if err := watcher.Add(path.Dir(filePath)); err != nil {
return errors.Wrapf(err, "adding path %s to file watcher", filePath)
if err := watcher.Add(path.Dir(f.filePath)); err != nil {
return errors.Wrapf(err, "adding path %s to file watcher", f.filePath)
}
return nil
}
Expand Down
Loading