diff --git a/ldoverrides/file_source_builder.go b/ldoverrides/file_source_builder.go index faacad73..7f7d3c02 100644 --- a/ldoverrides/file_source_builder.go +++ b/ldoverrides/file_source_builder.go @@ -5,6 +5,7 @@ import ( "time" "github.com/launchdarkly/go-server-sdk/v7/internal/filedata" + "github.com/launchdarkly/go-server-sdk/v7/ldfilewatch" "github.com/launchdarkly/go-server-sdk/v7/subsystems" ) @@ -174,5 +175,6 @@ func (b *FileSourceBuilder) Build(context subsystems.ClientContext) (subsystems. changeDetection: b.changeDetection, pollInterval: pollInterval, loggers: loggers, + watchFiles: ldfilewatch.WatchOptionalFiles, }, nil } diff --git a/ldoverrides/file_source_impl.go b/ldoverrides/file_source_impl.go index bc75e5f7..112f090e 100644 --- a/ldoverrides/file_source_impl.go +++ b/ldoverrides/file_source_impl.go @@ -8,7 +8,6 @@ import ( "github.com/launchdarkly/go-sdk-common/v3/ldlog" "github.com/launchdarkly/go-server-sdk/v7/internal/filedata" - "github.com/launchdarkly/go-server-sdk/v7/ldfilewatch" "github.com/launchdarkly/go-server-sdk/v7/subsystems" "github.com/launchdarkly/go-server-sdk/v7/subsystems/ldstoreimpl" "github.com/launchdarkly/go-server-sdk/v7/subsystems/ldstoretypes" @@ -20,6 +19,8 @@ type fileOverrideSource struct { changeDetection ChangeDetection pollInterval time.Duration loggers ldlog.Loggers + // watchFiles sets up watching mode. Tests replace it with one that fails. + watchFiles func(paths []string, loggers ldlog.Loggers, reload func(), closeCh <-chan struct{}) error reloader *filedata.Reloader poller *filedata.Poller @@ -61,9 +62,16 @@ func (f *fileOverrideSource) Start(sink subsystems.OverrideSink) { switch f.changeDetection { case Watching: f.closeWatchCh = make(chan struct{}) - if err := ldfilewatch.WatchOptionalFiles(f.paths, f.loggers, f.reloader.Trigger, f.closeWatchCh); err != nil { - // COVERAGE: constructing a watcher only fails under unusual OS conditions - f.loggers.Errorf("Unable to watch override files: %s", err) + if err := f.watchFiles(f.paths, f.loggers, f.reloader.Trigger, f.closeWatchCh); err != nil { + // The watcher itself cannot be created, for example because the limit on inotify + // instances is reached. Without change detection a later edit would never take effect, + // so the source polls the files instead, no more often than polling mode allows. + interval := f.pollInterval + if interval < MinimumPollInterval { + interval = MinimumPollInterval + } + f.loggers.Errorf("Unable to watch override files; polling them instead every %s: %s", interval, err) + f.poller = filedata.NewPoller(f.paths, interval, f.reloader.Trigger) } case Polling: f.poller = filedata.NewPoller(f.paths, f.pollInterval, f.reloader.Trigger) diff --git a/ldoverrides/file_source_test.go b/ldoverrides/file_source_test.go index 0b725b8b..c16fb65e 100644 --- a/ldoverrides/file_source_test.go +++ b/ldoverrides/file_source_test.go @@ -1,6 +1,7 @@ package ldoverrides import ( + "errors" "github.com/launchdarkly/go-sdk-common/v3/ldlog" "github.com/launchdarkly/go-sdk-common/v3/ldlogtest" "os" @@ -323,6 +324,33 @@ func TestFileSourceWatchingModeReloadsOnChange(t *testing.T) { require.Len(t, flags, 0) } +func TestFileSourceWatchingModeFallsBackToPollingWhenTheWatcherCannotBeCreated(t *testing.T) { + // Without change detection a later edit would never take effect. The source polls instead, at + // no less than the minimum poll interval, which watching mode otherwise ignores. + path := filepath.Join(t.TempDir(), "overrides.json") + writeFile(t, path, `{"flagValues": {"flag1": true}}`) + + mockLog := ldlogtest.NewMockLog() + context := sharedtest.NewTestContext("", nil, &subsystems.LoggingConfiguration{Loggers: mockLog.Loggers}) + source, err := FileSource().FilePaths(path).ChangeDetection(Watching).PollInterval(time.Millisecond).Build(context) + require.NoError(t, err) + source.(*fileOverrideSource).watchFiles = func([]string, ldlog.Loggers, func(), <-chan struct{}) error { + return errors.New("watcher unavailable") + } + sink := newCapturingSink() + source.Start(sink) + t.Cleanup(func() { _ = source.Close() }) + sink.requireSnapshot(t) + + writeFile(t, path, `{"flagValues": {"flag1": true, "flag2": false}}`) + newTime := time.Now().Add(2 * time.Second) + require.NoError(t, os.Chtimes(path, newTime, newTime)) + flags := flagsByKey(t, sink.requireSnapshot(t)) + require.Len(t, flags, 2) + mockLog.AssertMessageMatch(t, true, ldlog.Error, + "Unable to watch override files; polling them instead every 1s: watcher unavailable") +} + func TestFileSourcePollingModeReloadsOnChange(t *testing.T) { path := filepath.Join(t.TempDir(), "overrides.json") writeFile(t, path, `{"flagValues": {"flag1": true}}`)