sendToOutChan is the single send path to outChan. Holds outChanMu read-lock across the whole observation + send so CloseOutChan (the writer-lock holder) cannot interleave a close between our not-closed check and the chansend. Tries a non-blocking send first (the fast, jitter-free common case where
(mock *models.Mock)
| 326 | // customer-facing recording-loss flake and is strictly worse than a |
| 327 | // 200 ms worst-case shutdown delay. |
| 328 | func (m *SyncMockManager) sendToOutChan(mock *models.Mock) { |
| 329 | m.outChanMu.RLock() |
| 330 | defer m.outChanMu.RUnlock() |
| 331 | if m.outChanClosed || m.outChan == nil { |
| 332 | return |
| 333 | } |
| 334 | select { |
| 335 | case m.outChan <- mock: |
| 336 | return |
| 337 | default: |
| 338 | } |
| 339 | // Fast path full. Bounded block so normal scheduling jitter |
| 340 | // doesn't cost us a mock. |
| 341 | timer := time.NewTimer(sendBudget) |
| 342 | select { |
| 343 | case m.outChan <- mock: |
| 344 | timer.Stop() |
| 345 | case <-timer.C: |
| 346 | n := m.dropCount.Add(1) |
| 347 | // The existing per-1024 sampled Error fires at n==1 AND every |
| 348 | // subsequent 1024th drop. Per-Copilot review on #4176, the |
| 349 | // "your recording is now lossy" wording lives on the same n==1 |
| 350 | // branch rather than as a separate Warn so operators see one |
| 351 | // clear signal at the moment capture goes lossy, instead of |
| 352 | // two separate lines that may interleave with other logs. |
| 353 | // Subsequent sampled emissions stay terse to avoid drowning a |
| 354 | // stuck consumer's goroutine in its own logging. |
| 355 | if n == 1 || n%sendDropSampleRate == 0 { |
| 356 | msg := "syncMock outChan overflow; mock dropped — consumer can't keep up with mock production" |
| 357 | if n == 1 { |
| 358 | msg = "syncMock outChan overflow on FIRST drop — mock recording is now LOSSY for this session; subsequent overflow drops are silent except for the per-1024 sampled line. Reduce concurrent test load, upgrade to a release with a larger outChan capacity, or investigate consumer-side stalls (slow disk / network to k8s-proxy) before re-running for a clean recording." |
| 359 | } |
| 360 | m.dropLogger().Error(msg, |
| 361 | zap.Uint64("dropsSoFar", n), |
| 362 | zap.Int("outChanCap", cap(m.outChan)), |
| 363 | zap.Duration("budget", sendBudget), |
| 364 | ) |
| 365 | } |
| 366 | } |
| 367 | } |
| 368 | |
| 369 | // DropCount exposes the cumulative drop counter for tests and |
| 370 | // external observability. The value is monotonically increasing; |