R-31: a second off-site Save while the first still provisions is refused, not raced

Both saves used to list no sub-account and create one (a dedicated box is a
second bill). Per-customer in-memory claim; the second gets 409 and nothing
is saved or created. The async save + status card stays open.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-10-06 21:04:21 +02:00
parent de3480fcd2
commit 6f8aa46cf7
4 changed files with 136 additions and 7 deletions
+12 -2
View File
@@ -779,7 +779,12 @@ func (s *Server) handleConfigCreate(w http.ResponseWriter, r *http.Request) {
}
// Offsite provisioning (fail-closed): a provisioning error must NOT save a half-enabled config.
if err := s.applyOffsite(r.Context(), r, cfg); err != nil {
if err := s.applyOffsite(r.Context(), r, cfg); errors.Is(err, offsite.ErrProvisionInProgress) {
// R-31: a second Save while the first still provisions — nothing saved, nothing created.
s.logger.Printf("[INFO] offsite provision for %s refused: one is already running (R-31)", customerID)
http.Error(w, err.Error(), http.StatusConflict)
return
} else if err != nil {
s.logger.Printf("[ERROR] offsite provision for %s: %v", customerID, err)
http.Error(w, "Offsite provisioning failed: "+err.Error(), http.StatusBadGateway)
return
@@ -859,7 +864,12 @@ func (s *Server) handleConfigUpdate(w http.ResponseWriter, r *http.Request, cust
cfg.ConfigJSON = buildConfigJSON(r)
if err := s.applyOffsite(r.Context(), r, cfg); err != nil {
if err := s.applyOffsite(r.Context(), r, cfg); errors.Is(err, offsite.ErrProvisionInProgress) {
// R-31: a second Save while the first still provisions — nothing saved, nothing created.
s.logger.Printf("[INFO] offsite provision for %s refused: one is already running (R-31)", customerID)
http.Error(w, err.Error(), http.StatusConflict)
return
} else if err != nil {
s.logger.Printf("[ERROR] offsite provision for %s: %v", customerID, err)
http.Error(w, "Offsite provisioning failed: "+err.Error(), http.StatusBadGateway)
return