carlos / platform Public

hostfiles: the platform relay logs a stage's failure and recovery once each, as the publisher's loop logs a scan, not every retry (end sweep, task 5)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev 69fd647e4396ce333600c6792a1c0f395da11608 parent 42f0cd8
2 files changed, +49 −2
  • internal/hostfiles/relay.go +12 −1
  • internal/hostfiles/relay_test.go +37 −1
diff --git a/internal/hostfiles/relay.go b/internal/hostfiles/relay.go
index cd10b7cb7..701b50327 100644
--- a/internal/hostfiles/relay.go
+++ b/internal/hostfiles/relay.go
@@ -58,13 +58,20 @@ func (r *PlatformRelay) Run(ctx context.Context) error {
retry = time.Second
}
w := r.signal()
+ failing := false // logged on the transitions only, as Run in run.go logs a scan
for {
r.mu.Lock()
body := r.pending
r.mu.Unlock()
if body != nil {
if err := r.Stage.StagePlatform(ctx, r.Name, body); err != nil {
- log.Printf("hostfiles: stage platform item %s: %v (retrying)", r.Name, err)
+ if ctx.Err() != nil {
+ return ctx.Err()
+ }
+ if !failing {
+ failing = true
+ log.Printf("hostfiles: %s: stage failing, retrying every %s: %v", r.Name, retry, err)
+ }
select {
case <-ctx.Done():
return ctx.Err()
@@ -72,6 +79,10 @@ func (r *PlatformRelay) Run(ctx context.Context) error {
}
continue
}
+ if failing {
+ failing = false
+ log.Printf("hostfiles: %s: stage recovered", r.Name)
+ }
r.mu.Lock()
if string(r.pending) == string(body) {
r.pending = nil
diff --git a/internal/hostfiles/relay_test.go b/internal/hostfiles/relay_test.go
index 73a886c99..7d43c29a8 100644
--- a/internal/hostfiles/relay_test.go
+++ b/internal/hostfiles/relay_test.go
@@ -1,13 +1,43 @@
package hostfiles
import (
+ "bytes"
"context"
"errors"
+ "log"
+ "strings"
"sync"
"testing"
"time"
)
+// relayLog captures the package's log lines for one test.
+type relayLog struct {
+ mu sync.Mutex
+ buf bytes.Buffer
+}
+
+func (l *relayLog) Write(p []byte) (int, error) {
+ l.mu.Lock()
+ defer l.mu.Unlock()
+ return l.buf.Write(p)
+}
+
+func (l *relayLog) count(substr string) int {
+ l.mu.Lock()
+ defer l.mu.Unlock()
+ return strings.Count(l.buf.String(), substr)
+}
+
+func captureRelayLog(t *testing.T) *relayLog {
+ t.Helper()
+ l := &relayLog{}
+ prev := log.Writer()
+ log.SetOutput(l)
+ t.Cleanup(func() { log.SetOutput(prev) })
+ return l
+}
+
type flakyStager struct {
mu sync.Mutex
fail int
@@ -39,8 +69,11 @@ func (f *flakyStager) last() []byte {
}
// Ruling 22: Publish never waits on the store; Run stages the latest body,
-// retrying a failed stage, and stages nothing again once it has.
+// retrying a failed stage, and stages nothing again once it has. Two
+// failed stages log one "failing" line and the stage that follows one
+// "recovered" line: transitions, as run.go logs a scan, not every retry.
func TestPlatformRelayStagesTheLatestBodyWithoutBlockingItsCaller(t *testing.T) {
+ logs := captureRelayLog(t)
st := &flakyStager{fail: 2, block: make(chan struct{})}
r := &PlatformRelay{Name: "instance-key-adoption.json", Stage: st, Retry: 10 * time.Millisecond}
ctx, cancel := context.WithCancel(context.Background())
@@ -69,6 +102,9 @@ func TestPlatformRelayStagesTheLatestBodyWithoutBlockingItsCaller(t *testing.T)
if n != 1 {
t.Fatalf("staged %d times, want once", n)
}
+ if failing, recovered := logs.count(r.Name+": stage failing"), logs.count(r.Name+": stage recovered"); failing != 1 || recovered != 1 {
+ t.Fatalf("logged %d failing and %d recovered lines across two failed stages, want one of each", failing, recovered)
+ }
cancel()
if err := <-done; !errors.Is(err, context.Canceled) {
t.Fatalf("Run = %v", err)