carlos / platform Public

status --instances closes the console's shared registry handle; app create then fails for everyone until restart Issue closed

What happened

On 2026-10-03, creating apps on carloku failed for everyone. The browser form said "Could not check that name. Please try again." The CLI said "could not create the app — try again". Users who retried with the CLI then hit "app creation limit reached for today", because every failed attempt counts against the 10-per-day create budget.

The console log on the flagship IE box (build 7edd4b33) shows the cause, starting at 16:23:35 UTC and repeating until the console was restarted at 18:54 UTC:

console: api routes: registry list paths: sql: database is closed

Restarting console.service fixed it, but it will happen again the next time an operator runs carlos status --instances.

Cause

openRegistry (internal/console/view.go:506) caches one *registry.Registry per path for the life of the process. Every caller shares that handle.

handleAPIStatusInstances (internal/console/apistatusinstances.go:117, still on main) gets the shared handle through openPassRegistry and then closes it:

reg, err := s.openPassRegistry()   // returns the cached, process-wide handle
...
defer reg.Close()

After one carlos status --instances call, every later registry read in that console process fails with sql: database is closed. The registry is read when creating an app (apexHolder in apex.go), listing routes, managing domains, loading the fleet and account review pages, and in the CLI's config and create paths.

The failure was hard to diagnose because claimApp (internal/console/server.go, around lines 3455–3510) returns claimCheckFailed without logging the error in all three places it can fail:

  • the HasAppIn lookup
  • the HasOwners lookup
  • a non-bucket error from apexHolder

The CLI handler's comment says "the detail stays on the box", but for this outcome nothing is kept anywhere.

Fix

  1. Remove defer reg.Close() from handleAPIStatusInstances. Add a regression test: call the status-instances handler, then check that a later openRegistry read still works.
  2. Make it impossible to close the shared handle by accident. For example, openRegistry could return a wrapper whose Close does nothing, or the cache could hand out a type that has no Close method. Any future defer reg.Close() would then fail to compile or be harmless.
  3. Log err with the account and app name on each claimCheckFailed return in claimApp, so the next failure here can be seen in the console log.

Side effect worth knowing

The create budget is spent per attempt, before the name is read. When the console itself is broken, users use up their daily budget on retries that could never succeed. The budget is held in memory, so a console restart resets it. Consider not charging the budget for claimCheckFailed or claimStoreFailed outcomes, which are server faults, not user actions.

Fixed and merged to main as 78fc8d4 (branch commit 2bd3b744, review #210). Removed the handler Close; shared registry factories and fleet helpers now return an interface without Close. Added all three missing claimCheckFailed logs with account/app/operation/error context.

Regression reproduced sql: database is closed before the fix and now verifies shared ListPaths reads and app creation after status requests. Local build, vet, formatting and all package tests pass; branch CI passed gate, five cross-builds, and MinIO contract. Opus review: LAND, no blockers. Checking main CI now.

This is a source fix, not a production rollout. The optional create-attempt budget refund remains unchanged.

Main 78fc8d4 is green: gate, all five cross-builds, and MinIO contract passed. Source fix complete; production rollout is still pending.

The close operation was refused because only the author or an owner can close this discussion. Please close #208; the fix is merged and main CI is green. Releasing my claim now that implementation is complete.

Closing: fixed on main as 78fc8d4. carloku's console still runs 7edd4b33 until it is rolled, so status --instances can still break app create there until then.