| 1 | package console |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | |
| 6 | "github.com/carlosframework/platform/internal/hostops" |
| 7 | "github.com/carlosframework/platform/internal/release" |
| 8 | ) |
| 9 | |
| 10 | // claimAppCall runs one independent app mutation as a joined exclusive |
| 11 | // app-mutation operation through the data boundary's runner |
| 12 | // (hostops RunAppMutation via release.WithAppMutation). The whole walk is |
| 13 | // one durable effect; its permit is what lets the walk's bucket, SQL and |
| 14 | // backend writes through the boundaries. The runner settles a callback |
| 15 | // error "not-applied" (and reopens) only when no boundary marked a write or |
| 16 | // backend call issued, so an ordinary validation or stale-etag refusal does |
| 17 | // not hold the app. Once anything was issued, failure stays uncertain and |
| 18 | // keeps the operation closed for fenced reconciliation; it never expires |
| 19 | // like a registration. |
| 20 | // |
| 21 | // Restore is NOT routed here: the SQL agent's claim_restore.go owns the |
| 22 | // receipt-first restore door on the same typed API (BeginExclusive, |
| 23 | // PrepareRestore, CompleteRestore, RecoverMutation). |
| 24 | func claimAppCall[T any](s *Server, ctx context.Context, acct *Account, app, action string, call func(context.Context) (T, error)) (T, error) { |
| 25 | var result T |
| 26 | if !s.cfg.HostClaim { |
| 27 | return call(ctx) |
| 28 | } |
| 29 | // Fail closed: a plain bucket would run the call with no operation. |
| 30 | if _, ok := s.cfg.Releases.(release.AppMutationRunner); !ok { |
| 31 | return result, hostops.ErrContract |
| 32 | } |
| 33 | err := release.WithAppMutation(ctx, s.cfg.Releases, acct.Sqid, app, action, func(callCtx context.Context) error { |
| 34 | var callErr error |
| 35 | result, callErr = call(callCtx) |
| 36 | return callErr |
| 37 | }) |
| 38 | return result, err |
| 39 | } |
| 40 | |
| 41 | func (s *Server) claimAppMutation(ctx context.Context, acct *Account, app, action string, call func(context.Context) error) error { |
| 42 | _, err := claimAppCall(s, ctx, acct, app, action, func(ctx context.Context) (struct{}, error) { return struct{}{}, call(ctx) }) |
| 43 | return err |
| 44 | } |
| 45 | |
| 46 | // claimProjection runs one per-console SQL projection of open-admission |
| 47 | // bucket evidence (restore receipt, tombstone) inside its own short |
| 48 | // exclusive operation, so purge cannot start between the evidence read and |
| 49 | // the SQL write. fn receives the permitted context for RestoreProjection / |
| 50 | // TrashProjection and the SQL view. |
| 51 | func (s *Server) claimProjection(ctx context.Context, acct *Account, app string, fn func(context.Context) error) error { |
| 52 | return s.claimAppMutation(ctx, acct, app, "projection", fn) |
| 53 | } |
| 54 | |