Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ require (
github.com/golang-migrate/migrate/v4 v4.19.1
github.com/google/uuid v1.6.0
github.com/joho/godotenv v1.5.1
github.com/pelletier/go-toml/v2 v2.3.1
golang.org/x/net v0.55.0
golang.org/x/sync v0.20.0
gorm.io/driver/postgres v1.6.0
Expand Down Expand Up @@ -71,7 +72,6 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pelletier/go-toml/v2 v2.3.1 // indirect
github.com/philhofer/fwd v1.2.0 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.59.1 // indirect
Expand Down
41 changes: 27 additions & 14 deletions internal/handler/admin_platform_stack_admins.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,30 @@ var aclJobLocks = aclJobLockSet{held: make(map[uint]struct{})}
// errAclJobBusy is a sentinel so the handler answers 409 (come back in a
// minute) rather than 502 (something broke). Nothing is wrong when this fires.
var errAclJobBusy = errors.New(
"another ACL operation is running for this VTA — that takes about a minute; try again after it finishes")
"another maintenance operation is running for this stack — try again after it finishes")

// acquireSessionMaintenance serialises every operation that stops one or more
// components in a stack. The in-process lock is fast; the database timestamp
// closes the same race across API replicas. Callers must invoke the returned
// release function.
func (h *SetupHandler) acquireSessionMaintenance(sessionID uint) (func(), error) {
if !aclJobLocks.TryLock(sessionID) {
return nil, errAclJobBusy
}
startedAt, ok, err := h.acquireVtaAclMaintenance(sessionID)
if err != nil {
aclJobLocks.Unlock(sessionID)
return nil, fmt.Errorf("failed to lock stack maintenance: %w", err)
}
if !ok {
aclJobLocks.Unlock(sessionID)
return nil, errAclJobBusy
}
return func() {
h.releaseVtaAclMaintenance(sessionID, startedAt)
aclJobLocks.Unlock(sessionID)
}, nil
}

// platformSession loads the platform stack's session, writing the response and
// returning nil when there isn't one. Same two-step lookup GetPlatformStack
Expand Down Expand Up @@ -183,21 +206,11 @@ func (h *SetupHandler) runVtaAclJob(
// rather than Lock: a caller who waits would sit through the other window
// and then start their own, so the honest answer is to refuse now and let
// them retry once — a queue of these is a queue of outages.
if !aclJobLocks.TryLock(session.ID) {
return "", nil, errAclJobBusy
}
defer aclJobLocks.Unlock(session.ID)

// The in-process lock above protects callers handled by this replica. This
// row closes the same race across replicas and doubles as snapshot metadata.
lockStartedAt, ok, lockErr := h.acquireVtaAclMaintenance(session.ID)
release, lockErr := h.acquireSessionMaintenance(session.ID)
if lockErr != nil {
return "", nil, fmt.Errorf("failed to lock ACL maintenance: %w", lockErr)
}
if !ok {
return "", nil, errAclJobBusy
return "", nil, lockErr
}
defer h.releaseVtaAclMaintenance(session.ID, lockStartedAt)
defer release()

ns := h.k8s.UserNamespace(fmt.Sprintf("%d", session.UserID))
target := vtaAclTargetFor(session)
Expand Down
Loading
Loading