fix(core): prevent infinite loop in drainEvents on closed channel (#152)
drainEvents uses a select loop with a default case to drain buffered events. When the channel is closed, receives always succeed immediately with a zero value, so the default case never triggers and the function spins in an infinite loop, consuming 100% CPU. Check the ok value from the receive and return immediately when the channel is closed. Add regression tests for both closed and open channel scenarios.
This commit is contained in:
1 parent
6455d2a07a
commit
8ec95a280c
2 files changed
+51
-1
No files matched your search
+5
-1
@@ -4423,7 +4423,11 @@ func drainEvents(ch <-chan Event) {
|
||||
drained := 0
|
||||
for {
|
||||
select {
|
||||
case <-ch:
|
||||
case _, ok := <-ch:
|
||||
if !ok {
|
||||
// Channel is closed; stop immediately to avoid an infinite loop.
|
||||
return
|
||||
}
|
||||
drained++
|
||||
default:
|
||||
if drained > 0 {
|
||||
|
||||
@@ -2726,3 +2726,49 @@ func TestCmdBindSetup_UsesSharedLogic(t *testing.T) {
|
||||
t.Error("expected instructions written to file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDrainEventsClosedChannel(t *testing.T) {
|
||||
ch := make(chan Event, 2)
|
||||
ch <- Event{Type: EventToolUse, Content: "a"}
|
||||
ch <- Event{Type: EventToolUse, Content: "b"}
|
||||
close(ch)
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
drainEvents(ch)
|
||||
close(done)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
// ok — returned promptly
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("drainEvents did not return on closed channel (infinite loop)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDrainEventsOpenChannel(t *testing.T) {
|
||||
ch := make(chan Event, 3)
|
||||
ch <- Event{Type: EventToolUse, Content: "a"}
|
||||
ch <- Event{Type: EventToolUse, Content: "b"}
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
drainEvents(ch)
|
||||
close(done)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
// ok
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("drainEvents did not return on open channel with buffered events")
|
||||
}
|
||||
|
||||
// Channel should now be empty.
|
||||
select {
|
||||
case <-ch:
|
||||
t.Fatal("expected channel to be drained")
|
||||
default:
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user