package console import ( "bytes" "context" "encoding/json" "io" "net/http" "sync" "time" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/hostops" "github.com/carlosframework/platform/internal/hostwrite" "github.com/carlosframework/platform/internal/release" ) type WakeRetryResetDoor struct { production bool Claims release.Store Authority func(context.Context) error StopAttempt func(context.Context, hostclaim.Observed, hostclaim.WakeRetryAttempt) error NewID func() string // lifecycle registers the reset with the holder app's admission and // returns ctx carrying that registration's claim grant. The mounted door // always has it (Server.wakeRetryResetDoor); only a direct caller with no // claim runtime runs without it. lifecycle func(context.Context, hostclaim.Observed) (context.Context, func(), error) } // wakeRetryResetKind is the admission registration a reset runs under. const wakeRetryResetKind = hostops.RegistrationWakeRetryReset type wakeResetRequest struct { Action string `json:"action"` Incarnation string `json:"incarnation"` Episode string `json:"episode"` RequestID string `json:"request_id"` } func (d *WakeRetryResetDoor) reset(ctx context.Context, host, actor string, in wakeResetRequest) error { if d == nil || d.Claims == nil || d.Authority == nil || d.StopAttempt == nil || d.NewID == nil { return hostwrite.ErrUnavailable } if err := d.Authority(ctx); err != nil { return err } if actor == "" || in.Action != "reset-wake-retry" || in.Incarnation == "" || in.Episode == "" || in.RequestID == "" { return hostclaim.ErrWrongState } observed, err := hostclaim.Read(ctx, d.Claims, host) if err != nil { return err } if observed.Claim == nil || observed.Claim.Holder == nil || observed.Claim.Holder.Incarnation != in.Incarnation { return hostclaim.ErrWrongState } audit, _ := json.Marshal(struct{ Actor, RequestID string }{actor, in.RequestID}) retry := observed.Claim.Holder.WakeRetry if retry == nil || retry.EpisodeID != in.Episode { return hostclaim.ErrWrongState } // The registration comes before any attempt is stopped: a holder app // closed under another exclusive operation refuses here, with no effect. // The stop receives cancellation on renewal loss, and the claim write // carries the lifecycle grant. write := ctx if d.lifecycle != nil { var finish func() if write, finish, err = d.lifecycle(ctx, observed); err != nil { return err } defer finish() ctx = write } if err := ctx.Err(); err != nil { return err } if retry.State == "attempting" { if retry.Attempt == nil { return hostclaim.ErrWrongState } if err = d.StopAttempt(ctx, observed, *retry.Attempt); err != nil { return err } fresh, err := hostclaim.Read(ctx, d.Claims, host) if err != nil { return err } if fresh.ETag != observed.ETag || fresh.Claim == nil || fresh.Claim.Holder == nil || fresh.Claim.Holder.Incarnation != in.Incarnation { return hostclaim.ErrWrongState } r := fresh.Claim.Holder.WakeRetry if r == nil || r.EpisodeID != in.Episode || r.Attempt == nil || *r.Attempt != *retry.Attempt { return hostclaim.ErrWrongState } observed = fresh } if err := d.Authority(ctx); err != nil { return err } _, err = hostclaim.Update(write, d.Claims, observed, hostclaim.ResetWakeRetry(hostclaim.WakeRetryReset{HolderIncarnation: in.Incarnation, ObservedEpisode: in.Episode, NewEpisodeID: d.NewID(), OperatorRequest: string(audit), AttemptSettled: true})) return err } func (s *Server) handleHostWakeRetryReset(w http.ResponseWriter, r *http.Request, actor string, raw json.RawMessage) { if len(raw) > 16<<10 { w.WriteHeader(http.StatusBadRequest) return } var in wakeResetRequest decoder := json.NewDecoder(bytes.NewReader(raw)) decoder.DisallowUnknownFields() if decoder.Decode(&in) != nil || decoder.Decode(new(any)) != io.EOF { w.WriteHeader(http.StatusBadRequest) return } door := s.wakeRetryResetDoor() if door == nil { w.WriteHeader(http.StatusServiceUnavailable) return } if err := door.reset(r.Context(), r.PathValue("host"), actor, in); err != nil { w.WriteHeader(http.StatusConflict) return } w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(map[string]bool{"ok": true}) } // wakeRetryResetDoor is the configured door as the route runs it. The route // answers only with HostClaim on, so the door reads and writes claims through // Config.Releases (the guarded data boundary once ConfigureClaimRuntime ran), // never the handle it was configured with, and every claim write carries the // reset's own registration grant: no ordinary permit and no exemption. func (s *Server) wakeRetryResetDoor() *WakeRetryResetDoor { if s.cfg.WakeRetryReset == nil { return nil } d := *s.cfg.WakeRetryReset d.Claims = s.cfg.Releases d.lifecycle = s.registerWakeRetryReset return &d } // registerWakeRetryReset registers the reset with the holder app's canonical // admission: kind reset-wake-retry, the holder's exact incarnation and the // claim observation the door read. RegisterStart refuses a closed admission, // so a foreign exclusive operation stops the reset before StopAttempt. The // grant authorizes claim writes only for this holder app and only while the // registration is live. The request renews it until the stop and claim write // finish, then expires it so the admission sweep can settle it. func (s *Server) registerWakeRetryReset(ctx context.Context, observed hostclaim.Observed) (context.Context, func(), error) { a := s.cfg.ClaimAuthority if !s.claimRuntimeConfigured || a == nil || a.Control == nil { return nil, nil, hostwrite.ErrUnavailable } if observed.Absent || observed.Claim == nil || observed.Claim.Holder == nil { return nil, nil, hostclaim.ErrWrongState } h := observed.Claim.Holder account, err := s.store.AccountBySqid(h.Account) if err != nil { return nil, nil, err } if account == nil { return nil, nil, hostops.ErrIdentity } appID, err := s.store.ClaimAppID(account.ID, h.App) if err != nil { return nil, nil, err } admission, err := hostops.ReadAdmission(ctx, s.cfg.Releases, h.Account, h.App, appID) if err != nil { return nil, nil, err } id, err := claimEffectID() if err != nil { return nil, nil, err } controller, err := claimEffectID() if err != nil { return nil, nil, err } owner := hostops.RegistrationOwner{Console: a.ConsoleID, Enrollment: a.Enrollment, Controller: controller} reg := hostops.Registration{Kind: wakeRetryResetKind, Host: observed.Claim.Host, Owner: owner, AppID: admission.Value.AppID, Epoch: admission.Value.Epoch, Claim: hostops.SavedObservation(observed), Incarnation: h.Incarnation, CreatedAt: h.CreatedAt} registeredAt := time.Now() start, err := a.Control.RegisterStart(ctx, h.Account, h.App, reg.AppID, id, reg, registeredAt) if err != nil { return nil, nil, err } requestCtx, stopRenewal := keepWakeResetRegistration(ctx, registeredAt, hostops.RegistrationTTL-hostops.RegistrationSkew, hostops.RegistrationRenewEvery, start.Renew) finish := func() { stopRenewal() cleanup, done := context.WithTimeout(context.WithoutCancel(ctx), 5*time.Second) defer done() _ = start.FinishWakeRetryReset(cleanup) } return start.Lifecycle(requestCtx), finish, nil } func keepWakeResetRegistration(ctx context.Context, registeredAt time.Time, liveFor, every time.Duration, renew func(context.Context, time.Time) error) (context.Context, func()) { requestCtx, cancel := context.WithCancel(ctx) deadline := registeredAt.Add(liveFor) if !time.Now().Before(deadline) { cancel() } watchdog := time.AfterFunc(time.Until(deadline), cancel) var wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() ticker := time.NewTicker(every) defer ticker.Stop() for { select { case <-requestCtx.Done(): return case <-ticker.C: started := time.Now() renewCtx, done := context.WithDeadline(requestCtx, deadline) err := renew(renewCtx, started) done() if err != nil || requestCtx.Err() != nil { cancel() return } deadline = started.Add(liveFor) watchdog.Reset(time.Until(deadline)) } } }() return requestCtx, func() { cancel() wg.Wait() watchdog.Stop() } }