package maintenanceguard import ( "context" "errors" "math" "os" "slices" ) type GuardSnapshot struct { repository *Repository value Guard etag string exists bool } func (s GuardSnapshot) Exists() bool { return s.exists } func (s GuardSnapshot) Value() Guard { return cloneGuard(s.value) } func (s GuardSnapshot) Projection() (Projection, error) { if !s.exists { return Projection{}, ErrInvalid } raw, e := guardBytes(s.value) if e != nil { return Projection{}, e } return Projection{Version, s.value.Generation, digest(raw)}, nil } func (r *Repository) ReadGuard(ctx context.Context, host string) (GuardSnapshot, error) { key, e := GuardKey(host) if e != nil { return GuardSnapshot{}, e } raw, etag, e := r.store.Get(ctx, key) if errors.Is(e, os.ErrNotExist) { return GuardSnapshot{repository: r}, nil } if e != nil { return GuardSnapshot{}, e } var g Guard if decode(raw, &g) != nil || !g.valid() || g.Identity.Host != host || !cleanETag(etag) { return GuardSnapshot{}, ErrInvalid } if e = r.checkGuardReferences(ctx, g); e != nil { return GuardSnapshot{}, e } return GuardSnapshot{r, cloneGuard(g), etag, true}, nil } func (r *Repository) guardClaim(ctx context.Context, c ClaimSnapshot, g Guard) error { if c.value.Kind != MaintenanceOperation || c.value.Account != g.Identity.Account || c.value.App != g.Identity.App || c.value.OperationID != g.OperationID { return ErrConflict } return r.currentClaim(ctx, c) } func (r *Repository) writeGuard(ctx context.Context, c ClaimSnapshot, g Guard, etag string) (GuardSnapshot, error) { if e := r.guardClaim(ctx, c, g); e != nil { return GuardSnapshot{}, e } raw, e := guardBytes(g) if e != nil { return GuardSnapshot{}, e } if e = r.checkGuardReferences(ctx, g); e != nil { return GuardSnapshot{}, e } key, _ := GuardKey(g.Identity.Host) next, e := r.store.Put(ctx, key, raw, etag) if e != nil { return GuardSnapshot{}, e } if !cleanETag(next) { return GuardSnapshot{}, ErrUnavailable } // There is no cross-object atomicity. A lost claim leaves the durable guard // in place, but the caller receives no successful execution/resume handoff. if e = r.currentClaim(ctx, c); e != nil { return GuardSnapshot{}, e } return GuardSnapshot{r, cloneGuard(g), next, true}, nil } func (r *Repository) CreateGuard(ctx context.Context, c ClaimSnapshot, g Guard) (GuardSnapshot, error) { if !g.valid() || g.Generation != 1 || g.Phase != Requested || g.Evidence != nil { return GuardSnapshot{}, ErrInvalid } return r.writeGuard(ctx, c, g, "") } func newAttempt(old, next Guard) bool { return old.Request.ID != next.Request.ID && old.Attempt.ID != next.Attempt.ID && next.Evidence == nil } func samePins(old, next Guard) bool { return old.RecordRevision == next.RecordRevision && old.ArtifactVersion == next.ArtifactVersion && old.ArtifactDigest == next.ArtifactDigest && old.ConfigDigest == next.ConfigDigest && slices.Equal(old.RequiredContracts, next.RequiredContracts) } func validAdvance(old, next Guard) bool { if !next.valid() || old.Generation == math.MaxInt64 || next.Generation != old.Generation+1 || next.Identity != old.Identity || next.OwnerBox != old.OwnerBox || !containsAll(next.RequiredContracts, old.RequiredContracts) { return false } if old.Phase == Resumed { return next.Phase == Requested && next.OperationID != old.OperationID && newAttempt(old, next) } if next.OperationID != old.OperationID { return false } if next.Phase == Requested { return (old.Phase == Succeeded || old.Phase == Failed || old.Phase == Ambiguous) && newAttempt(old, next) } if next.Phase == ResumeRequested { return (old.Phase == Fenced || old.Phase == Succeeded || old.Phase == Failed || old.Phase == Ambiguous) && newAttempt(old, next) } if old.Request != next.Request || old.Attempt != next.Attempt || !samePins(old, next) { return false } switch old.Phase { case Requested: return next.Phase == Quiescing || next.Phase == Failed || next.Phase == Ambiguous case Quiescing: return next.Phase == Fenced || next.Phase == Failed || next.Phase == Ambiguous case Fenced: return next.Phase == Executing || next.Phase == Failed || next.Phase == Ambiguous case Executing: return next.Phase == Succeeded || next.Phase == Failed || next.Phase == Ambiguous case ResumeRequested: return next.Phase == Resumed || next.Phase == Failed || next.Phase == Ambiguous } return false } // AdvanceGuard validates continuity and state progression, then performs one // CAS. Fresh owner approval, stopped-process/lease proofs and checker semantics // remain caller preconditions; valid references alone do not prove them. func (r *Repository) AdvanceGuard(ctx context.Context, c ClaimSnapshot, old GuardSnapshot, next Guard) (GuardSnapshot, error) { if old.repository != r || !old.exists || !cleanETag(old.etag) || !validAdvance(old.value, next) { return GuardSnapshot{}, ErrConflict } return r.writeGuard(ctx, c, next, old.etag) } type ServingDecision string const ( Unmanaged ServingDecision = "unmanaged" Managed ServingDecision = "managed" Denied ServingDecision = "denied" ) // CheckServing is a denial check, not a lease or process-health permit. The // caller supplies contracts from an already verified signed release manifest, // verifies the current record identity, and repeats this check after lease // acquisition before starting or seeding anything. Legacy unmanaged records may // omit Record: both barriers must be absent before that omission is accepted. func (r *Repository) CheckServing(ctx context.Context, identity Identity, projection *Projection, verifiedContracts []string) (ServingDecision, error) { if !identity.locationValid() || !ValidContracts(verifiedContracts) { return Denied, ErrDenied } s, e := r.ReadGuard(ctx, identity.Host) if e != nil { return Denied, ErrDenied } if !s.exists { if projection == nil { return Unmanaged, nil } return Denied, ErrDenied } if !identity.valid() || projection == nil || !projection.valid() || s.value.Identity != identity || s.value.Phase != Resumed || !containsAll(verifiedContracts, s.value.RequiredContracts) { return Denied, ErrDenied } expected, e := s.Projection() if e != nil || *projection != expected { return Denied, ErrDenied } return Managed, nil }