carlos / platform Public

appconfig: a drop-in read error keeps the pending marker, not clears it

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Paul Campbell pushed by paul@keymail.dev f1d93b5d9c882dacd84afc33334ffd5fe47aba24 parent 04051c4
2 files changed, +40 −1
  • internal/appconfig/unitsync.go +5 −1
  • internal/appconfig/unitsync_test.go +35 −0
diff --git a/internal/appconfig/unitsync.go b/internal/appconfig/unitsync.go
index 5572e727b..a5e217ab1 100644
--- a/internal/appconfig/unitsync.go
+++ b/internal/appconfig/unitsync.go
@@ -212,7 +212,11 @@ func applyStamped(envDir string, ops *UnitOps, plan stampedPlan, work map[string
for _, unit := range pending {
marked[unit] = true
w, prepared := work[unit]
- _, exists, isMarked, _ := readDropIn(ops.DropInRoot, unit)
+ _, exists, isMarked, err := readDropIn(ops.DropInRoot, unit)
+ if err != nil {
+ fmt.Printf(" %s: read drop-in: %v; restart still owed\n", unit, err)
+ continue
+ }
switch {
case prepared && !w.ok:
// Prepared this pass and something failed (env write, drop-in
diff --git a/internal/appconfig/unitsync_test.go b/internal/appconfig/unitsync_test.go
index dd6634b00..530789309 100644
--- a/internal/appconfig/unitsync_test.go
+++ b/internal/appconfig/unitsync_test.go
@@ -434,6 +434,41 @@ func TestSyncUnitsRefusedRouteKeepsAnEarlierMarker(t *testing.T) {
}
}
+func TestSyncUnitsUnreadableDropInKeepsMarker(t *testing.T) {
+ ctx, s, reg, envDir, priv := unitKit(t)
+ f := newFakeUnits(t)
+ f.loaded("relay.service", envDir, "relay.example.com")
+ upsertStamped(t, reg, "relay.example.com", "relay.service", nil)
+ syncOnce(t, ctx, reg, s, priv, envDir, f) // delivered, cleared
+
+ // Make the drop-in unreadable with something other than not-exist: a
+ // directory in its place. os.ReadFile on a directory errors without
+ // os.IsNotExist, exactly the case readDropIn's caller must not treat
+ // as "reaped".
+ dropInPath := filepath.Join(dropInDir(f.root, "relay.service"), DropInFile)
+ if err := os.Remove(dropInPath); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.Mkdir(dropInPath, 0o755); err != nil {
+ t.Fatal(err)
+ }
+ if err := writePending(f.root, "relay.service"); err != nil {
+ t.Fatal(err)
+ }
+ f.restarted = nil
+ // The route is refused this pass (a second route names the unit), so
+ // the only path that could clear or keep the marker is the drop-in
+ // read at the top of the loop.
+ upsertStamped(t, reg, "b.example.com", "relay.service", nil)
+ out := syncOnce(t, ctx, reg, s, priv, envDir, f)
+ if len(f.restarted) != 0 || !hasPending(f.root, "relay.service") {
+ t.Fatalf("unreadable drop-in: restarted=%v pending=%v, want no restart and the marker kept", f.restarted, hasPending(f.root, "relay.service"))
+ }
+ if !strings.Contains(out, "read drop-in") {
+ t.Fatalf("expected a read-drop-in failure line, got %q", out)
+ }
+}
+
func TestEnvFilesMatch(t *testing.T) {
want := "/etc/carlos/apps/h.env"
if !envFilesMatch([]string{want}, want) {