Add worker self-retire on terminal dial failure
This commit is contained in:
@@ -41,6 +41,7 @@ const (
|
|||||||
EventConnected PoolEventKind = "connected"
|
EventConnected PoolEventKind = "connected"
|
||||||
EventDisconnected PoolEventKind = "disconnected"
|
EventDisconnected PoolEventKind = "disconnected"
|
||||||
EventDialFailed PoolEventKind = "dial_failed"
|
EventDialFailed PoolEventKind = "dial_failed"
|
||||||
|
EventRetired PoolEventKind = "retired"
|
||||||
)
|
)
|
||||||
|
|
||||||
type PoolEvent struct {
|
type PoolEvent struct {
|
||||||
@@ -70,6 +71,7 @@ type PoolPlugin struct {
|
|||||||
Inbox chan<- types.InboxMessage
|
Inbox chan<- types.InboxMessage
|
||||||
Events chan<- PoolEvent
|
Events chan<- PoolEvent
|
||||||
InboxCounter *atomic.Uint64
|
InboxCounter *atomic.Uint64
|
||||||
|
Retire func(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -219,7 +219,11 @@ func (w *DefaultWorker) runSession(ctx context.Context, pool PoolPlugin) {
|
|||||||
ctx, retryMgr, w.dialFn, onError, w.handler,
|
ctx, retryMgr, w.dialFn, onError, w.handler,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// TODO: cleanup worker? surface error?
|
if pool.Retire != nil &&
|
||||||
|
!errors.Is(err, context.Canceled) &&
|
||||||
|
!errors.Is(err, context.DeadlineExceeded) {
|
||||||
|
pool.Retire(err)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+109
-9
@@ -26,6 +26,7 @@ func makeWorkerContext(t *testing.T) (
|
|||||||
Inbox: inbox,
|
Inbox: inbox,
|
||||||
Events: events,
|
Events: events,
|
||||||
InboxCounter: &atomic.Uint64{},
|
InboxCounter: &atomic.Uint64{},
|
||||||
|
Retire: func(_ error) {},
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -79,12 +80,12 @@ func TestWorkerSession(t *testing.T) {
|
|||||||
}, "expected EventConnected")
|
}, "expected EventConnected")
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO: should dial failure cleanup worker?
|
t.Run("dial failure exhausted - worker exits cleanly, no connected/disconnected events", func(t *testing.T) {
|
||||||
t.Run("dial failure exhausted - session stays alive, no events emitted", func(t *testing.T) {
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
w := makeWorker(t, nil, nil, ctx, cancel)
|
config, _ := NewWorkerConfig(WithRetryDisabled())
|
||||||
|
w := makeWorker(t, nil, config, ctx, cancel)
|
||||||
w.dialFn = func(_ context.Context) (types.Socket, error) {
|
w.dialFn = func(_ context.Context) (types.Socket, error) {
|
||||||
return nil, fmt.Errorf("connection refused")
|
return nil, fmt.Errorf("connection refused")
|
||||||
}
|
}
|
||||||
@@ -93,6 +94,17 @@ func TestWorkerSession(t *testing.T) {
|
|||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
wg.Go(func() { w.Start(pool) })
|
wg.Go(func() { w.Start(pool) })
|
||||||
|
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() { wg.Wait(); close(done) }()
|
||||||
|
honeybeetest.Eventually(t, func() bool {
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}, "expected worker to exit after terminal dial failure")
|
||||||
|
|
||||||
honeybeetest.Never(t, func() bool {
|
honeybeetest.Never(t, func() bool {
|
||||||
select {
|
select {
|
||||||
case e := <-events:
|
case e := <-events:
|
||||||
@@ -101,18 +113,106 @@ func TestWorkerSession(t *testing.T) {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}, "expected no connected/disconnected events when dial fails")
|
}, "expected no connected/disconnected events when dial fails")
|
||||||
|
})
|
||||||
|
|
||||||
// worker goroutine is still running
|
t.Run("Retire called on terminal dial failure", func(t *testing.T) {
|
||||||
assert.False(t, func() bool {
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
done := make(chan struct{})
|
defer cancel()
|
||||||
go func() { wg.Wait(); close(done) }()
|
|
||||||
|
config, _ := NewWorkerConfig(WithRetryDisabled())
|
||||||
|
w := makeWorker(t, nil, config, ctx, cancel)
|
||||||
|
w.dialFn = func(_ context.Context) (types.Socket, error) {
|
||||||
|
return nil, fmt.Errorf("connection refused")
|
||||||
|
}
|
||||||
|
_, events, pool := makeWorkerContext(t)
|
||||||
|
|
||||||
|
retired := atomic.Bool{}
|
||||||
|
var retiredErr atomic.Pointer[error]
|
||||||
|
pool.Retire = func(err error) { retiredErr.Store(&err); retired.Store(true) }
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Go(func() { w.Start(pool) })
|
||||||
|
|
||||||
|
honeybeetest.Eventually(t, func() bool {
|
||||||
|
return retired.Load()
|
||||||
|
}, "expected Retire to be called")
|
||||||
|
|
||||||
|
assert.ErrorContains(t, *retiredErr.Load(), "connection refused", "expected dial error forwarded to Retire")
|
||||||
|
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() { wg.Wait(); close(done) }()
|
||||||
|
honeybeetest.Eventually(t, func() bool {
|
||||||
select {
|
select {
|
||||||
case <-done:
|
case <-done:
|
||||||
return true
|
return true
|
||||||
case <-time.After(50 * time.Millisecond):
|
default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}(), "expected worker to still be running after dial failure")
|
}, "expected worker to exit cleanly")
|
||||||
|
|
||||||
|
honeybeetest.Never(t, func() bool {
|
||||||
|
select {
|
||||||
|
case e := <-events:
|
||||||
|
return e.Kind == EventConnected || e.Kind == EventDisconnected
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}, "expected no connected/disconnected events")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Retire not called when context cancelled during dial", func(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
w := makeWorker(t, nil, nil, ctx, cancel)
|
||||||
|
w.dialFn = func(dialCtx context.Context) (types.Socket, error) {
|
||||||
|
<-dialCtx.Done()
|
||||||
|
return nil, dialCtx.Err()
|
||||||
|
}
|
||||||
|
_, _, pool := makeWorkerContext(t)
|
||||||
|
|
||||||
|
retired := atomic.Bool{}
|
||||||
|
pool.Retire = func(_ error) { retired.Store(true) }
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Go(func() { w.Start(pool) })
|
||||||
|
w.Stop()
|
||||||
|
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() { wg.Wait(); close(done) }()
|
||||||
|
honeybeetest.Eventually(t, func() bool {
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}, "expected worker to exit after Stop")
|
||||||
|
|
||||||
|
assert.False(t, retired.Load(), "expected Retire not called when context cancelled")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Retire not called on successful connection", func(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
mockSocket := honeybeetest.NewMockSocket()
|
||||||
|
w := makeWorker(t, mockSocket, nil, ctx, cancel)
|
||||||
|
_, events, pool := makeWorkerContext(t)
|
||||||
|
|
||||||
|
pool.Retire = func(_ error) { panic("Retire must not be called on successful connection") }
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Go(func() { w.Start(pool) })
|
||||||
|
|
||||||
|
honeybeetest.Eventually(t, func() bool {
|
||||||
|
select {
|
||||||
|
case e := <-events:
|
||||||
|
return e.Kind == EventConnected
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}, "expected EventConnected without Retire being called")
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Stop before connection established - exits cleanly, no events", func(t *testing.T) {
|
t.Run("Stop before connection established - exits cleanly, no events", func(t *testing.T) {
|
||||||
|
|||||||
Reference in New Issue
Block a user