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>
10 files changed,
+248
−20
internal/console/apicli.go+1 −1internal/console/apiinstances.go+2 −2internal/console/instancesplacement.go+61 −0internal/console/regioncandidates.go+29 −7internal/console/regioncandidates_test.go+134 −3internal/console/regionclaim.go+1 −1internal/console/regiondefault.go+2 −2internal/console/regionfill_test.go+14 −0internal/console/regionpicker.go+2 −2internal/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) |