carlos / platform Public

relay: test WatchMasters reload (review fix)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Paul Campbell pushed by paul@keymail.dev df42101995345b20fa560291e31eb91038e3e0fc parent f5ac3f5
2 files changed, +63 −4
  • internal/relay/masters.go +8 −4
  • internal/relay/masters_test.go +55 −0
diff --git a/internal/relay/masters.go b/internal/relay/masters.go
index c0ec6ff52..bf6f1c695 100644
--- a/internal/relay/masters.go
+++ b/internal/relay/masters.go
@@ -72,13 +72,17 @@ func (m *Masters) Kids() []string {
return out
}
+// mastersPollInterval is how often WatchMasters checks path's mtime.
+// Overridden in tests to avoid a slow test suite.
+var mastersPollInterval = 5 * time.Second
+
// WatchMasters re-reads path whenever its mtime changes, polling every
-// five seconds (fsnotify would be a dependency for one file). onChange is
-// called with the parse result or error each time the content changes,
-// and once at start.
+// mastersPollInterval (fsnotify would be a dependency for one file).
+// onChange is called with the parse result or error each time the
+// content changes, and once at start.
func WatchMasters(ctx context.Context, path string, onChange func(*Masters, error)) {
var last time.Time
- tick := time.NewTicker(5 * time.Second)
+ tick := time.NewTicker(mastersPollInterval)
defer tick.Stop()
load := func() {
st, err := os.Stat(path)
diff --git a/internal/relay/masters_test.go b/internal/relay/masters_test.go
index 3c19a51f1..1213e2fe5 100644
--- a/internal/relay/masters_test.go
+++ b/internal/relay/masters_test.go
@@ -2,7 +2,10 @@ package relay
import (
"bytes"
+ "context"
"encoding/base64"
+ "os"
+ "path/filepath"
"reflect"
"strings"
"testing"
@@ -45,3 +48,55 @@ func TestParseMastersRejectsBadLines(t *testing.T) {
}
func b64(b []byte) string { return base64.StdEncoding.EncodeToString(b) }
+
+func TestWatchMastersReloadsOnChange(t *testing.T) {
+ old := mastersPollInterval
+ mastersPollInterval = 50 * time.Millisecond
+ defer func() { mastersPollInterval = old }()
+
+ path := filepath.Join(t.TempDir(), "relay-masters")
+ write := func(s string) {
+ if err := os.WriteFile(path, []byte(s), 0o600); err != nil {
+ t.Fatal(err)
+ }
+ }
+ write("k1 " + b64(bytes.Repeat([]byte{1}, 32)) + " -\n")
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ got := make(chan []string, 4)
+ errs := make(chan error, 4)
+ go WatchMasters(ctx, path, func(m *Masters, err error) {
+ if err != nil {
+ errs <- err
+ return
+ }
+ got <- m.Kids()
+ })
+ select {
+ case k := <-got:
+ if !reflect.DeepEqual(k, []string{"k1"}) {
+ t.Fatalf("initial kids = %v", k)
+ }
+ case err := <-errs:
+ t.Fatal(err)
+ case <-time.After(5 * time.Second):
+ t.Fatal("no initial load")
+ }
+ // Make the mtime observably newer, then rewrite with a second kid.
+ time.Sleep(20 * time.Millisecond)
+ write("k1 " + b64(bytes.Repeat([]byte{1}, 32)) + " -\nk2 " + b64(bytes.Repeat([]byte{2}, 32)) + " -\n")
+ future := time.Now().Add(2 * time.Second)
+ if err := os.Chtimes(path, future, future); err != nil {
+ t.Fatal(err)
+ }
+ select {
+ case k := <-got:
+ if !reflect.DeepEqual(k, []string{"k1", "k2"}) {
+ t.Fatalf("reloaded kids = %v", k)
+ }
+ case err := <-errs:
+ t.Fatal(err)
+ case <-time.After(15 * time.Second):
+ t.Fatal("no reload within three poll ticks")
+ }
+}