package host import ( "context" "encoding/json" "errors" "net" "net/http" "sync" "time" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/instancectl" "github.com/carlosframework/platform/internal/rootstate" ) var openClaimServingParkAuthority = func() (rootstate.Store, error) { return rootstate.Open("/var/lib/carlos-claims/authority/serving-parks", true, false) } type servingParkAuthority interface { parkServingDatabase(context.Context, net.Conn, servingArtifactIdentity, servingArtifactIdentity) (servingParkAuthorization, error) parkLegacyServingDatabase(context.Context, net.Conn, servingArtifactIdentity, servingLegacyPredecessor) (servingParkAuthorization, error) completeServingDatabasePark(context.Context, net.Conn, string, servingArtifactIdentity) error retainLegacyNoProcess(context.Context, hostclaim.Observed, StopResult) error } type servingEmptyAuthority interface { EmptyAuthority(context.Context, hostclaim.Observed, ClaimLaunchInputs) error } // Closing cancels and joins every request and stop-evidence retention before // releasing private state. A timed-out join leaves the store open for retry. type servingParkRuntime struct { authority servingParkAuthority state rootstate.Store ctx context.Context cancel context.CancelFunc mu sync.Mutex active int recoveryMu sync.Mutex recoveryAfter string lastRecoveryLog time.Time closing, closed, started bool idle chan struct{} } func withServingParkRoot(h ClaimRootHTTP, empty servingEmptyAuthority) (ClaimRootHTTP, error) { state, err := openClaimServingParkAuthority() if err != nil { return ClaimRootHTTP{}, err } runtime, err := newServingParkRuntime(h, state, empty) if err != nil { state.Close() return ClaimRootHTTP{}, err } h.servingParks = runtime h.Stop.Coordinator.RetainNoProcess = runtime.retainNoProcess return h, nil } func newServingParkRuntime(h ClaimRootHTTP, state rootstate.Store, empty servingEmptyAuthority) (*servingParkRuntime, error) { authority, err := newServingParkAuthority(h, state, empty) if err != nil { return nil, err } ctx, cancel := context.WithCancel(context.Background()) return &servingParkRuntime{authority: authority, state: state, ctx: ctx, cancel: cancel, idle: make(chan struct{})}, nil } func (p *servingParkRuntime) begin(ctx context.Context) (context.Context, func(), error) { if p == nil || ctx == nil { return nil, nil, hostbox.ErrUnsupported } p.mu.Lock() defer p.mu.Unlock() if p.closing { return nil, nil, hostbox.ErrUnsupported } p.active++ call, cancel := context.WithCancel(ctx) stop := context.AfterFunc(p.ctx, cancel) return call, func() { stop() cancel() p.mu.Lock() defer p.mu.Unlock() p.active-- if p.closing && p.active == 0 { close(p.idle) } }, nil } func (p *servingParkRuntime) Close(ctx context.Context) error { if p == nil { return nil } p.mu.Lock() if !p.closing { p.closing = true p.cancel() if p.active == 0 { close(p.idle) } } p.mu.Unlock() select { case <-p.idle: case <-ctx.Done(): select { case <-p.idle: default: return ctx.Err() } } p.mu.Lock() defer p.mu.Unlock() if p.closed { return nil } p.closed = true return p.state.Close() } func (p *servingParkRuntime) retainNoProcess(ctx context.Context, o hostclaim.Observed, result StopResult) error { call, done, err := p.begin(ctx) if err != nil { return err } defer done() return p.authority.retainLegacyNoProcess(call, o, result) } func (h ClaimRootHTTP) serveServingPark(w http.ResponseWriter, r *http.Request, q hostbox.JournalRequest) { out := hostbox.JournalResponse{V: 1, ID: q.ID} ctx, done, err := h.servingParks.begin(r.Context()) if err != nil { w.WriteHeader(http.StatusServiceUnavailable) return } defer done() conn := instancectl.RequestConn(ctx) if conn == nil || h.RequireNewWork == nil || h.InventoryActor == nil || h.Journals == nil || q.ServingPark == nil { w.WriteHeader(http.StatusForbidden) return } // This gate also covers receipt-only replay. The outer HTTP handler has // authenticated the live edge main process; effects authenticate it again // after acquiring root's namespace and host locks. if err = h.RequireNewWork(ctx); err != nil { w.WriteHeader(http.StatusForbidden) return } actor, err := h.InventoryActor(ctx) if err != nil || !actor.Eligible || actor.Quarantined || actor.Box == "" || actor.Enrollment == "" || actor.Revision == 0 || actor.Binding == "" || q.ServingPark.Successor.Box != actor.Box || q.ServingPark.Successor.Region != actor.Region { w.WriteHeader(http.StatusForbidden) return } out, err = h.Journals.ExecuteServingPark(ctx, actor.Box+"/"+actor.Enrollment, q, func(ctx context.Context) (hostbox.ServingParkResult, error) { p, a := *q.ServingPark, h.servingParks.authority switch q.Op { case "serving-park": return a.parkServingDatabase(ctx, conn, p.Successor, p.Predecessor) case "serving-park-legacy": return a.parkLegacyServingDatabase(ctx, conn, p.Successor, p.Legacy) case "serving-park-complete": err := a.completeServingDatabasePark(ctx, conn, p.Retirement, p.Successor) return hostbox.ServingParkResult{ID: p.Retirement, Successor: p.Successor}, err default: return hostbox.ServingParkResult{}, hostbox.ErrUnsupported } }) if err != nil { out.ServingPark = nil switch { case errors.Is(err, hostbox.ErrJournalUncertain): out.Error = "uncertain" case errors.Is(err, hostbox.ErrChanged), errors.Is(err, hostbox.ErrTypedJournal): out.Error = "refused" case errors.Is(err, hostbox.ErrUnsupported): out.Error = "unsupported" default: out.Error = "uncertain" } } w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(out) }