carlos / platform Public

appconfig: socket-exec acks carry the bounce result; status never claims adoption on empty identities (review fix)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Paul Campbell pushed by paul@keymail.dev d8a75cc0e16579cc7a57ca6fad6e33e69bcee65e parent 46f7c83
6 files changed, +297 −45
  • internal/appconfig/configack_test.go +49 −0
  • internal/appconfig/syncexec.go +41 −17
  • internal/appconfig/syncexec_test.go +94 −0
  • internal/console/apirelay.go +33 −9
  • internal/console/apirelay_test.go +62 −14
  • internal/console/relayops.go +18 −5
diff --git a/internal/appconfig/configack_test.go b/internal/appconfig/configack_test.go
index cdc681ad3..9a8a419b5 100644
--- a/internal/appconfig/configack_test.go
+++ b/internal/appconfig/configack_test.go
@@ -178,3 +178,52 @@ func TestSyncAckWithoutRelayHasNilSuffixes(t *testing.T) {
t.Fatalf("ack.RelaySuffixes = %+v, want nil (no CARLOS_RELAY entry)", acks[0].RelaySuffixes)
}
}
+
+// TestSyncAckHibernatingRouteReportsNoRestart is spec §5's "hibernating,
+// delivered on wake" case: try-restart on an inactive unit exits 0 having
+// restarted nothing (sync.go's own long-standing comment on the restart
+// call), so a hibernating route's ack must not read as an ordinary
+// Restarted:true adoption — it has to say the config landed but nothing
+// woke to run it.
+func TestSyncAckHibernatingRouteReportsNoRestart(t *testing.T) {
+ s := release.NewDirStore(t.TempDir())
+ ctx := context.Background()
+ reg := testRegistry(t)
+ envDir := t.TempDir()
+
+ priv, pub := syncRelayTestKeys(t)
+ rec := &relay.Record{Enabled: true, Incarnation: "inc00005", Regions: []string{"eu-west-1"}}
+ masters := []relay.RegionMaster{{Region: "eu-west-1", Kid: "k1", Master: []byte("0123456789abcdef0123456789abcdef")}}
+ sealRelayConfig(t, ctx, s, pub, "abc", "meet", rec, masters)
+
+ if err := reg.Upsert(registry.Route{
+ Host: "meet.example.com", Kind: "instance", App: "meet", Account: "abc", Status: "ready",
+ Backing: registry.BackingUnit, Hibernate: true,
+ }); err != nil {
+ t.Fatalf("Upsert: %v", err)
+ }
+
+ var acks []ConfigAck
+ ack := func(a ConfigAck) { acks = append(acks, a) }
+ // try-restart on a hibernating (inactive) unit exits 0 having done
+ // nothing — the no-op restart callers everywhere in this package rely
+ // on returning nil for.
+ restart := func(host string) error { return nil }
+ if err := Sync(ctx, reg, s, priv, envDir, restart, ack); err != nil {
+ t.Fatalf("Sync: %v", err)
+ }
+
+ if len(acks) != 1 {
+ t.Fatalf("acks = %d, want 1: %+v", len(acks), acks)
+ }
+ got := acks[0]
+ if !got.Hibernating {
+ t.Fatalf("ack.Hibernating = false, want true")
+ }
+ if got.Restarted {
+ t.Fatalf("ack.Restarted = true, want false — the route was asleep, nothing actually restarted")
+ }
+ if got.RestartError != "" {
+ t.Fatalf("ack.RestartError = %q, want empty", got.RestartError)
+ }
+}
diff --git a/internal/appconfig/syncexec.go b/internal/appconfig/syncexec.go
index 32eca0b6d..be90b85d9 100644
--- a/internal/appconfig/syncexec.go
+++ b/internal/appconfig/syncexec.go
@@ -63,10 +63,12 @@ const configEnvSuffix = ".config.env"
// environments resolve to different manifests, and keying on (acct, app)
// alone would render whichever route was seen first into both files.
//
-// ack, if non-nil, is called once per route this pass actually converges
-// (wrote a changed or removed file) with that route's config-adoption
-// acknowledgement (spec §5, configack.go) — nil means no acks, same
-// contract as Sync's.
+// ack, if non-nil, is called once per route this pass actually WRITES a
+// changed file for, with that route's config-adoption acknowledgement
+// (spec §5, configack.go) — nil means no acks, same contract as Sync's. A
+// route whose file is REMOVED (manifest absent) gets no ack: there is
+// nothing for a route to have adopted once its config is gone, and a
+// removal is not one of spec §5's three delivery shapes.
func SyncExec(ctx context.Context, reg *registry.Registry, s release.Store, priv *ecdh.PrivateKey, dataDir string, bounce func(host string) error, appsDomain string, ack func(ConfigAck)) error {
routes, err := reg.List()
if err != nil {
@@ -851,13 +853,14 @@ const execAddrConfigHeader = `# Written by carlos (SyncExec, issue #18). Carlos
// themselves, so a header-only change (e.g. its text edited in a future
// release) converges exactly like a var change would, once.
// ack, if non-nil, is called once this route's file has actually changed
-// (written or removed) with its config-adoption acknowledgement (spec §5).
-// Restarted is always false and RestartError always empty here — unlike
-// syncHost's unit path, this shape's bounce is best-effort and
-// fire-and-forget (bounceLogged swallows its error), and a tcp-exec-file
-// route's process belongs to an external supervisor this platform never
-// restarts at all; that shape's cutover is a member acknowledgement
-// (`carlos relay adopted`) instead.
+// (written, never on removal — see SyncExec's own doc comment) with its
+// config-adoption acknowledgement (spec §5). Restarted/RestartError come
+// from bounceLogged's own outcome when bounce is non-nil (the socket-exec
+// shape: spec §5 step 5 counts that bounce as the cutover, "since the
+// platform restarted it") and are always false/empty when bounce is nil
+// (the tcp-exec-file shape: its process belongs to an external supervisor
+// this platform never restarts at all, so that shape's cutover is a
+// member acknowledgement, `carlos relay adopted`, instead).
func syncExecHost(rt registry.Route, m *Manifest, key string, priv *ecdh.PrivateKey, dataDir string, bounce func(host string) error, header string, ack func(ConfigAck)) {
if rt.Host == "" || rt.Host == "." || rt.Host == ".." || rt.Host != filepath.Base(rt.Host) {
fmt.Printf(" %q: not a bare hostname, refusing to derive a config file path from it\n", rt.Host)
@@ -923,7 +926,7 @@ func syncExecHost(rt registry.Route, m *Manifest, key string, priv *ecdh.Private
return
}
fmt.Printf(" %s: config updated\n", rt.Host)
- bounceLogged(rt.Host, bounce)
+ bounceErr := bounceLogged(rt.Host, bounce)
if ack != nil {
a := ConfigAck{Host: rt.Host, Shape: shapeOf(rt), At: time.Now()}
if suffixes, err := relaySuffixesFromRelayVar(m, priv); err != nil {
@@ -931,20 +934,41 @@ func syncExecHost(rt registry.Route, m *Manifest, key string, priv *ecdh.Private
} else {
a.RelaySuffixes = suffixes
}
+ // bounce == nil (the tcp-exec-file/addr shape) leaves Restarted
+ // false and RestartError empty — see this function's own doc
+ // comment for why. bounce != nil is the socket-exec shape, and its
+ // bounce IS this pass's restart (spec §5 step 5: a socket-exec
+ // route's cutover is the ack "since the platform restarted it"),
+ // so the ack has to carry what bounceLogged actually returned
+ // rather than assume success the way the removal branch above may.
+ if bounce != nil {
+ if bounceErr != nil {
+ a.RestartError = bounceErr.Error()
+ } else {
+ a.Restarted = true
+ }
+ }
ack(a)
}
}
-// bounceLogged: bounce is best-effort — a failure is logged, never
-// propagated; the file on disk is already correct and the next natural
-// wake picks it up.
-func bounceLogged(host string, bounce func(host string) error) {
+// bounceLogged calls bounce — best-effort in the sense that a failure is
+// logged rather than aborting the pass, the file on disk is already
+// correct and the next natural wake picks it up — and returns what bounce
+// itself returned, so a caller that also writes a config-ack (syncExecHost
+// above) can report whether THIS pass's restart actually succeeded rather
+// than silently discarding that fact. bounce == nil (the tcp-exec-file/
+// addr shape, whose process this platform does not own) is a no-op
+// returning nil.
+func bounceLogged(host string, bounce func(host string) error) error {
if bounce == nil {
- return
+ return nil
}
if err := bounce(host); err != nil {
fmt.Printf(" %s: bounce after config change: %v\n", host, err)
+ return err
}
+ return nil
}
// reapConfigEnv deletes *.config.env (and stale *.config.env.tmp) for
diff --git a/internal/appconfig/syncexec_test.go b/internal/appconfig/syncexec_test.go
index e97709589..ae4fa0414 100644
--- a/internal/appconfig/syncexec_test.go
+++ b/internal/appconfig/syncexec_test.go
@@ -13,6 +13,7 @@ import (
"time"
"github.com/carlosframework/platform/internal/registry"
+ "github.com/carlosframework/platform/internal/relay"
"github.com/carlosframework/platform/internal/release"
)
@@ -1896,3 +1897,96 @@ func TestSyncExecHoldsConfigWhenHostNamesADifferentAccount(t *testing.T) {
t.Fatalf("agreeing host's stale file survived, stat err = %v", err)
}
}
+
+// TestSyncExecWritesConfigAckForSocketExecRoute is the review's finding 1
+// (2026-09-07): syncExecHost DOES bounce a socket-exec route (bounce is
+// non-nil for it, only a tcp-exec-file/addr route gets nil), so its ack
+// must carry that bounce's own result — this pins Restarted true and
+// RelaySuffixes naming what was actually sealed, the same way syncHost's
+// unit-path ack does.
+func TestSyncExecWritesConfigAckForSocketExecRoute(t *testing.T) {
+ ctx, s, reg, dd, _, _ := execKit(t)
+ priv, pub := syncRelayTestKeys(t)
+
+ rec := &relay.Record{Enabled: true, Incarnation: "inc00003", Regions: []string{"eu-west-1"}}
+ masters := []relay.RegionMaster{{Region: "eu-west-1", Kid: "k1", Master: []byte("0123456789abcdef0123456789abcdef")}}
+ wantSuffixes := sealRelayConfig(t, ctx, s, pub, "bab", "eleven", rec, masters)
+
+ if err := reg.Upsert(registry.Route{
+ Host: "inst.example.com", Kind: "instance", App: "eleven", Account: "bab",
+ Status: "ready", Socket: "/run/carlos/inst.sock",
+ }); err != nil {
+ t.Fatalf("Upsert: %v", err)
+ }
+
+ var acks []ConfigAck
+ ack := func(a ConfigAck) { acks = append(acks, a) }
+ bounce := func(h string) error { return nil }
+ if err := SyncExec(ctx, reg, s, priv, dd, bounce, "", ack); err != nil {
+ t.Fatalf("SyncExec: %v", err)
+ }
+
+ if len(acks) != 1 {
+ t.Fatalf("acks = %d, want 1: %+v", len(acks), acks)
+ }
+ got := acks[0]
+ if got.Shape != "socket-exec" {
+ t.Fatalf("ack.Shape = %q, want socket-exec", got.Shape)
+ }
+ if !got.Restarted {
+ t.Fatalf("ack.Restarted = false, want true — bounce succeeded and IS this shape's restart (spec §5 step 5)")
+ }
+ if got.RestartError != "" {
+ t.Fatalf("ack.RestartError = %q, want empty", got.RestartError)
+ }
+ if got.RelaySuffixes["eu-west-1"] != wantSuffixes["eu-west-1"] {
+ t.Fatalf("ack.RelaySuffixes[eu-west-1] = %q, want %q", got.RelaySuffixes["eu-west-1"], wantSuffixes["eu-west-1"])
+ }
+}
+
+// TestSyncExecWritesConfigAckForTCPExecFileRoute is the addr shape's other
+// half of the same ack path: bounce is nil for it (this platform never
+// restarts a process it does not own), so Restarted/RestartError must stay
+// false/empty even though the file itself landed and named real
+// identities.
+func TestSyncExecWritesConfigAckForTCPExecFileRoute(t *testing.T) {
+ ctx, s, reg, dd, _, _ := execKit(t)
+ priv, pub := syncRelayTestKeys(t)
+
+ if err := SetExecDelivery(ctx, s, "bab", "titogo", ExecDeliveryFile); err != nil {
+ t.Fatalf("SetExecDelivery: %v", err)
+ }
+ rec := &relay.Record{Enabled: true, Incarnation: "inc00004", Regions: []string{"eu-west-1"}}
+ masters := []relay.RegionMaster{{Region: "eu-west-1", Kid: "k1", Master: []byte("fedcba9876543210fedcba9876543210")}}
+ wantSuffixes := sealRelayConfig(t, ctx, s, pub, "bab", "titogo", rec, masters)
+
+ if err := reg.Upsert(registry.Route{
+ Host: "demo.go.tito.io", Kind: "instance", App: "titogo", Account: "bab",
+ Status: "ready", Addr: "127.0.0.1:8444",
+ }); err != nil {
+ t.Fatalf("Upsert: %v", err)
+ }
+
+ var acks []ConfigAck
+ ack := func(a ConfigAck) { acks = append(acks, a) }
+ if err := SyncExec(ctx, reg, s, priv, dd, func(string) error { return nil }, "", ack); err != nil {
+ t.Fatalf("SyncExec: %v", err)
+ }
+
+ if len(acks) != 1 {
+ t.Fatalf("acks = %d, want 1: %+v", len(acks), acks)
+ }
+ got := acks[0]
+ if got.Shape != "tcp-exec-file" {
+ t.Fatalf("ack.Shape = %q, want tcp-exec-file", got.Shape)
+ }
+ if got.Restarted {
+ t.Fatalf("ack.Restarted = true, want false — this platform never restarts this shape's process")
+ }
+ if got.RestartError != "" {
+ t.Fatalf("ack.RestartError = %q, want empty", got.RestartError)
+ }
+ if got.RelaySuffixes["eu-west-1"] != wantSuffixes["eu-west-1"] {
+ t.Fatalf("ack.RelaySuffixes[eu-west-1] = %q, want %q", got.RelaySuffixes["eu-west-1"], wantSuffixes["eu-west-1"])
+ }
+}
diff --git a/internal/console/apirelay.go b/internal/console/apirelay.go
index b3a941747..2a152c026 100644
--- a/internal/console/apirelay.go
+++ b/internal/console/apirelay.go
@@ -295,7 +295,20 @@ func relayCutoverFor(rec *relay.Record, host string) *relay.RouteCutover {
// has never actually seen. "Freshest" (latest At) only matters if more than
// one box's ack somehow matches, which config sync's own per-(box,host) key
// makes rare in practice but not impossible across a route migration.
-func relayMatchingAck(acks []appconfig.ConfigAck, host string, issued map[string]string) *appconfig.ConfigAck {
+//
+// phase and issued are checked FIRST, and deliberately: maps.Equal(nil,
+// nil) is true, so without this guard an enabled record still at phase
+// "config" — IssuedSuffixes not written yet, a state an interrupted enable
+// can persist in indefinitely — would match ANY route's ordinary pre-relay
+// ack (RelaySuffixes nil, no CARLOS_RELAY entry at all) and print an
+// adoption for identities the console has never actually issued. Only a
+// record that finished an operation (phase done) with something to show
+// for it (len(issued) > 0) can have an ack legitimately match it at all
+// (review finding 2026-09-07).
+func relayMatchingAck(acks []appconfig.ConfigAck, host string, issued map[string]string, phase string) *appconfig.ConfigAck {
+ if phase != relayPhaseDone || len(issued) == 0 {
+ return nil
+ }
var best *appconfig.ConfigAck
for i := range acks {
a := &acks[i]
@@ -322,13 +335,24 @@ func (s *Server) relayReply(ctx context.Context, acct *Account, app string, rec
if err != nil {
return apiRelayReply{}, err
}
- // Read once for the whole reply, not once per route: there is no
- // per-host box list to narrow this by (see the file's own doc comment),
- // so every route below filters this same slice by Host instead of each
- // paying for its own List+Get pass over every box's acks.
- acks, err := s.relayConfigAcks(ctx)
- if err != nil {
- return apiRelayReply{}, err
+ // Read once for the whole reply, not once per route — and skipped
+ // entirely when there is nothing for an ack to say anything about: a
+ // disabled record has no key delivered (relayReply's own switch below
+ // never reads an ack for it), and no routes means no hosts to match.
+ // relayConfigAcks itself narrows the underlying List to just THIS
+ // app's route hosts before issuing any Get (review finding
+ // 2026-09-07: this used to cost a Get per ack in the whole fleet on
+ // every status call).
+ var acks []appconfig.ConfigAck
+ if rec.Enabled && len(routes) > 0 {
+ hosts := make(map[string]bool, len(routes))
+ for _, rt := range routes {
+ hosts[rt.Host] = true
+ }
+ acks, err = s.relayConfigAcks(ctx, hosts)
+ if err != nil {
+ return apiRelayReply{}, err
+ }
}
for _, rt := range routes {
if rt.Kind == "static" {
@@ -373,7 +397,7 @@ func (s *Server) relayReply(ctx context.Context, acct *Account, app string, rec
// or a stale ack left over from before a rotation) leaves the
// route at the base "written, awaiting delivery" rather than
// reporting an adoption under keys the route may never have run.
- if ca := relayMatchingAck(acks, rt.Host, rec.IssuedSuffixes); ca != nil {
+ if ca := relayMatchingAck(acks, rt.Host, rec.IssuedSuffixes, rec.Phase); ca != nil {
row.ConfigAckAt = ca.At.UTC().Format(time.RFC3339)
row.ConfigAckShape = ca.Shape
switch {
diff --git a/internal/console/apirelay_test.go b/internal/console/apirelay_test.go
index b39fc9557..64b245b4e 100644
--- a/internal/console/apirelay_test.go
+++ b/internal/console/apirelay_test.go
@@ -14,6 +14,7 @@ package console
import (
"context"
+ "maps"
"net/http"
"strings"
"testing"
@@ -456,7 +457,7 @@ func TestAPIRelayStatusNamesEachRouteShapeAndWhoseMoveItIs(t *testing.T) {
issued := relayRecord(t, fx, "titogo").IssuedSuffixes
if err := appconfig.WriteConfigAck(ctx, fx.rel, appconfig.ConfigAck{
Box: "box1", Host: "c.titogo.oncarlos.com", Shape: "tcp-exec-file", At: time.Now(),
- RelaySuffixes: cloneSuffixes(issued),
+ RelaySuffixes: maps.Clone(issued),
}); err != nil {
t.Fatalf("seed ack c: %v", err)
}
@@ -545,17 +546,6 @@ func TestAPIRelayRotateStagingRefusalsAreClientErrors(t *testing.T) {
}
}
-// cloneSuffixes copies a suffix map so a test can hand one ack the record's
-// real IssuedSuffixes and another a value that merely LOOKS like it without
-// aliasing the record's own map.
-func cloneSuffixes(m map[string]string) map[string]string {
- out := make(map[string]string, len(m))
- for k, v := range m {
- out[k] = v
- }
- return out
-}
-
// TestAPIRelayStatusReadsConfigAcks is Task 12's console half: three
// unit-backed routes, one config-ack each, seeded exactly as config sync
// itself writes them (control/config-ack/<box>/<host>.json,
@@ -578,6 +568,8 @@ func TestAPIRelayStatusReadsConfigAcks(t *testing.T) {
Kind: "service", Unit: "titogo-b.service", Status: "ready"})
fx.addRoute(t, registry.Route{Host: "c.titogo.oncarlos.com", App: "titogo",
Kind: "service", Unit: "titogo-c.service", Status: "ready"})
+ fx.addRoute(t, registry.Route{Host: "d.titogo.oncarlos.com", App: "titogo",
+ Kind: "service", Unit: "titogo-d.service", Status: "ready"})
if status, out := postJSON(t, relayURL(fx, "titogo"), map[string]any{}, token); status != http.StatusOK {
t.Fatalf("enable = %d %v, want 200", status, out)
@@ -590,14 +582,14 @@ func TestAPIRelayStatusReadsConfigAcks(t *testing.T) {
now := time.Now()
if err := appconfig.WriteConfigAck(ctx, fx.rel, appconfig.ConfigAck{
Box: "box1", Host: "a.titogo.oncarlos.com", Shape: "unit", At: now,
- Restarted: true, RelaySuffixes: cloneSuffixes(issued),
+ Restarted: true, RelaySuffixes: maps.Clone(issued),
}); err != nil {
t.Fatalf("seed ack a: %v", err)
}
if err := appconfig.WriteConfigAck(ctx, fx.rel, appconfig.ConfigAck{
Box: "box1", Host: "b.titogo.oncarlos.com", Shape: "unit", At: now,
RestartError: "systemctl try-restart carlos-app@b.titogo.oncarlos.com: exit status 1",
- RelaySuffixes: cloneSuffixes(issued),
+ RelaySuffixes: maps.Clone(issued),
}); err != nil {
t.Fatalf("seed ack b: %v", err)
}
@@ -608,6 +600,12 @@ func TestAPIRelayStatusReadsConfigAcks(t *testing.T) {
}); err != nil {
t.Fatalf("seed ack c: %v", err)
}
+ if err := appconfig.WriteConfigAck(ctx, fx.rel, appconfig.ConfigAck{
+ Box: "box1", Host: "d.titogo.oncarlos.com", Shape: "unit", At: now,
+ Hibernating: true, RelaySuffixes: maps.Clone(issued),
+ }); err != nil {
+ t.Fatalf("seed ack d: %v", err)
+ }
_, out := getJSON(t, relayURL(fx, "titogo"), token)
rows := relayRouteRows(t, out)
@@ -620,4 +618,54 @@ func TestAPIRelayStatusReadsConfigAcks(t *testing.T) {
if got := str(t, rows["c.titogo.oncarlos.com"], "delivery"); got != "written, awaiting delivery" {
t.Fatalf("c delivery = %q, want the base written-awaiting-delivery sentence", got)
}
+ wantRoute(t, rows, "d.titogo.oncarlos.com", "unit", "hibernating, delivered on wake")
+}
+
+// TestAPIRelayStatusNeverAdoptsOnEmptyIssuedSuffixes is the review's finding
+// 2 (2026-09-07): maps.Equal(nil, nil) is true, so without an explicit
+// guard an enabled record still at phase "config" — IssuedSuffixes not
+// written yet, the state an enable interrupted between spec §5 steps 4 and
+// 5 persists in until the reconcile finishes it — would match ANY route's
+// ordinary pre-relay ack (no CARLOS_RELAY entry at all, RelaySuffixes nil)
+// and print an adoption for identities the console has never actually
+// issued.
+func TestAPIRelayStatusNeverAdoptsOnEmptyIssuedSuffixes(t *testing.T) {
+ fx, _ := relayFixture(t, "code-relay-empty-issued")
+ ctx := context.Background()
+ token := cliToken(t, fx, "alice@keymail.dev", "code-relay-empty-issued-2")
+
+ fx.addRoute(t, registry.Route{Host: "a.titogo.oncarlos.com", App: "titogo",
+ Kind: "service", Unit: "titogo-a.service", Status: "ready"})
+
+ if status, out := postJSON(t, relayURL(fx, "titogo"), map[string]any{}, token); status != http.StatusOK {
+ t.Fatalf("enable = %d %v, want 200", status, out)
+ }
+
+ // Roll the finished enable back to the interrupted-mid-walk shape by
+ // hand: still enabled, phase "config", nothing issued yet.
+ rec, etag, err := relay.GetRecord(ctx, fx.rel, fx.sqid, "titogo")
+ if err != nil {
+ t.Fatalf("GetRecord: %v", err)
+ }
+ rec.Phase = "config"
+ rec.IssuedSuffixes = nil
+ if _, err := relay.PutRecord(ctx, fx.rel, fx.sqid, "titogo", rec, etag); err != nil {
+ t.Fatalf("PutRecord: %v", err)
+ }
+
+ // An ordinary ack — the shape a route synced before this app ever
+ // enabled the relay carries: RelaySuffixes nil, no error naming
+ // anything wrong with it.
+ if err := appconfig.WriteConfigAck(ctx, fx.rel, appconfig.ConfigAck{
+ Box: "box1", Host: "a.titogo.oncarlos.com", Shape: "unit", At: time.Now(),
+ Restarted: true,
+ }); err != nil {
+ t.Fatalf("seed ack: %v", err)
+ }
+
+ _, out := getJSON(t, relayURL(fx, "titogo"), token)
+ rows := relayRouteRows(t, out)
+ if got := str(t, rows["a.titogo.oncarlos.com"], "delivery"); strings.Contains(got, "adopted") {
+ t.Fatalf("delivery = %q, an empty-IssuedSuffixes record must never read as adopted", got)
+ }
}
diff --git a/internal/console/relayops.go b/internal/console/relayops.go
index 57c199aa5..8de68407b 100644
--- a/internal/console/relayops.go
+++ b/internal/console/relayops.go
@@ -1163,11 +1163,17 @@ func (s *Server) relayHeartbeats(ctx context.Context) ([]relay.Heartbeat, error)
}
// relayConfigAcks reads every per-route config-adoption acknowledgement
-// under configAckPrefix, across every box — there is no per-host box list
-// to narrow the read by, the same limit relayHeartbeats accepts for
-// control/relay-ack/, so relayReply reads all of them and filters by Host
-// itself (relayMatchingAck, apirelay.go).
-func (s *Server) relayConfigAcks(ctx context.Context) ([]appconfig.ConfigAck, error) {
+// under configAckPrefix whose HOST — a key's basename, ConfigAckKey's own
+// "<box>/<host>.json" shape — is in hosts. There is no per-host box list
+// to narrow the LISTING by, the same limit relayHeartbeats accepts for
+// control/relay-ack/, so this still lists every key under the prefix; but
+// hosts filters that listing down to this app's own routes before a
+// single Get is issued, so a status call costs one List plus a Get per
+// THIS APP's route, never a Get per ack in the whole fleet (review
+// finding 2026-09-07 — relayReply used to pass every ack for every app on
+// the deployment through this filter instead of narrowing the read
+// itself).
+func (s *Server) relayConfigAcks(ctx context.Context, hosts map[string]bool) ([]appconfig.ConfigAck, error) {
keys, err := s.cfg.Releases.List(ctx, configAckPrefix)
if err != nil {
return nil, err
@@ -1177,6 +1183,13 @@ func (s *Server) relayConfigAcks(ctx context.Context) ([]appconfig.ConfigAck, er
if !strings.HasSuffix(key, ".json") {
continue
}
+ host := strings.TrimSuffix(key, ".json")
+ if idx := strings.LastIndexByte(host, '/'); idx >= 0 {
+ host = host[idx+1:]
+ }
+ if !hosts[host] {
+ continue
+ }
body, _, err := s.cfg.Releases.Get(ctx, key)
if err != nil {
if errors.Is(err, os.ErrNotExist) {