Skip to content

Commit 56e30d6

Browse files
authored
fix: waiter#RunAndWait maybe cause deadlock. (#555)
for errChan cap is 1 and receive both event timeout err and callback err( block on the err for errChan handle receiving(wait#waitFunc) after callback send err(waiter#RunAndWait) ).
1 parent a70e216 commit 56e30d6

File tree

2 files changed

+90
-1
lines changed

2 files changed

+90
-1
lines changed

waiter.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,9 @@ func (w *waiter) reject(err error) {
175175

176176
func newWaiter() *waiter {
177177
w := &waiter{
178-
errChan: make(chan error, 1),
178+
// receive both event timeout err and callback err
179+
// but just return event timeout err
180+
errChan: make(chan error, 2),
179181
}
180182
return w
181183
}

waiter_test.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package playwright
22

33
import (
4+
"errors"
45
"fmt"
6+
"sync/atomic"
57
"testing"
68
"time"
79

@@ -136,3 +138,88 @@ func TestWaiterReturnErrorWhenMisuse(t *testing.T) {
136138
_, err = waiter.Wait()
137139
require.ErrorContains(t, err, "call RejectOnEvent before WaitForEvent")
138140
}
141+
142+
func TestWaiterDeadlockForErrChanCapIs1AndCallbackErr(t *testing.T) {
143+
// deadlock happen on waiter timeout before callback return err
144+
waiterTimeout := 200.0
145+
callbackTimeout := time.Duration(waiterTimeout+200.0) * time.Millisecond
146+
147+
mockCallbackErr := errors.New("mock callback error")
148+
149+
emitter := &eventEmitter{}
150+
w := &waiter{
151+
// just receive event timeout err or callback err
152+
errChan: make(chan error, 1),
153+
}
154+
155+
callbackOverCh := make(chan struct{})
156+
callbackErrCh := make(chan error)
157+
isAfterWaiterRunAndWaitExecuted := atomic.Bool{}
158+
go func() {
159+
_, err := w.WithTimeout(waiterTimeout).WaitForEvent(emitter, "", nil).RunAndWait(func() error {
160+
time.Sleep(callbackTimeout)
161+
close(callbackOverCh)
162+
// block for this err, for waiter.errChan has cache event timeout err
163+
return mockCallbackErr
164+
})
165+
166+
isAfterWaiterRunAndWaitExecuted.Store(true)
167+
callbackErrCh <- err
168+
}()
169+
170+
// ensure waiter timeout
171+
<-callbackOverCh
172+
// give some time but never enough
173+
time.Sleep(200 * time.Millisecond)
174+
175+
// Originally it was executed, but because waiter.errChan is currently caching the waiter timeout error,
176+
// the callback error is blocked (because waitFunc has not been executed yet,
177+
// waiter.errChan has not started receiving).
178+
require.False(t, isAfterWaiterRunAndWaitExecuted.Load())
179+
180+
// if not receive waiter timeout error, isAfterWaiterRunAndWaitExecuted should be always false
181+
err1 := <-w.errChan
182+
require.ErrorIs(t, err1, ErrTimeout)
183+
184+
// for w.errChan cache is empty, callback error is sent and received, and then return it
185+
err2 := <-callbackErrCh
186+
require.ErrorIs(t, err2, mockCallbackErr)
187+
require.True(t, isAfterWaiterRunAndWaitExecuted.Load())
188+
}
189+
190+
func TestWaiterHasNotDeadlockForErrChanCapBiggerThan1AndCallbackErr(t *testing.T) {
191+
// deadlock happen on waiter timeout before callback return err
192+
waiterTimeout := 100.0
193+
callbackTimeout := time.Duration(waiterTimeout+100.0) * time.Millisecond
194+
195+
mockCallbackErr := errors.New("mock callback error")
196+
197+
emitter := &eventEmitter{}
198+
w := newWaiter()
199+
200+
callbackOverCh := make(chan struct{})
201+
callbackErrCh := make(chan error)
202+
isAfterWaiterRunAndWaitExecuted := atomic.Bool{}
203+
go func() {
204+
_, err := w.WithTimeout(waiterTimeout).WaitForEvent(emitter, "", nil).RunAndWait(func() error {
205+
time.Sleep(callbackTimeout)
206+
close(callbackOverCh)
207+
return mockCallbackErr
208+
})
209+
isAfterWaiterRunAndWaitExecuted.Store(true)
210+
callbackErrCh <- err
211+
}()
212+
213+
// ensure waiter timeout
214+
<-callbackOverCh
215+
216+
// for waiter.errChan cap is 2(greater than 1), so it will not block(deadlock)
217+
require.Eventually(t,
218+
func() bool { return isAfterWaiterRunAndWaitExecuted.Load() }, 100*time.Millisecond, 10*time.Microsecond)
219+
220+
// the first err still is waiter timeout, and is returned
221+
err1 := <-w.errChan
222+
require.ErrorIs(t, err1, mockCallbackErr)
223+
err2 := <-callbackErrCh
224+
require.ErrorIs(t, err2, ErrTimeout)
225+
}

0 commit comments

Comments
 (0)