Files
httpx/internal/clock/clock.go
Aleksey Shakhmatov 5cfd1a7400 Fix sentinel error aliasing, hot-path allocations, and resource leaks
- Deduplicate sentinel errors: httpx.ErrNoHealthy, ErrCircuitOpen, and
  ErrRetryExhausted are now aliases to the canonical sub-package values
  so errors.Is works across package boundaries
- Retry transport returns ErrRetryExhausted only when all attempts are
  actually exhausted, not on early policy exit
- Balancer: pre-parse endpoint URLs at construction, replace req.Clone
  with cheap shallow struct copy to avoid per-request allocations
- Circuit breaker: Load before LoadOrStore to avoid allocating a Breaker
  on every request for known hosts
- Health checker: drain response body before close for connection reuse,
  probe endpoints concurrently, run initial probe synchronously in Start
- Client: add Close() to shut down health checker goroutine, propagate
  URL resolution errors instead of silently discarding them
- MockClock: fix lock ordering in Reset (clock.mu before t.mu), fix
  timer slice compaction to avoid backing-array aliasing, extract
  fireExpired to deduplicate Advance/Set
2026-03-20 15:21:32 +03:00

176 lines
3.8 KiB
Go

package clock
import (
"sync"
"time"
)
// Clock abstracts time operations for deterministic testing.
type Clock interface {
Now() time.Time
Since(t time.Time) time.Duration
NewTimer(d time.Duration) Timer
After(d time.Duration) <-chan time.Time
}
// Timer abstracts time.Timer for testability.
type Timer interface {
C() <-chan time.Time
Stop() bool
Reset(d time.Duration) bool
}
// System returns a Clock backed by the real system time.
func System() Clock { return systemClock{} }
type systemClock struct{}
func (systemClock) Now() time.Time { return time.Now() }
func (systemClock) Since(t time.Time) time.Duration { return time.Since(t) }
func (systemClock) NewTimer(d time.Duration) Timer { return &systemTimer{t: time.NewTimer(d)} }
func (systemClock) After(d time.Duration) <-chan time.Time { return time.After(d) }
type systemTimer struct{ t *time.Timer }
func (s *systemTimer) C() <-chan time.Time { return s.t.C }
func (s *systemTimer) Stop() bool { return s.t.Stop() }
func (s *systemTimer) Reset(d time.Duration) bool { return s.t.Reset(d) }
// Mock returns a manually-controlled Clock for tests.
func Mock(now time.Time) *MockClock {
return &MockClock{now: now}
}
// MockClock is a deterministic clock for testing.
type MockClock struct {
mu sync.Mutex
now time.Time
timers []*mockTimer
}
func (m *MockClock) Now() time.Time {
m.mu.Lock()
defer m.mu.Unlock()
return m.now
}
func (m *MockClock) Since(t time.Time) time.Duration {
m.mu.Lock()
defer m.mu.Unlock()
return m.now.Sub(t)
}
func (m *MockClock) NewTimer(d time.Duration) Timer {
m.mu.Lock()
defer m.mu.Unlock()
t := &mockTimer{
clock: m,
ch: make(chan time.Time, 1),
deadline: m.now.Add(d),
active: true,
}
m.timers = append(m.timers, t)
if d <= 0 {
t.fire(m.now)
}
return t
}
func (m *MockClock) After(d time.Duration) <-chan time.Time {
return m.NewTimer(d).C()
}
// Advance moves the clock forward by d and fires any expired timers.
func (m *MockClock) Advance(d time.Duration) {
m.mu.Lock()
m.now = m.now.Add(d)
now := m.now
m.mu.Unlock()
m.fireExpired(now)
}
// Set sets the clock to an absolute time and fires any expired timers.
func (m *MockClock) Set(t time.Time) {
m.mu.Lock()
m.now = t
now := m.now
m.mu.Unlock()
m.fireExpired(now)
}
// fireExpired fires all active timers whose deadline has passed, then
// removes inactive timers to prevent unbounded growth.
func (m *MockClock) fireExpired(now time.Time) {
m.mu.Lock()
timers := m.timers
m.mu.Unlock()
for _, t := range timers {
t.mu.Lock()
if t.active && !now.Before(t.deadline) {
t.fire(now)
}
t.mu.Unlock()
}
// Compact: remove inactive timers. Use a new slice to avoid aliasing
// the backing array (NewTimer may have appended between snapshots).
m.mu.Lock()
n := len(m.timers)
active := make([]*mockTimer, 0, n)
for _, t := range m.timers {
t.mu.Lock()
keep := t.active
t.mu.Unlock()
if keep {
active = append(active, t)
}
}
m.timers = active
m.mu.Unlock()
}
type mockTimer struct {
mu sync.Mutex
clock *MockClock
ch chan time.Time
deadline time.Time
active bool
}
func (t *mockTimer) C() <-chan time.Time { return t.ch }
func (t *mockTimer) Stop() bool {
t.mu.Lock()
defer t.mu.Unlock()
was := t.active
t.active = false
return was
}
func (t *mockTimer) Reset(d time.Duration) bool {
// Acquire clock lock first to match the lock ordering in fireExpired
// (clock.mu → t.mu), preventing deadlock.
t.clock.mu.Lock()
deadline := t.clock.now.Add(d)
t.clock.mu.Unlock()
t.mu.Lock()
defer t.mu.Unlock()
was := t.active
t.active = true
t.deadline = deadline
return was
}
// fire sends the time on the channel. Caller must hold t.mu.
func (t *mockTimer) fire(now time.Time) {
t.active = false
select {
case t.ch <- now:
default:
}
}