carlos / platform Public

console: a box counts toward automatic region eligibility only while its status snapshot is fresh (final review F1)

An edges file is not liveness: nothing deletes a retired box's file, and
its Generation moves only when the file is rewritten, so the last UK box
could be retired and London-timezone apps would still land in uk, where
nothing can mint them. readLiveEdgesSnapshot joins the files with
status.LoadBoxes and status.BoxStale over status.SnapshotPeriod, the rule
internal/edgesfile.EdgeIPs already uses, and regionCandidates counts a
non-home box for nothing but the code's existence unless it is fresh. No
snapshot, or snapshots that cannot be loaded, is not fresh; both are said
once. Every automatic-eligibility caller reads through Server.liveEdges.
The home candidate is unchanged, and so is the explicit door: its armed
gate reads the files alone, and R9's keyless proof keeps counting stale
boxes beside it.

Regression: TestAppDerivedNeedsALiveBox keeps the edges object and lets
the snapshot go stale, the case the deleted-object test hid.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev f2b1b463fe932543bb05ef8c8ddb2ebb28eb4ff1 parent 7311890
10 files changed, +248 −20
  • internal/console/apicli.go +1 −1
  • internal/console/apiinstances.go +2 −2
  • internal/console/instancesplacement.go +61 −0
  • internal/console/regioncandidates.go +29 −7
  • internal/console/regioncandidates_test.go +134 −3
  • internal/console/regionclaim.go +1 −1
  • internal/console/regiondefault.go +2 −2
  • internal/console/regionfill_test.go +14 −0
  • internal/console/regionpicker.go +2 −2
  • internal/console/server.go +2 −2
diff --git a/internal/console/apicli.go b/internal/console/apicli.go
index be1239d2e..7fa6c931a 100644
--- a/internal/console/apicli.go
+++ b/internal/console/apicli.go
@@ -478,7 +478,7 @@ func (s *Server) handleAPICreateApp(w http.ResponseWriter, r *http.Request) {
if err != nil {
log.Printf("console: claim %s/%s: read back region: %v", acct.Sqid, name, err)
}
- files, ferr := readEdgesSnapshot(r.Context(), s.cfg.Releases)
+ files, ferr := s.liveEdges(r.Context())
cands := homeOnlyCandidates()
if ferr == nil {
cands = s.regionCands(files)
diff --git a/internal/console/apiinstances.go b/internal/console/apiinstances.go
index 65314f9ad..224257efd 100644
--- a/internal/console/apiinstances.go
+++ b/internal/console/apiinstances.go
@@ -802,7 +802,7 @@ func (s *Server) handleAPIInstanceCreate(w http.ResponseWriter, r *http.Request)
// An APP-DERIVED region needs positive eligibility — the secrets key
// included (R9) — and never rides the dormant gate (codex R2-2).
if regionFrom == "app" {
- snap, err := readEdgesSnapshot(ctx, s.cfg.Releases)
+ snap, err := s.liveEdges(ctx)
if err != nil {
log.Printf("console: api instances create %s/%s: read edges: %v", acct.Sqid, app, err)
jsonError(w, http.StatusServiceUnavailable, msgAppRegionUnconfirmed(s.homeRegionOut(region)))
@@ -922,7 +922,7 @@ func (s *Server) handleAPIInstanceCreate(w http.ResponseWriter, r *http.Request)
// CURRENT writer proves the region keyless; unknown keeps today's
// behaviour, so roll order never refuses a working region.
if probe.DerivesPaths() || probe.Role == "replica" {
- if snap, err := readEdgesSnapshot(ctx, s.cfg.Releases); err == nil {
+ if snap, err := s.liveEdges(ctx); err == nil {
if c, ok := findCandidate(s.regionCands(snap), region); ok && c.NoKeyProven {
jsonError(w, http.StatusBadRequest, msgNoSecretsKey(s.homeRegionOut(region)))
return
diff --git a/internal/console/instancesplacement.go b/internal/console/instancesplacement.go
index 4415862ba..a3e601c4d 100644
--- a/internal/console/instancesplacement.go
+++ b/internal/console/instancesplacement.go
@@ -6,8 +6,11 @@ import (
"log"
"sort"
"strings"
+ "sync"
+ "time"
"github.com/carlosframework/platform/internal/release"
+ "github.com/carlosframework/platform/internal/status"
)
// edgesFilePrefix is where every box publishes what it serves — one object
@@ -60,6 +63,64 @@ type edgesFilePlacementView struct {
AcceptsInbound *bool `json:"accepts_inbound"`
SecretsKeyFP string `json:"secrets_key_fp"`
Hosts map[string]json.RawMessage `json:"hosts"`
+ // Fresh is not on the wire: readLiveEdgesSnapshot sets it when the
+ // box's own status snapshot is still fresh (status.BoxStale). An edges
+ // file is not liveness, see readLiveEdgesSnapshot.
+ Fresh bool `json:"-"`
+}
+
+// livenessSaid throttles readLiveEdgesSnapshot's two diagnostics to once
+// per process, the way noPointSaid does for a code with no point.
+var livenessSaid sync.Map
+
+// readLiveEdgesSnapshot is readEdgesSnapshot joined with box liveness, for
+// AUTOMATIC eligibility (regionCandidates). An edges file is not evidence
+// that its box is alive: nothing deletes a retired box's file, and the
+// file's own Generation moves only when the file is rewritten, so a
+// healthy box's is arbitrarily old (internal/edgesfile.EdgeIPs's doc
+// comment has the whole argument). The status snapshot is written every
+// tick, so status.BoxStale is a real liveness signal, and this is the same
+// join EdgeIPs makes, over the same period.
+//
+// A box with no fresh snapshot, and every box when the snapshots cannot be
+// loaded, is simply not Fresh. The fold then counts it for nothing, so a
+// deployment whose status store is split from its deployment bucket
+// (CARLOS_STATUS_BUCKET, which the console does not read) degrades to
+// home-only guesses. That is safe but invisible, so both cases are said
+// once: the load failure, and files found with no snapshot matching any.
+// The explicit door's placements stay on the generous file-only read; a
+// person asked for that region and can see it refused later.
+func readLiveEdgesSnapshot(ctx context.Context, s release.Store, now time.Time) (edgesSnapshot, error) {
+ snap, err := readEdgesSnapshot(ctx, s)
+ if err != nil || len(snap.Files) == 0 {
+ return snap, err
+ }
+ boxes, err := status.LoadBoxes(ctx, s)
+ if err != nil {
+ if _, said := livenessSaid.LoadOrStore("load", true); !said {
+ log.Printf("console: could not load box snapshots (%v), so no box outside the home counts for automatic region placement until they load", err)
+ }
+ return snap, nil
+ }
+ live := map[string]bool{}
+ for _, b := range boxes {
+ if !status.BoxStale(b, now, status.SnapshotPeriod) {
+ live[b.Label] = true
+ }
+ }
+ joined := 0
+ for i := range snap.Files {
+ snap.Files[i].Fresh = live[snap.Files[i].Box]
+ if snap.Files[i].Fresh {
+ joined++
+ }
+ }
+ if joined == 0 {
+ if _, said := livenessSaid.LoadOrStore("none", true); !said {
+ log.Printf("console: %d edges file(s) under %s but no fresh box snapshot matched any of them, so automatic region placement sees the home alone (are edges and status in different buckets? see CARLOS_STATUS_BUCKET/CARLOS_STATUS_PREFIX)", len(snap.Files), edgesFilePrefix)
+ }
+ }
+ return snap, nil
}
// loadEdgesJoin folds every readable edges file into host → the boxes that
diff --git a/internal/console/regioncandidates.go b/internal/console/regioncandidates.go
index 90b5b1106..0dc5a1b3c 100644
--- a/internal/console/regioncandidates.go
+++ b/internal/console/regioncandidates.go
@@ -7,6 +7,7 @@ import (
"os"
"sort"
"sync"
+ "time"
"github.com/carlosframework/platform/internal/geo"
"github.com/carlosframework/platform/internal/provision"
@@ -21,11 +22,11 @@ type regionCandidate struct {
Code string
Home bool
AWSRegion string
- Reachable bool // some box publishing the code has a dial address
+ Reachable bool // some FRESH box publishing the code has a dial address (F1: fresh = a current status snapshot)
Hosting bool // ... and its activator configured (unknown counts, as in placements)
KeyOK bool // ... and holds the deployment's secrets key (R9)
Eligible bool // ONE box has all of it, every fact POSITIVELY true
- NoKeyProven bool // R9: every reachable hosting box is a current writer, and none holds the key
+ NoKeyProven bool // R9: every reachable hosting box, fresh or stale, is a current writer, and none holds the key
}
// unavailableReason is "" for an eligible candidate, else the first bar it
@@ -90,14 +91,27 @@ func regionCandidates(snap edgesSnapshot, homeName, expectedFP string) []regionC
}
hosting := isTrue(f.HibernationConfigured) || f.HibernationConfigured == nil
keyOK := expectedFP != "" && f.SecretsKeyFP == expectedFP
+ if hosting {
+ // R9's negative proof counts every box with a dial address,
+ // fresh or not: a lingering keyless file still describes that
+ // box's configuration, and the explicit door this proof serves
+ // admits boxes from the files alone (placements), with no
+ // liveness rule of its own. Liveness changes nothing there.
+ a.hostingBoxes++
+ a.keyFound = a.keyFound || keyOK
+ a.unknownWriter = a.unknownWriter || f.AcceptsInbound == nil
+ }
+ if !f.Fresh {
+ // F1: a box with no fresh status snapshot is not evidence of
+ // anything but the code's existence (reason "absent"). Its
+ // edges file lingers after retirement; see readLiveEdgesSnapshot.
+ continue
+ }
eligible := isTrue(f.HibernationConfigured) && isTrue(f.ServesSharedPool) && isTrue(f.AcceptsInbound) && keyOK
a.c.Reachable = true
a.c.Hosting = a.c.Hosting || hosting
if hosting {
- a.hostingBoxes++
a.c.KeyOK = a.c.KeyOK || keyOK
- a.keyFound = a.keyFound || keyOK
- a.unknownWriter = a.unknownWriter || f.AcceptsInbound == nil
}
a.c.Eligible = a.c.Eligible || eligible
if eligible && f.Region != "" {
@@ -122,11 +136,19 @@ func regionCandidates(snap edgesSnapshot, homeName, expectedFP string) []regionC
}
// regionCands is regionCandidates with this console's home and expected
-// secrets fingerprint.
+// secrets fingerprint. The snapshot must come from liveEdges: a plain
+// readEdgesSnapshot marks no box fresh, so every coded region would read
+// as absent.
func (s *Server) regionCands(snap edgesSnapshot) []regionCandidate {
return regionCandidates(snap, s.cfg.HomeRegion, s.expectedSecretsFP())
}
+// liveEdges is the one read every automatic-eligibility caller makes: the
+// edges files joined with box liveness as of now (F1).
+func (s *Server) liveEdges(ctx context.Context) (edgesSnapshot, error) {
+ return readLiveEdgesSnapshot(ctx, s.cfg.Releases, time.Now())
+}
+
// candsOrHome is the candidates from a fresh snapshot, or the home alone
// when the listing fails (or there is no bucket): for displays and guesses
// that must not fail.
@@ -134,7 +156,7 @@ func (s *Server) candsOrHome(ctx context.Context) []regionCandidate {
if s.cfg.Releases == nil {
return homeOnlyCandidates()
}
- snap, err := readEdgesSnapshot(ctx, s.cfg.Releases)
+ snap, err := s.liveEdges(ctx)
if err != nil {
return homeOnlyCandidates()
}
diff --git a/internal/console/regioncandidates_test.go b/internal/console/regioncandidates_test.go
index f81ab92fd..ef9592f09 100644
--- a/internal/console/regioncandidates_test.go
+++ b/internal/console/regioncandidates_test.go
@@ -11,9 +11,11 @@ import (
"strconv"
"strings"
"testing"
+ "time"
"github.com/carlosframework/platform/internal/provision"
"github.com/carlosframework/platform/internal/release"
+ "github.com/carlosframework/platform/internal/status"
)
// mustTestSecretsKey is Task 9's (regionconfig_test.go).
@@ -63,11 +65,17 @@ var (
fleetBoxFile = edgeJSON("fl-box", "4.4.4.4", "eu-west-2", "fleet/acme", eligibleBox)
)
-// setEdges creates OR replaces edges files. putEdges is create-only (an
-// empty ifMatch on DirStore and S3), so it cannot change a box's file.
+// setEdges creates OR replaces edges files, and gives each box a FRESH
+// status snapshot: automatic eligibility needs one (F1), and the fixture
+// boxes are all meant to be live unless a test says otherwise through
+// setBoxSnapshot. putEdges is create-only (an empty ifMatch on DirStore and
+// S3), so it cannot change a box's file.
func setEdges(t *testing.T, fx *consoleFixture, files map[string]string) {
t.Helper()
ctx := context.Background()
+ for name := range files {
+ setBoxSnapshot(t, fx.rel, name, time.Now())
+ }
for name, body := range files {
key := "control/edges/" + name + ".json"
etag := ""
@@ -82,6 +90,16 @@ func setEdges(t *testing.T, fx *consoleFixture, files map[string]string) {
}
}
+// setBoxSnapshot writes (or rewrites) a box's status/boxes/<label>.json
+// generated at the given time: now for a live box, an hour ago for one that
+// has stopped reporting (status.BoxStale's rule).
+func setBoxSnapshot(t *testing.T, s release.Store, label string, at time.Time) {
+ t.Helper()
+ if err := status.WriteBox(context.Background(), s, status.Box{Label: label, GeneratedAt: at}); err != nil {
+ t.Fatalf("snapshot %s: %v", label, err)
+ }
+}
+
type failingList struct{ release.Store }
func (failingList) List(context.Context, string) ([]string, error) {
@@ -94,7 +112,9 @@ func captureLog(w io.Writer) (restore func()) {
return func() { log.SetOutput(prev); log.SetFlags(flags) }
}
-// views is a COMPLETE snapshot of the given edges-file bodies.
+// views is a COMPLETE snapshot of the given edges-file bodies, every box
+// FRESH (F1): the pure fold is handed what readLiveEdgesSnapshot would
+// produce for a fleet that is all reporting. markStale takes one back.
func views(t *testing.T, bodies ...string) edgesSnapshot {
t.Helper()
snap := edgesSnapshot{Complete: true}
@@ -103,11 +123,122 @@ func views(t *testing.T, bodies ...string) edgesSnapshot {
if err := json.Unmarshal([]byte(b), &f); err != nil {
t.Fatal(err)
}
+ f.Fresh = true
snap.Files = append(snap.Files, f)
}
return snap
}
+// markStale is views' box that has stopped reporting: its edges file is
+// still in the snapshot, its status snapshot is not fresh.
+func markStale(snap edgesSnapshot, box string) edgesSnapshot {
+ for i := range snap.Files {
+ if snap.Files[i].Box == box {
+ snap.Files[i].Fresh = false
+ }
+ }
+ return snap
+}
+
+// F1: a retired box's edges file lingers (nothing deletes it), so the file
+// alone is not liveness. A non-home box counts toward automatic eligibility
+// only while its status snapshot is fresh, the rule edgesfile.EdgeIPs uses.
+// The existing disappearance test deletes the object; this one leaves it.
+func TestRegionCandidatesNeedAFreshBoxSnapshot(t *testing.T) {
+ ctx := context.Background()
+ s := release.NewDirStore(t.TempDir())
+ for key, body := range map[string]string{"ie-box": homeBox, "uk-box": ukEligible} {
+ if _, err := s.Put(ctx, "control/edges/"+key+".json", []byte(body), ""); err != nil {
+ t.Fatal(err)
+ }
+ }
+ now := time.Now()
+ uk := func(name string) regionCandidate {
+ t.Helper()
+ snap, err := readLiveEdgesSnapshot(ctx, s, now)
+ if err != nil {
+ t.Fatalf("%s: %v", name, err)
+ }
+ c, _ := findCandidate(regionCandidates(snap, "ie", testKeyFP), "uk")
+ return c
+ }
+ if c := uk("no snapshot"); c.Eligible || c.unavailableReason() != "absent" {
+ t.Fatalf("no snapshot at all: uk = %+v, want absent", c)
+ }
+ setBoxSnapshot(t, s, "uk-box", now.Add(-time.Hour))
+ if c := uk("stale"); c.Eligible || c.unavailableReason() != "absent" {
+ t.Fatalf("stale snapshot, edges file still present: uk = %+v, want absent", c)
+ }
+ setBoxSnapshot(t, s, "uk-box", now)
+ if c := uk("fresh"); !c.Eligible {
+ t.Fatalf("fresh snapshot: uk = %+v, want eligible", c)
+ }
+ // The home candidate stays as it is: no snapshot was ever written for
+ // ie-box, and it is eligible throughout.
+ snap, _ := readLiveEdgesSnapshot(ctx, s, now)
+ if h, _ := findCandidate(regionCandidates(snap, "ie", testKeyFP), ""); !h.Eligible || h.AWSRegion != "eu-west-1" {
+ t.Fatalf("home = %+v", h)
+ }
+}
+
+// F1 leaves the explicit door where it was: R9's keyless proof is read
+// from the files, like the armed gate it sits beside, so a stale keyless
+// box still proves its region keyless (C31 on an explicit --region) while
+// automatic placement reads the same region as absent.
+func TestStaleBoxesStillCountForTheKeylessProof(t *testing.T) {
+ snap := markStale(views(t, homeBox, ukNoKey), "uk-box")
+ c, _ := findCandidate(regionCandidates(snap, "ie", testKeyFP), "uk")
+ if c.Eligible || c.unavailableReason() != "absent" || !c.NoKeyProven {
+ t.Fatalf("stale keyless uk = %+v, want absent and proven keyless", c)
+ }
+ // A stale KEYED box beside a fresh keyless one: not proven, as before.
+ keyless2 := edgeJSON("uk-box-2", "1.2.3.5", "eu-west-2", "uk", edgeOpts{hib: bp(true), shared: bp(true), inbound: bp(true)})
+ c, _ = findCandidate(regionCandidates(markStale(views(t, homeBox, ukEligible, keyless2), "uk-box"), "ie", testKeyFP), "uk")
+ if c.NoKeyProven || c.Eligible {
+ t.Fatalf("stale keyed beside fresh keyless = %+v, want neither proven nor eligible", c)
+ }
+}
+
+// F1: snapshots that cannot be loaded make no box fresh, said once.
+func TestLiveEdgesSnapshotLoadFailureIsNotFreshAndLoggedOnce(t *testing.T) {
+ ctx := context.Background()
+ s := release.NewDirStore(t.TempDir())
+ if _, err := s.Put(ctx, "control/edges/uk-box.json", []byte(ukEligible), ""); err != nil {
+ t.Fatal(err)
+ }
+ setBoxSnapshot(t, s, "uk-box", time.Now())
+ var buf bytes.Buffer
+ restore := captureLog(&buf)
+ defer restore()
+ livenessSaid.Delete("load")
+ for i := 0; i < 2; i++ {
+ snap, err := readLiveEdgesSnapshot(ctx, failingListPrefix{Store: s, prefix: "status/boxes/"}, time.Now())
+ if err != nil {
+ t.Fatal(err)
+ }
+ if c, _ := findCandidate(regionCandidates(snap, "ie", testKeyFP), "uk"); c.Eligible {
+ t.Fatalf("round %d: snapshots unloadable, uk = %+v, want ineligible", i, c)
+ }
+ }
+ if n := strings.Count(buf.String(), "box snapshots"); n != 1 {
+ t.Fatalf("load failure logged %d times, want once: %s", n, buf.String())
+ }
+}
+
+// failingListPrefix fails the listing of ONE prefix (the status half of the
+// join) and serves every other read.
+type failingListPrefix struct {
+ release.Store
+ prefix string
+}
+
+func (f failingListPrefix) List(ctx context.Context, prefix string) ([]string, error) {
+ if prefix == f.prefix {
+ return nil, errors.New("status bucket unreachable")
+ }
+ return f.Store.List(ctx, prefix)
+}
+
func TestRegionCandidatesNeedPositiveEligibility(t *testing.T) {
ca := edgeJSON("ca-box", "7.7.7.7", "ca-central-1", "ca", edgeOpts{})
cands := regionCandidates(views(t, homeBox, ukEligible, auPoolOnly, usNoInbound, ca, nzNoHosting, jpNoEIP, fleetBoxFile), "ie", testKeyFP)
diff --git a/internal/console/regionclaim.go b/internal/console/regionclaim.go
index b5fb535e5..2bc8b4a98 100644
--- a/internal/console/regionclaim.go
+++ b/internal/console/regionclaim.go
@@ -55,7 +55,7 @@ type explicitGate struct {
func (s *Server) readExplicitGate(ctx context.Context) explicitGate {
g := explicitGate{}
g.caps, g.published = placements(ctx, s.cfg.Releases)
- if snap, err := readEdgesSnapshot(ctx, s.cfg.Releases); err == nil {
+ if snap, err := s.liveEdges(ctx); err == nil {
g.cands = s.regionCands(snap)
}
return g
diff --git a/internal/console/regiondefault.go b/internal/console/regiondefault.go
index e2428ed9e..2d8fef8f3 100644
--- a/internal/console/regiondefault.go
+++ b/internal/console/regiondefault.go
@@ -33,7 +33,7 @@ func boolp(b bool) *bool { return &b }
func (s *Server) appDefault(ctx context.Context, acct *Account, app string) appDefaultView {
stored, source, rerr := s.appRegion(acct.ID, app)
- snap, ferr := readEdgesSnapshot(ctx, s.cfg.Releases)
+ snap, ferr := s.liveEdges(ctx)
cands := homeOnlyCandidates()
if ferr == nil {
cands = s.regionCands(snap)
@@ -138,7 +138,7 @@ func (s *Server) handleAPIRegionGuess(w http.ResponseWriter, r *http.Request) {
if ip == nil {
ip = s.personIP(r)
}
- files, err := readEdgesSnapshot(r.Context(), s.cfg.Releases)
+ files, err := s.liveEdges(r.Context())
cands := homeOnlyCandidates()
if err == nil {
cands = s.regionCands(files)
diff --git a/internal/console/regionfill_test.go b/internal/console/regionfill_test.go
index affd566c4..51100bdc9 100644
--- a/internal/console/regionfill_test.go
+++ b/internal/console/regionfill_test.go
@@ -9,6 +9,7 @@ import (
"regexp"
"strings"
"testing"
+ "time"
"github.com/carlosframework/platform/internal/provision"
"github.com/carlosframework/platform/internal/release"
@@ -126,6 +127,19 @@ func TestAppDerivedNeedsPositiveEligibility(t *testing.T) {
}
}
+// F1: the box is retired but its edges object remains, as it does in
+// production (nothing deletes it). Its status snapshot has gone stale, and
+// that alone must take uk out of automatic placement: C16 absent, the same
+// refusal as the deleted-object case above.
+func TestAppDerivedNeedsALiveBox(t *testing.T) {
+ fx, token, u := fillFixture(t, "uk")
+ setBoxSnapshot(t, fx.rel, "uk-box", time.Now().Add(-time.Hour))
+ status, _, out := create(t, u, token, `{"host":"y.go.tito.io"}`)
+ if status != http.StatusBadRequest || !strings.Contains(out, reasonText("absent")) {
+ t.Fatalf("app-derived uk with a stale box snapshot = %d %s, want C16 absent", status, out)
+ }
+}
+
func TestAppDerivedEdgesReadFailureIs503(t *testing.T) {
fx, token, u := fillFixture(t, "uk")
fx.srv.cfg.Releases = failingList{fx.rel}
diff --git a/internal/console/regionpicker.go b/internal/console/regionpicker.go
index bdba50856..fc3e1b7a3 100644
--- a/internal/console/regionpicker.go
+++ b/internal/console/regionpicker.go
@@ -30,7 +30,7 @@ func (s *Server) newAppPicker(ctx context.Context, ip net.IP) *regionPickerView
if !s.appRegionsOn() {
return nil
}
- files, err := readEdgesSnapshot(ctx, s.cfg.Releases)
+ files, err := s.liveEdges(ctx)
if err != nil {
return nil
}
@@ -82,7 +82,7 @@ func (s *Server) handleNewAppRegion(w http.ResponseWriter, r *http.Request) {
jsonError(w, http.StatusForbidden, "Cross-site request blocked.")
return
}
- files, err := readEdgesSnapshot(r.Context(), s.cfg.Releases)
+ files, err := s.liveEdges(r.Context())
cands := homeOnlyCandidates()
if err == nil {
cands = s.regionCands(files)
diff --git a/internal/console/server.go b/internal/console/server.go
index 737d1da57..8a67efb46 100644
--- a/internal/console/server.go
+++ b/internal/console/server.go
@@ -3045,7 +3045,7 @@ func (s *Server) handleAppInstances(w http.ResponseWriter, r *http.Request) {
default:
// A home app is told where it starts only when the home has a
// sibling to be told apart from.
- if files, err := readEdgesSnapshot(r.Context(), s.cfg.Releases); err == nil && eligibleCount(s.regionCands(files)) >= 2 {
+ if files, err := s.liveEdges(r.Context()); err == nil && eligibleCount(s.regionCands(files)) >= 2 {
p.RegionLine = fmt.Sprintf(regionCopy.StartsIn, d.Name)
}
}
@@ -3745,7 +3745,7 @@ func (s *Server) handleNewApp(w http.ResponseWriter, r *http.Request) {
detail := "created app " + name
if s.appRegionsOn() {
region, _, _ := s.store.AppRegion(acct.ID, name)
- files, _ := readEdgesSnapshot(r.Context(), s.cfg.Releases)
+ files, _ := s.liveEdges(r.Context())
detail = fmt.Sprintf(regionCopy.AuditCreated, name, s.placeOf(region, s.regionCands(files)))
}
s.audit(acct, name, addr, "create", detail)