package hostbox import ( "context" "errors" "sort" "github.com/carlosframework/platform/internal/park" ) // LeaseSet names locks in the engine's data bucket. A moves/[/] // scope needs its own lock; staging writers are bounded by Park.EvidenceAt. // An instances/[/] scope needs its replica lease and scope lock // (the same location, deduplicated). Deployment-bucket staging leases must // never be expressed as data-bucket names. An empty set refuses. type LeaseSet func(context.Context, string, string) ([]string, error) // AggregateLeases adapts a renewing lease implementation to park.Leases. // Constituents are acquired in lexical order so competing workers cannot // deadlock by discovering the same set in a different order. type AggregateLeases struct { Leases park.Leases Resolve LeaseSet } func (a AggregateLeases) Acquire(ctx context.Context, bucket, scope string) (park.HeldLease, error) { return a.acquire(ctx, bucket, scope, nil) } func (a AggregateLeases) AcquireHeld(ctx context.Context, bucket, scope string, own park.HeldLease) (park.HeldLease, error) { bound, ok := own.(interface{ LeaseScope() (string, string) }) if !ok || own == nil { return nil, ErrUnsupported } b, s := bound.LeaseScope() if b != bucket || s != scope { return nil, ErrChanged } if err := own.Check(ctx); err != nil { return nil, err } return a.acquire(ctx, bucket, scope, own) } func (a AggregateLeases) acquire(ctx context.Context, bucket, scope string, own park.HeldLease) (park.HeldLease, error) { if ctx == nil || a.Leases == nil || a.Resolve == nil || bucket == "" || scope == "" { return nil, ErrUnsupported } names, err := a.Resolve(ctx, bucket, scope) if err != nil { return nil, err } names = append([]string(nil), names...) sort.Strings(names) uniq := names[:0] hasScope := false for _, name := range names { if name == "" { return nil, ErrUnsupported } if len(uniq) == 0 || uniq[len(uniq)-1] != name { uniq = append(uniq, name) } if name == scope { hasScope = true } } if len(uniq) == 0 || !hasScope { return nil, ErrUnsupported } held := &aggregateHeld{bucket: bucket, scope: scope} for _, name := range uniq { var h park.HeldLease var acquireErr error if own != nil && name == scope { h = borrowedLease{own} } else { h, acquireErr = a.Leases.Acquire(ctx, bucket, name) } if acquireErr != nil { return nil, errors.Join(acquireErr, held.releaseAndClose(ctx)) } if h == nil { return nil, errors.Join(ErrUnsupported, held.releaseAndClose(ctx)) } held.leases = append(held.leases, h) } return held, nil } type aggregateHeld struct { bucket, scope string leases []park.HeldLease closed bool } func (h *aggregateHeld) Check(ctx context.Context) error { if h == nil || h.closed || len(h.leases) == 0 { return ErrChanged } var errs []error for _, lease := range h.leases { errs = append(errs, lease.Check(ctx)) } return errors.Join(errs...) } func (h *aggregateHeld) Release(ctx context.Context) error { if h == nil || h.closed || len(h.leases) == 0 { return ErrChanged } var errs []error for i := len(h.leases) - 1; i >= 0; i-- { errs = append(errs, h.leases[i].Release(ctx)) } return errors.Join(errs...) } func (h *aggregateHeld) Close() { if h == nil || h.closed { return } h.closed = true for i := len(h.leases) - 1; i >= 0; i-- { h.leases[i].Close() } } func (h *aggregateHeld) releaseAndClose(ctx context.Context) error { var errs []error for i := len(h.leases) - 1; i >= 0; i-- { errs = append(errs, h.leases[i].Release(ctx)) h.leases[i].Close() } h.closed = true return errors.Join(errs...) } func (h *aggregateHeld) LeaseScope() (string, string) { return h.bucket, h.scope } type borrowedLease struct{ park.HeldLease } func (b borrowedLease) Release(ctx context.Context) error { return b.Check(ctx) } func (b borrowedLease) Close() {} func (b borrowedLease) Context(ctx context.Context) context.Context { if bound, ok := b.HeldLease.(interface { Context(context.Context) context.Context }); ok { return bound.Context(ctx) } return ctx }