carlos / platform Public

appconfig: gate the unit pass's instance-overlay Get on Backing == BackingUnit

Sync ran overlayFor for every syncable route and only checked
rt.Backing != registry.BackingUnit 53 lines later, so it paid a Get for
every exec-backed route with an overlay -- exactly the routes step 5
targets -- and discarded the result. A Get or parse failure on such a
route also printed "instance config unresolved, env file left as-is"
about a file this pass never writes, which SyncExec already reports
correctly on its own.

Fix: gate the overlayFor call itself on rt.Backing == registry.BackingUnit,
leaving the later Backing check (the "exec-backed" informational print)
exactly where it was -- moving that check earlier instead would make it
fire for an exec-backed route with no config at all, which it does not do
today.

Adds tests pinning both halves: an exec-backed route with an overlay costs
the unit pass zero Gets, and a Get failure on that overlay prints nothing
from the unit pass.

Final review, plan 4 (tenant email senders, per-instance config layer).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev 96fb0e05951c81373a4a9d2b715d336fc063560c parent 76c1dc5
2 files changed, +122 −29
  • internal/appconfig/instances_test.go +74 −0
  • internal/appconfig/sync.go +48 −29
diff --git a/internal/appconfig/instances_test.go b/internal/appconfig/instances_test.go
index 4d87cafb1..beb862c4d 100644
--- a/internal/appconfig/instances_test.go
+++ b/internal/appconfig/instances_test.go
@@ -553,6 +553,80 @@ func TestSyncCostsOneListingPerAppAndOneGetPerOverlay(t *testing.T) {
}
}
+// The unit pass never delivers an exec-backed route's config at all
+// (TestSyncSkipsExecBackedRoutes) — SyncExec does, and reports its own
+// overlay failures on it correctly. Final review, plan 4: the unit pass
+// used to fetch the overlay for EVERY syncable route regardless of Backing
+// and only checked Backing 53 lines later, so it paid a Get for exactly the
+// exec-backed, overlay-bearing routes step 5 targets, and threw the result
+// away. This pins the fixed cost: zero Gets against that key from the unit
+// pass, whatever the route's overlay actually holds.
+func TestSyncPaysNoGetForAnExecBackedRoutesOverlay(t *testing.T) {
+ ctx, base, reg, envDir, priv := syncInstanceKit(t)
+ if err := SetEnv(ctx, base, "bab", "titogo", map[string]string{"APP_WIDE": "yes"}); err != nil {
+ t.Fatalf("SetEnv: %v", err)
+ }
+ if _, _, err := ApplyInstance(ctx, base, "bab", "titogo", "book.example.com",
+ map[string]Var{"TENANT": {Kind: "env", Value: "book"}}, nil); err != nil {
+ t.Fatalf("ApplyInstance: %v", err)
+ }
+ // Exec-backed: Backing left at its zero value, the fleet default (same
+ // shape as TestSyncSkipsExecBackedRoutes).
+ if err := reg.Upsert(registry.Route{
+ Host: "book.example.com", Kind: "instance", App: "titogo", Account: "bab", Status: "ready",
+ }); err != nil {
+ t.Fatalf("Upsert: %v", err)
+ }
+ s := newCountingStore(base)
+ if err := Sync(ctx, reg, s, priv, envDir, func(string) error { return nil }, nil); err != nil {
+ t.Fatalf("Sync: %v", err)
+ }
+ if got := s.n(s.gets, InstanceConfigKey("bab", "titogo", "book.example.com")); got != 0 {
+ t.Fatalf("unit pass Get'd an exec-backed route's overlay %d time(s), want 0: SyncExec owns delivering it, not Sync", got)
+ }
+ if _, err := os.Stat(filepath.Join(envDir, "book.example.com.env")); !os.IsNotExist(err) {
+ t.Fatalf("expected no env file for an exec-backed route, stat err = %v", err)
+ }
+}
+
+// A Get failure against an exec-backed route's overlay must be invisible to
+// the unit pass: gating the fetch on Backing == BackingUnit (rather than
+// merely discarding its result) means the failure never happens here at
+// all, so the pass cannot print the misleading "env file left as-is" claim
+// about a file it never writes for this route. SyncExec, exercised
+// separately, is the one that reports a failure on this exact key.
+func TestSyncPrintsNothingForAnExecBackedRoutesOverlayGetFailure(t *testing.T) {
+ ctx, base, reg, envDir, priv := syncInstanceKit(t)
+ if err := SetEnv(ctx, base, "bab", "titogo", map[string]string{"APP_WIDE": "yes"}); err != nil {
+ t.Fatalf("SetEnv: %v", err)
+ }
+ if _, _, err := ApplyInstance(ctx, base, "bab", "titogo", "book.example.com",
+ map[string]Var{"TENANT": {Kind: "env", Value: "book"}}, nil); err != nil {
+ t.Fatalf("ApplyInstance: %v", err)
+ }
+ if err := reg.Upsert(registry.Route{
+ Host: "book.example.com", Kind: "instance", App: "titogo", Account: "bab", Status: "ready",
+ }); err != nil {
+ t.Fatalf("Upsert: %v", err)
+ }
+ failing := &failingKeyStore{Store: base, failKey: InstanceConfigKey("bab", "titogo", "book.example.com"),
+ err: errors.New("s3: transient 503 SlowDown")}
+ out := captureStdout(t, func() {
+ if err := Sync(ctx, reg, failing, priv, envDir, func(string) error { return nil }, nil); err != nil {
+ t.Fatalf("Sync: %v", err)
+ }
+ })
+ if strings.Contains(out, "unresolved") {
+ t.Fatalf("the unit pass printed about an exec-backed route's overlay, which it never delivers or Gets:\n%s", out)
+ }
+ if !strings.Contains(out, "exec-backed") {
+ t.Fatalf("expected the ordinary exec-backed notice (this route still has a default bundle), got:\n%s", out)
+ }
+ if _, err := os.Stat(filepath.Join(envDir, "book.example.com.env")); !os.IsNotExist(err) {
+ t.Fatalf("expected no env file for an exec-backed route, stat err = %v", err)
+ }
+}
+
// Two independently corrupt overlays are two independent failures, not one.
// The listing itself succeeds (both hosts are named), so the "once per app"
// dedup for a failed LISTING must not swallow the second route's Get
diff --git a/internal/appconfig/sync.go b/internal/appconfig/sync.go
index ca6a5a727..5e7135fdc 100644
--- a/internal/appconfig/sync.go
+++ b/internal/appconfig/sync.go
@@ -181,37 +181,56 @@ func Sync(ctx context.Context, reg *registry.Registry, s release.Store, priv *ec
// with no default bundle used to resolve to "never configured" and
// stop here, but with an overlay it now resolves to something (senders
// design §6.2), so the skip has to see the overlay first.
- inst, instETag, err := overlayFor(k.acct, k.app, rt.Host)
- if err != nil {
- var listErr *instanceOverlayListErr
- if errors.As(err, &listErr) {
- // Said ONCE per (account, app), not once per route: the
- // listing this failed on is cached per app inside the
- // loader, so every route of the app, across every
- // environment it is bound to, fails identically and for
- // the same reason; a line each would bury the cause under
- // its own repetitions on a box with many routes.
- ak := appKey{k.acct, k.app}
- if !said[ak] {
- said[ak] = true
- // rt.Host anchors the line to a real route (the
- // first one this pass happened to reach), so an
- // operator scanning the log has something to grep
- // for; every other route of this app is silently
- // covered by the same line.
- fmt.Printf(" %s/%s: instance config unresolved (seen at %s), env files left as-is: %v\n", k.acct, k.app, rt.Host, err)
+ //
+ // Fetched ONLY for unit-backed routes (final review, plan 4): an
+ // exec-backed route never reaches syncHost below — SyncExec renders
+ // its overlay and reports its own listing/Get failures on it — so a
+ // Get paid here would be charged and discarded for every exec-backed
+ // route that has one, which is exactly the shape step 5 targets. A
+ // Get or parse failure paid here for such a route would also print
+ // "env file left as-is" about a file this pass never writes, a claim
+ // SyncExec already makes correctly on its own. Deliberately NOT
+ // hoisting the rt.Backing check up to gate this whole block, though:
+ // that would make the "exec-backed" print below fire for an
+ // exec-backed route with no config at all, which it does not do
+ // today — the check stays exactly where it always was, and only the
+ // overlay fetch itself is narrowed.
+ var inst *Manifest
+ var instETag string
+ if rt.Backing == registry.BackingUnit {
+ var err error
+ inst, instETag, err = overlayFor(k.acct, k.app, rt.Host)
+ if err != nil {
+ var listErr *instanceOverlayListErr
+ if errors.As(err, &listErr) {
+ // Said ONCE per (account, app), not once per route: the
+ // listing this failed on is cached per app inside the
+ // loader, so every route of the app, across every
+ // environment it is bound to, fails identically and for
+ // the same reason; a line each would bury the cause under
+ // its own repetitions on a box with many routes.
+ ak := appKey{k.acct, k.app}
+ if !said[ak] {
+ said[ak] = true
+ // rt.Host anchors the line to a real route (the
+ // first one this pass happened to reach), so an
+ // operator scanning the log has something to grep
+ // for; every other route of this app is silently
+ // covered by the same line.
+ fmt.Printf(" %s/%s: instance config unresolved (seen at %s), env files left as-is: %v\n", k.acct, k.app, rt.Host, err)
+ }
+ } else {
+ // A per-host Get or parse failure, unlike the listing
+ // above, is specific to THIS route: the listing already
+ // succeeded (it named this host), so another route of
+ // the same app can have a perfectly good overlay, or a
+ // failure of its own for a different reason. Folding this
+ // into the app-level line would leave that route's stale
+ // credential file invisible, so it is reported every time.
+ fmt.Printf(" %s: instance config unresolved, env file left as-is: %v\n", rt.Host, err)
}
- } else {
- // A per-host Get or parse failure, unlike the listing
- // above, is specific to THIS route: the listing already
- // succeeded (it named this host), so another route of
- // the same app can have a perfectly good overlay, or a
- // failure of its own for a different reason. Folding this
- // into the app-level line would leave that route's stale
- // credential file invisible, so it is reported every time.
- fmt.Printf(" %s: instance config unresolved, env file left as-is: %v\n", rt.Host, err)
+ continue
}
- continue
}
m := r.m
if m == nil && inst == nil {