| 63 | } |
| 64 | |
| 65 | func (p *Server) scheduleDebugCleanup( |
| 66 | ctx context.Context, |
| 67 | logMessage string, |
| 68 | fields []slog.Field, |
| 69 | cleanup func(context.Context, *chatdebug.Service) error, |
| 70 | ) { |
| 71 | debugSvc := p.debugService() |
| 72 | if debugSvc == nil { |
| 73 | return |
| 74 | } |
| 75 | |
| 76 | // Acquire inflightMu around the positive Add so Close() cannot |
| 77 | // call drainInflight concurrently when the counter is at zero. |
| 78 | // See drainInflight for the WaitGroup contract this preserves. |
| 79 | p.inflightMu.Lock() |
| 80 | p.inflight.Add(1) |
| 81 | p.inflightMu.Unlock() |
| 82 | go func() { |
| 83 | defer p.inflight.Done() |
| 84 | |
| 85 | cleanupCtx := context.WithoutCancel(ctx) |
| 86 | for attempt := 0; attempt < debugCleanupAttempts; attempt++ { |
| 87 | if attempt > 0 { |
| 88 | timer := p.clock.NewTimer(debugCleanupRetryDelay, "chatd", "debug_cleanup") |
| 89 | <-timer.C |
| 90 | } |
| 91 | |
| 92 | passCtx, cancel := context.WithTimeout(cleanupCtx, debugCleanupTimeout) |
| 93 | err := cleanup(passCtx, debugSvc) |
| 94 | cancel() |
| 95 | if err == nil { |
| 96 | return |
| 97 | } |
| 98 | |
| 99 | logFields := append([]slog.Field{ |
| 100 | slog.F("attempt", attempt+1), |
| 101 | slog.F("max_attempts", debugCleanupAttempts), |
| 102 | }, fields...) |
| 103 | logFields = append(logFields, slog.Error(err)) |
| 104 | p.logger.Warn(cleanupCtx, logMessage, logFields...) |
| 105 | } |
| 106 | }() |
| 107 | } |
| 108 | |
| 109 | func (p *Server) newDebugAwareModel( |
| 110 | ctx context.Context, |