Skip to content

Commit

Permalink
Add filewatcher/v2
Browse files Browse the repository at this point in the history
Being the first lib implemented in baseplate.go and long predates
generics, the API of filewatcher lacks type safety and can cause other
annoyances.

Add filewatcher/v2 that provides:

* Generics/type safety
* Use io.Closer over Stop()
* Use With- options over a Config struct
* Use slog at error level over log wrapper

And change original filewatcher lib to a thin wrapper of v2.

Also add filewatcher/v2/fwtest and move the fakes there.

This change intentionally does not change any of its users (experiments,
secrets, etc.), as a proof that we did not break v0 API. The next change
will switch them to use v2.
  • Loading branch information
fishy committed May 24, 2024
1 parent f1ad2b4 commit 86a36cc
Show file tree
Hide file tree
Showing 10 changed files with 1,257 additions and 348 deletions.
347 changes: 47 additions & 300 deletions filewatcher/filewatcher.go

Large diffs are not rendered by default.

39 changes: 0 additions & 39 deletions filewatcher/filewatcher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import (
"github.com/google/go-cmp/cmp"

"github.com/reddit/baseplate.go/filewatcher"
"github.com/reddit/baseplate.go/log"
)

const fsEventsDelayForTests = 10 * time.Millisecond
Expand Down Expand Up @@ -107,7 +106,6 @@ func TestFileWatcher(t *testing.T) {
filewatcher.Config{
Path: path,
Parser: parser,
Logger: log.TestWrapper(t),
PollingInterval: c.interval,
FSEventsDelay: fsEventsDelayForTests,
},
Expand Down Expand Up @@ -152,7 +150,6 @@ func TestFileWatcherTimeout(t *testing.T) {
filewatcher.Config{
Path: path,
Parser: parser,
Logger: log.TestWrapper(t),
FSEventsDelay: fsEventsDelayForTests,
},
)
Expand Down Expand Up @@ -196,7 +193,6 @@ func TestFileWatcherRename(t *testing.T) {
filewatcher.Config{
Path: path,
Parser: parser,
Logger: log.TestWrapper(t),
PollingInterval: writeDelay,
FSEventsDelay: fsEventsDelayForTests,
},
Expand Down Expand Up @@ -230,11 +226,6 @@ func TestParserFailure(t *testing.T) {
}
return value, nil
}
var loggerCalled atomic.Int64
logger := func(_ context.Context, msg string) {
loggerCalled.Store(1)
t.Log(msg)
}

dir := t.TempDir()
path := filepath.Join(dir, "foo")
Expand All @@ -247,7 +238,6 @@ func TestParserFailure(t *testing.T) {
filewatcher.Config{
Path: path,
Parser: parser,
Logger: logger,
PollingInterval: -1, // disable polling as we need exact numbers of parser calls in this test
FSEventsDelay: fsEventsDelayForTests,
},
Expand All @@ -270,9 +260,6 @@ func TestParserFailure(t *testing.T) {
}
// Give it some time to handle the file content change
time.Sleep(500 * time.Millisecond)
if loggerCalled.Load() == 0 {
t.Error("Expected logger being called")
}
value = data.Get().(int64)
if value != expected {
t.Errorf("data.Get().(int64) expected %d, got %d", expected, value)
Expand Down Expand Up @@ -363,7 +350,6 @@ func TestFileWatcherDir(t *testing.T) {
filewatcher.Config{
Path: dir,
Parser: filewatcher.WrapDirParser(parser),
Logger: log.TestWrapper(t),
PollingInterval: -1, // disable polling
FSEventsDelay: fsEventsDelayForTests,
},
Expand Down Expand Up @@ -430,22 +416,6 @@ func limitedParser(t *testing.T, expectedSize int64) filewatcher.Parser {
}
}

type logWrapper struct {
called atomic.Int64
}

func (w *logWrapper) wrapper(tb testing.TB) log.Wrapper {
return func(_ context.Context, msg string) {
tb.Helper()
tb.Logf("logger called with msg: %q", msg)
w.called.Add(1)
}
}

func (w *logWrapper) getCalled() int64 {
return w.called.Load()
}

func TestParserSizeLimit(t *testing.T) {
interval := fsEventsDelayForTests
backupInitialReadInterval := filewatcher.InitialReadInterval
Expand Down Expand Up @@ -477,13 +447,11 @@ func TestParserSizeLimit(t *testing.T) {

ctx, cancel := context.WithTimeout(context.Background(), timeout)
t.Cleanup(cancel)
var wrapper logWrapper
data, err := filewatcher.New(
ctx,
filewatcher.Config{
Path: path,
Parser: limitedParser(t, size),
Logger: wrapper.wrapper(t),
MaxFileSize: limit,
PollingInterval: writeDelay,
FSEventsDelay: fsEventsDelayForTests,
Expand All @@ -501,13 +469,6 @@ func TestParserSizeLimit(t *testing.T) {
// We expect the second parse would fail because of the data is beyond the
// hard limit, so the data should still be expectedPayload
compareBytesData(t, data.Get(), expectedPayload)
// Since we expect the second parse would fail, we also expect the logger to
// be called at least once.
// The logger could be called twice because of reload triggered by polling.
const expectedCalledMin = 1
if called := wrapper.getCalled(); called < expectedCalledMin {
t.Errorf("Expected log.Wrapper to be called at least %d times, actual %d", expectedCalledMin, called)
}
}

func TestMockFileWatcher(t *testing.T) {
Expand Down
3 changes: 3 additions & 0 deletions filewatcher/v2/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Package filewatcher provides a go implementation of baseplate's FileWatcher:
// https://baseplate.readthedocs.io/en/stable/api/baseplate/lib/file_watcher.html
package filewatcher
41 changes: 41 additions & 0 deletions filewatcher/v2/doc_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package filewatcher_test

import (
"context"
"encoding/json"
"io"
"time"

"github.com/reddit/baseplate.go/filewatcher/v2"
"github.com/reddit/baseplate.go/log"
)

// This example demonstrates how to use filewatcher.
func Example() {
const (
// The path to the file.
path = "/opt/data.json"
// Timeout on the initial read.
timeout = time.Second * 30
)

// The type of the parsed data
type dataType map[string]any

// Wrap a json decoder as parser
parser := func(f io.Reader) (dataType, error) {
var data dataType
err := json.NewDecoder(f).Decode(&data)
return data, err
}

ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
data, err := filewatcher.New(ctx, path, parser)
if err != nil {
log.Fatal(err)
}

// Whenever you need to use the parsed data, just call data.Get():
_ = data.Get()
}
Loading

0 comments on commit 86a36cc

Please sign in to comment.