carlos / platform Public

domains: stamp once per tick, never on a fleet box, inert without an apps domain (#303 review)

Reconcile no longer trails StampHealth; domainsLoop runs it once after
the duties on every platform box (a fleet box never contends and its
EdgeIPs fall back to its own address, which would judge every row
elsewhere). StampHealth honours Config's AppsDomain contract. The
domainsDuty doc no longer claims the stamps as deployment-global state.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell pushed by paul@keymail.dev 528de5fd8c75c7abb752a99518f6064e7653bef4 parent 7cd8309
3 files changed, +70 −21
  • internal/domains/reconcile.go +14 −6
  • internal/domains/reconcile_test.go +34 −0
  • internal/host/agent.go +22 −15
diff --git a/internal/domains/reconcile.go b/internal/domains/reconcile.go
index b15752bdb..dae29e6bc 100644
--- a/internal/domains/reconcile.go
+++ b/internal/domains/reconcile.go
@@ -408,10 +408,10 @@ func Reconcile(ctx context.Context, s release.Store, reg *registry.Registry, cfg
// Sections 4 and 5 — the per-box health stamps — live in StampHealth
// (#303): they write THIS box's registry and must run on every box, not
- // only on the housekeeper this pass is gated to. Reconcile still runs
- // them, so a single-box deployment's behaviour and the tests that pin
- // it are unchanged; the agent runs StampHealth on every tick besides.
- return StampHealth(ctx, reg, cfg, routes)
+ // only on the housekeeper this pass is gated to. The agent runs it once
+ // per tick after this pass, on every box; this pass does not, so the
+ // housekeeper stamps once like everyone else.
+ return nil
}
// StampHealth is the per-box half of the domains pass: DNS health (section
@@ -423,9 +423,17 @@ func Reconcile(ctx context.Context, s release.Store, reg *registry.Registry, cfg
// edges-file publisher's "pointing" bound (#287) needs the HOME box's own
// row stamped.
//
-// routes may be nil, in which case the registry is listed here; Reconcile
-// passes the rows it already holds.
+// Housekeeper or not, the stamps are what the console shows: a home box's
+// status snapshot carries them, and a non-housekeeper home box would show
+// every attached hostname as un-pointing forever (#303) — and the edges-file
+// publisher's "pointing" bound (#287) reads the home box's own row.
+//
+// routes may be nil, in which case the registry is listed here. Inert with
+// no AppsDomain, per Config's contract.
func StampHealth(ctx context.Context, reg *registry.Registry, cfg Config, routes []registry.Route) error {
+ if cfg.AppsDomain == "" {
+ return nil
+ }
if cfg.Now == nil {
cfg.Now = time.Now
}
diff --git a/internal/domains/reconcile_test.go b/internal/domains/reconcile_test.go
index 86b868151..f90fcdaab 100644
--- a/internal/domains/reconcile_test.go
+++ b/internal/domains/reconcile_test.go
@@ -541,6 +541,9 @@ func TestReconcileStampsDNS(t *testing.T) {
if err := domains.Reconcile(ctx, s, reg, c); err != nil {
t.Fatal(err)
}
+ if err := domains.StampHealth(ctx, reg, c, nil); err != nil { // as domainsLoop does after the pass
+ t.Fatal(err)
+ }
now := c.Now().Unix()
for host, want := range map[string]string{
"points.test": "pointing",
@@ -612,6 +615,9 @@ func TestReconcileStampsCertExpiry(t *testing.T) {
if err := domains.Reconcile(ctx, s, reg, c); err != nil {
t.Fatal(err)
}
+ if err := domains.StampHealth(ctx, reg, c, nil); err != nil { // as domainsLoop does after the pass
+ t.Fatal(err)
+ }
if got := mustGet(t, reg, "kass.training"); got.CertExpiresAt != notAfter.Unix() {
t.Errorf("kass.training cert_expires_at = %d, want %d", got.CertExpiresAt, notAfter.Unix())
}
@@ -664,6 +670,9 @@ func TestReconcileStampsAliasDNS(t *testing.T) {
if err := domains.Reconcile(ctx, s, reg, c); err != nil {
t.Fatal(err)
}
+ if err := domains.StampHealth(ctx, reg, c, nil); err != nil { // as domainsLoop does after the pass
+ t.Fatal(err)
+ }
now := c.Now().Unix()
for alias, want := range map[string]string{
"points-alias.test": "pointing",
@@ -696,6 +705,9 @@ func TestReconcileStampsAliasCert(t *testing.T) {
if err := domains.Reconcile(ctx, s, reg, c); err != nil {
t.Fatal(err)
}
+ if err := domains.StampHealth(ctx, reg, c, nil); err != nil { // as domainsLoop does after the pass
+ t.Fatal(err)
+ }
if got := aliasByName(t, reg, "kass-alias.training"); got.CertExpiresAt != notAfter.Unix() {
t.Errorf("kass-alias.training cert_expires_at = %d, want %d", got.CertExpiresAt, notAfter.Unix())
}
@@ -1258,3 +1270,25 @@ func TestStampHealthHoldsLastVerdictOnResolverFailure(t *testing.T) {
t.Errorf("fresh row = %q @%d; want \"\" @2000", got.DNSStatus, got.DNSCheckedAt)
}
}
+
+// Back-to-back passes are idempotent: a second StampHealth with the same
+// view changes nothing but dns_checked_at.
+func TestStampHealthIsIdempotent(t *testing.T) {
+ reg := regWith(t, registry.Route{Host: "app.acct.oncarlos.test", Kind: "service", Addr: "10.0.0.1:1", Status: "ready"})
+ c := cfg(t)
+ for i := 0; i < 2; i++ {
+ if err := domains.StampHealth(context.Background(), reg, c, nil); err != nil {
+ t.Fatal(err)
+ }
+ }
+ if got := mustGet(t, reg, "app.acct.oncarlos.test"); got.DNSStatus != "pointing" || got.DNSCheckedAt != c.Now().Unix() {
+ t.Errorf("after two passes: %q @%d", got.DNSStatus, got.DNSCheckedAt)
+ }
+ // And inert with no apps domain, per Config's contract.
+ if err := domains.StampHealth(context.Background(), reg, domains.Config{Resolve: c.Resolve, Now: func() time.Time { return time.Unix(9, 0) }}, nil); err != nil {
+ t.Fatal(err)
+ }
+ if got := mustGet(t, reg, "app.acct.oncarlos.test"); got.DNSCheckedAt == 9 {
+ t.Error("a pass with no AppsDomain wrote a stamp")
+ }
+}
diff --git a/internal/host/agent.go b/internal/host/agent.go
index cfe329fc6..298afdb68 100644
--- a/internal/host/agent.go
+++ b/internal/host/agent.go
@@ -1641,30 +1641,37 @@ func domainsLoop(reg *registry.Registry, acmeCacheFlag string) {
duty := domainsDuty(func(ctx context.Context) error {
return domains.Reconcile(ctx, store, reg, cfg)
})
+ fleetBox := shouldSkipPublisherElection(os.Getenv)
delay := time.Minute
for {
time.Sleep(delay)
delay = 5*time.Minute + rand.N(time.Minute)
- // The per-box half first, on EVERY box (#303): DNS and TLS health
- // stamp this box's own registry from this box's own view, and no
- // lease is involved. Then the deployment-global half, gated. The
- // housekeeper stamps twice a tick (Reconcile runs the same pass
- // last) — idempotent, and cheaper than a special case.
- if err := domains.StampHealth(ctx, reg, cfg, nil); err != nil {
- log.Printf("agent: domains health: %v", err)
- }
+ // The deployment-global half, gated; then the per-box half on EVERY
+ // platform box (#303): DNS and TLS health stamp this box's own
+ // registry from this box's own view, no lease involved, ONCE per
+ // tick — after the duties so a host section 3 just minted is
+ // stamped in the same tick it appeared. Never on a fleet box: it
+ // never contends for the lease for the same reason (housekeeperNever),
+ // and its EdgeIPs fall back to its own address, which would judge
+ // every row "elsewhere".
runHousekeeperDuties(housekeeperGate, duty)
+ if !fleetBox {
+ if err := domains.StampHealth(ctx, reg, cfg, nil); err != nil {
+ log.Printf("agent: domains health: %v", err)
+ }
+ }
}
}
// domainsDuty wraps one domains reconcile pass as a housekeeper duty. The
-// pass is deployment-wide state — custom-domain `pointing` stamps, cert
-// expiry stamps, purging trashed apps — with one correct writer, and since
-// multi-region (spec 2026-08-20) every platform box in a deployment carries
-// PLATFORM_APPS_DOMAIN (the per-account wildcards and the steering probe
-// need it), so an ungated loop would be N reconcilers racing over the same
-// records, each judging `pointing` against its OWN edge-IP list. Gating on
-// the lease is what makes the env safe to set fleet-wide. Single-box
+// pass is deployment-wide state — materializing routes from claims, minting
+// platform-domain and canary hosts, purging trashed apps — with one correct
+// writer, and since multi-region (spec 2026-08-20) every platform box in a
+// deployment carries PLATFORM_APPS_DOMAIN (the per-account wildcards and the
+// steering probe need it), so an ungated loop would be N reconcilers racing
+// over the same records. Gating on the lease is what makes the env safe to
+// set fleet-wide. (The DNS/TLS health stamps are NOT in this pass: they are
+// per-box, domains.StampHealth, run by domainsLoop on every box — #303.) Single-box
// deployments are unchanged: the one box always holds the lease. A box that
// never contends (a fleet box) never runs the pass — which is right, the
// pass is a platform-box job.