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
This commit is contained in:
@@ -64,6 +64,7 @@ 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,
|
||||
@@ -84,6 +85,25 @@ 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()
|
||||
|
||||
@@ -94,27 +114,27 @@ func (m *MockClock) Advance(d time.Duration) {
|
||||
}
|
||||
t.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// Set sets the clock to an absolute time and fires any expired timers.
|
||||
func (m *MockClock) Set(t time.Time) {
|
||||
// Compact: remove inactive timers. Use a new slice to avoid aliasing
|
||||
// the backing array (NewTimer may have appended between snapshots).
|
||||
m.mu.Lock()
|
||||
m.now = t
|
||||
now := m.now
|
||||
timers := m.timers
|
||||
m.mu.Unlock()
|
||||
|
||||
for _, tmr := range timers {
|
||||
tmr.mu.Lock()
|
||||
if tmr.active && !now.Before(tmr.deadline) {
|
||||
tmr.fire(now)
|
||||
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)
|
||||
}
|
||||
tmr.mu.Unlock()
|
||||
}
|
||||
m.timers = active
|
||||
m.mu.Unlock()
|
||||
}
|
||||
|
||||
type mockTimer struct {
|
||||
mu sync.Mutex
|
||||
clock *MockClock
|
||||
ch chan time.Time
|
||||
deadline time.Time
|
||||
active bool
|
||||
@@ -131,12 +151,17 @@ func (t *mockTimer) Stop() bool {
|
||||
}
|
||||
|
||||
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
|
||||
// Note: deadline will be recalculated on next Advance
|
||||
t.deadline = time.Now().Add(d) // placeholder; mock users should use Advance
|
||||
t.deadline = deadline
|
||||
return was
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user