package host import ( "context" "errors" "log" "sort" "time" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/instancectl" ) // Select at most one page of the oldest complete parks. Incomplete records and // unknown contents never contribute reclaimable space. The namespace lock // serializes selection, slot ownership and identity-checked deletion. func (l *servingParkLedger) maintainParks(ctx context.Context, room func() error) error { if room == nil { return hostbox.ErrUnsupported } return l.withNamespace(ctx, func() error { if err := ctx.Err(); err != nil { return err } if err := l.collectOrphanBindings(ctx); err != nil { return err } pressure := room() if pressure != nil && !errors.Is(pressure, instancectl.ErrDiskFull) { return pressure } var candidates []servingParkRecord var failures []error after := "" for { ids, more, err := l.recoveryIDs(ctx, after, servingParkRecoveryLimit) if err != nil { return err } for _, id := range ids { if err := ctx.Err(); err != nil { return err } r, err := l.read(id) if err != nil { if len(failures) < servingParkRecoveryLimit { failures = append(failures, err) } continue } if r.Complete && !r.Closed && r.Disposition == servingParkRetain && (pressure != nil || !l.now().Before(r.ReapAfter)) { candidates = append(candidates, r) sort.Slice(candidates, func(i, j int) bool { if candidates[i].ParkedAt.Equal(candidates[j].ParkedAt) { return candidates[i].Authorization.ID < candidates[j].Authorization.ID } return candidates[i].ParkedAt.Before(candidates[j].ParkedAt) }) if len(candidates) > servingParkRecoveryLimit { candidates = candidates[:servingParkRecoveryLimit] } } } if !more { break } after = ids[len(ids)-1] } for _, r := range candidates { if err := ctx.Err(); err != nil { return err } cause := "expiry" if l.now().Before(r.ReapAfter) { err := room() if err == nil { continue } if !errors.Is(err, instancectl.ErrDiskFull) { return err } cause = "pressure" } err := func() error { unlock, err := l.state.Lock(ctx, "slot-"+servingParkKey(r.Authorization.Successor.Box, r.Authorization.Successor.Host)+".lock") if err != nil { return err } defer unlock() unlock, err = l.state.Lock(ctx, "retirement-"+r.Authorization.ID+".lock") if err != nil { return err } defer unlock() if r.Deleting { cause = r.Cause } if err := l.deleteLocked(ctx, &r, cause); err != nil { return err } slot, err := l.slot(r.Authorization) if err != nil { return err } return l.collectClosedLocked(ctx, &r, slot) }() if err != nil { failures = append(failures, err) } } return errors.Join(failures...) }) } func (p *servingParkRuntime) Start(ctx context.Context) { if p == nil { return } p.mu.Lock() if p.started || p.closing { p.mu.Unlock() return } p.started = true p.mu.Unlock() call, done, err := p.begin(ctx) if err != nil { return } go func() { defer done() ticker := time.NewTicker(time.Minute) defer ticker.Stop() after := "" lastLog := time.Time{} for { bounded, cancel := context.WithCancel(call) if a, ok := p.authority.(interface { maintainServingParks(context.Context, string) (string, error) }); ok { var err error after, err = a.maintainServingParks(bounded, after) if err != nil && call.Err() == nil && time.Since(lastLog) >= time.Hour { lastLog = time.Now() log.Print(err) } } cancel() select { case <-call.Done(): return case <-ticker.C: } } }() } func maintainServingParkPass(ctx context.Context, after string, cleanup func(context.Context) error, recover func(context.Context, string, int) (servingParkRecoveryPage, error), budget time.Duration) (string, error) { pressure, stopPressure := context.WithTimeout(ctx, budget) pressureErr := cleanup(pressure) stopPressure() recovery, stopRecovery := context.WithTimeout(ctx, budget) defer stopRecovery() page, err := recover(recovery, after, servingParkRecoveryLimit) for _, result := range page.Results { err = errors.Join(err, result.Err) } next := "" if page.More { next = page.Next } return next, errors.Join(pressureErr, err) }