carlos / platform Public

hostfiles: importing wake state for a host with no route writes nothing

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev 6f4ac2737f2d3dfff8420ff2f41f324fbcba3acd parent 94b80df
2 files changed, +25 −1
  • internal/hostfiles/wake.go +10 −1
  • internal/hostfiles/wake_test.go +15 −0
diff --git a/internal/hostfiles/wake.go b/internal/hostfiles/wake.go
index 5421a3e22..fae6854da 100644
--- a/internal/hostfiles/wake.go
+++ b/internal/hostfiles/wake.go
@@ -69,7 +69,9 @@ func ExportWake(reg *registry.Registry, host string) (WakeState, error) {
}
// ImportWake makes host's wake state exactly ws. It validates everything
-// before it writes anything.
+// and checks that host has a route before it writes anything. It is
+// retry-safe (a repeat is the same full replace) but not atomic across its
+// three writes.
func ImportWake(reg *registry.Registry, host string, ws WakeState) error {
if ws.V != 1 {
return fmt.Errorf("hostfiles: wake state version %d; this binary reads 1", ws.V)
@@ -80,6 +82,13 @@ func ImportWake(reg *registry.Registry, host string, ws WakeState) error {
if !registry.ValidReplication(ws.Replication) {
return fmt.Errorf("hostfiles: wake state for %s names replication class %q", host, ws.Replication)
}
+ rt, err := reg.Get(host)
+ if err != nil {
+ return err
+ }
+ if rt == nil {
+ return fmt.Errorf("hostfiles: wake state for %s: no route", host)
+ }
rows := make([]registry.Timer, 0, len(ws.Timers))
for _, t := range ws.Timers {
rows = append(rows, registry.Timer{Host: host, Name: t.Name, Kind: t.Kind, Spec: t.Spec, Path: t.Path,
diff --git a/internal/hostfiles/wake_test.go b/internal/hostfiles/wake_test.go
index 36b01d234..435d1dc1b 100644
--- a/internal/hostfiles/wake_test.go
+++ b/internal/hostfiles/wake_test.go
@@ -174,3 +174,18 @@ func TestImportWakeOfTheDefaultClassReplacesAnotherClass(t *testing.T) {
t.Fatalf("Replication = %q, want the default", rt.Replication)
}
}
+
+func TestImportWakeForAHostWithNoRouteWritesNothing(t *testing.T) {
+ r := wakeReg(t)
+ if err := r.ReplaceTimers("ghost.test", []registry.Timer{{Host: "ghost.test", Name: "keep", Kind: "schedule", Spec: "@every 1m", Path: "/k", NextAt: 1, CreatedAt: 1}}); err != nil {
+ t.Fatal(err)
+ }
+ ws := WakeState{V: 1, Host: "ghost.test", Timers: []WakeTimer{{Name: "new", Kind: "schedule", Spec: "@every 2m", Path: "/n", NextAt: 2, CreatedAt: 2}}}
+ if err := ImportWake(r, "ghost.test", ws); err == nil {
+ t.Fatal("imported wake state for a host with no route")
+ }
+ rows, _ := r.TimersFor("ghost.test")
+ if len(rows) != 1 || rows[0].Name != "keep" {
+ t.Fatalf("a refused import changed the timers: %+v", rows)
+ }
+}