Skip to content

Commit a7bd3e9

Browse files
authored
fix: race problem in timeout mono (#42)
* fix: race problem in timeout mono * add ut * fix ut * set codecov
1 parent 7f7286f commit a7bd3e9

File tree

3 files changed

+121
-26
lines changed

3 files changed

+121
-26
lines changed

codecov.yml

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
coverage:
2+
range: 70..100
3+
round: down
4+
precision: 2

mono/timeout.go

+32-26
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package mono
22

33
import (
44
"context"
5+
"math"
6+
"sync/atomic"
57
"time"
68

79
"github.com/jjeffcaii/reactor-go"
@@ -13,37 +15,56 @@ type monoTimeout struct {
1315
timeout time.Duration
1416
}
1517

18+
func newMonoTimeout(source reactor.RawPublisher, timeout time.Duration) *monoTimeout {
19+
return &monoTimeout{
20+
source: source,
21+
timeout: timeout,
22+
}
23+
}
24+
25+
func (m *monoTimeout) SubscribeWith(ctx context.Context, s reactor.Subscriber) {
26+
m.source.SubscribeWith(ctx, &timeoutSubscriber{
27+
actual: s,
28+
timeout: m.timeout,
29+
done: make(chan struct{}),
30+
})
31+
}
32+
1633
type timeoutSubscriber struct {
1734
actual reactor.Subscriber
1835
timeout time.Duration
1936
done chan struct{}
37+
closed int32
2038
}
2139

2240
func (t *timeoutSubscriber) OnComplete() {
23-
select {
24-
case <-t.done:
25-
default:
41+
if atomic.CompareAndSwapInt32(&t.closed, 0, math.MaxInt32) || atomic.CompareAndSwapInt32(&t.closed, 1, math.MaxInt32) {
2642
close(t.done)
2743
t.actual.OnComplete()
2844
}
2945
}
3046

3147
func (t *timeoutSubscriber) OnError(err error) {
32-
select {
33-
case <-t.done:
34-
hooks.Global().OnErrorDrop(err)
35-
default:
48+
if atomic.CompareAndSwapInt32(&t.closed, 0, -1) {
3649
close(t.done)
3750
t.actual.OnError(err)
51+
return
52+
}
53+
54+
// item is emitted before error reach, should be processed as completed.
55+
if atomic.CompareAndSwapInt32(&t.closed, 1, -1) {
56+
close(t.done)
57+
t.actual.OnComplete()
3858
}
59+
60+
hooks.Global().OnErrorDrop(err)
3961
}
4062

4163
func (t *timeoutSubscriber) OnNext(any reactor.Any) {
42-
select {
43-
case <-t.done:
44-
hooks.Global().OnNextDrop(any)
45-
default:
64+
if atomic.CompareAndSwapInt32(&t.closed, 0, 1) {
4665
t.actual.OnNext(any)
66+
} else {
67+
hooks.Global().OnNextDrop(any)
4768
}
4869
}
4970

@@ -59,18 +80,3 @@ func (t *timeoutSubscriber) OnSubscribe(ctx context.Context, subscription reacto
5980
}()
6081
t.actual.OnSubscribe(ctx, subscription)
6182
}
62-
63-
func (m *monoTimeout) SubscribeWith(ctx context.Context, s reactor.Subscriber) {
64-
m.source.SubscribeWith(ctx, &timeoutSubscriber{
65-
actual: s,
66-
timeout: m.timeout,
67-
done: make(chan struct{}),
68-
})
69-
}
70-
71-
func newMonoTimeout(source reactor.RawPublisher, timeout time.Duration) *monoTimeout {
72-
return &monoTimeout{
73-
source: source,
74-
timeout: timeout,
75-
}
76-
}

mono/timeout_test.go

+85
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package mono
22

33
import (
44
"context"
5+
"io"
56
"testing"
67
"time"
78

@@ -33,3 +34,87 @@ func TestMonoTimeout_SubscribeWith(t *testing.T) {
3334
newMonoTimeout(source, 20*time.Millisecond).SubscribeWith(context.Background(), s)
3435
time.Sleep(30 * time.Millisecond)
3536
}
37+
38+
func TestTimeoutSubscriber(t *testing.T) {
39+
t.Run("ErrorCompleteNext", func(t *testing.T) {
40+
ctrl := gomock.NewController(t)
41+
defer ctrl.Finish()
42+
43+
ms := NewMockSubscription(ctrl)
44+
45+
ms.EXPECT().Request(gomock.Any()).AnyTimes()
46+
ms.EXPECT().Cancel().AnyTimes()
47+
48+
msub := NewMockSubscriber(ctrl)
49+
msub.EXPECT().OnSubscribe(gomock.Any(), gomock.Any()).Do(MockRequestInfinite).Times(1)
50+
msub.EXPECT().OnError(gomock.Any()).Times(1)
51+
msub.EXPECT().OnNext(gomock.Any()).Times(0)
52+
msub.EXPECT().OnComplete().Times(0)
53+
54+
sub := &timeoutSubscriber{
55+
actual: msub,
56+
done: make(chan struct{}),
57+
}
58+
59+
sub.OnSubscribe(context.Background(), ms)
60+
61+
sub.OnError(io.EOF)
62+
sub.OnComplete()
63+
sub.OnNext(1)
64+
})
65+
66+
t.Run("CompleteErrorNext", func(t *testing.T) {
67+
ctrl := gomock.NewController(t)
68+
defer ctrl.Finish()
69+
70+
ms := NewMockSubscription(ctrl)
71+
72+
ms.EXPECT().Request(gomock.Any()).AnyTimes()
73+
ms.EXPECT().Cancel().AnyTimes()
74+
75+
msub := NewMockSubscriber(ctrl)
76+
msub.EXPECT().OnSubscribe(gomock.Any(), gomock.Any()).Do(MockRequestInfinite).Times(1)
77+
msub.EXPECT().OnError(gomock.Any()).Times(0)
78+
msub.EXPECT().OnNext(gomock.Any()).Times(0)
79+
msub.EXPECT().OnComplete().Times(1)
80+
81+
sub := &timeoutSubscriber{
82+
actual: msub,
83+
done: make(chan struct{}),
84+
}
85+
86+
sub.OnSubscribe(context.Background(), ms)
87+
88+
sub.OnComplete()
89+
sub.OnError(io.EOF)
90+
sub.OnNext(1)
91+
})
92+
93+
t.Run("NextErrorComplete", func(t *testing.T) {
94+
ctrl := gomock.NewController(t)
95+
defer ctrl.Finish()
96+
97+
ms := NewMockSubscription(ctrl)
98+
99+
ms.EXPECT().Request(gomock.Any()).AnyTimes()
100+
ms.EXPECT().Cancel().AnyTimes()
101+
102+
msub := NewMockSubscriber(ctrl)
103+
msub.EXPECT().OnSubscribe(gomock.Any(), gomock.Any()).Do(MockRequestInfinite).Times(1)
104+
msub.EXPECT().OnError(gomock.Any()).Times(0)
105+
msub.EXPECT().OnNext(gomock.Any()).Times(1)
106+
msub.EXPECT().OnComplete().Times(1)
107+
108+
sub := &timeoutSubscriber{
109+
actual: msub,
110+
done: make(chan struct{}),
111+
}
112+
113+
sub.OnSubscribe(context.Background(), ms)
114+
115+
sub.OnNext(1)
116+
sub.OnError(io.EOF)
117+
sub.OnComplete()
118+
})
119+
120+
}

0 commit comments

Comments
 (0)