@@ -3,6 +3,7 @@ package spectatorclient
33import (
44 "context"
55 "errors"
6+ "sync"
67 "testing"
78 "time"
89
@@ -16,6 +17,7 @@ import (
1617 "github.com/uber/cadence/common/clock"
1718 "github.com/uber/cadence/common/log"
1819 "github.com/uber/cadence/common/types"
20+ csync "github.com/uber/cadence/service/sharddistributor/client/spectatorclient/sync"
1921)
2022
2123func TestWatchLoopBasicFlow (t * testing.T ) {
@@ -26,12 +28,13 @@ func TestWatchLoopBasicFlow(t *testing.T) {
2628 mockStream := sharddistributor .NewMockWatchNamespaceStateClient (ctrl )
2729
2830 spectator := & spectatorImpl {
29- namespace : "test-ns" ,
30- client : mockClient ,
31- logger : log .NewNoop (),
32- scope : tally .NoopScope ,
33- timeSource : clock .NewRealTimeSource (),
34- firstStateCh : make (chan struct {}),
31+ namespace : "test-ns" ,
32+ client : mockClient ,
33+ logger : log .NewNoop (),
34+ scope : tally .NoopScope ,
35+ timeSource : clock .NewRealTimeSource (),
36+ firstStateSignal : csync .NewResettableSignal (),
37+ enabled : func () bool { return true },
3538 }
3639
3740 // Expect stream creation
@@ -70,7 +73,7 @@ func TestWatchLoopBasicFlow(t *testing.T) {
7073 defer spectator .Stop ()
7174
7275 // Wait for first state
73- <- spectator .firstStateCh
76+ require . NoError ( t , spectator .firstStateSignal . Wait ( context . Background ()))
7477
7578 // Query shard owner
7679 owner , err := spectator .GetShardOwner (context .Background (), "shard-1" )
@@ -91,12 +94,13 @@ func TestGetShardOwner_CacheMiss_FallbackToRPC(t *testing.T) {
9194 mockStream := sharddistributor .NewMockWatchNamespaceStateClient (ctrl )
9295
9396 spectator := & spectatorImpl {
94- namespace : "test-ns" ,
95- client : mockClient ,
96- logger : log .NewNoop (),
97- scope : tally .NoopScope ,
98- timeSource : clock .NewRealTimeSource (),
99- firstStateCh : make (chan struct {}),
97+ namespace : "test-ns" ,
98+ client : mockClient ,
99+ logger : log .NewNoop (),
100+ scope : tally .NoopScope ,
101+ timeSource : clock .NewRealTimeSource (),
102+ firstStateSignal : csync .NewResettableSignal (),
103+ enabled : func () bool { return true },
100104 }
101105
102106 // Setup stream
@@ -142,7 +146,7 @@ func TestGetShardOwner_CacheMiss_FallbackToRPC(t *testing.T) {
142146 spectator .Start (context .Background ())
143147 defer spectator .Stop ()
144148
145- <- spectator .firstStateCh
149+ require . NoError ( t , spectator .firstStateSignal . Wait ( context . Background ()))
146150
147151 // Cache hit
148152 owner , err := spectator .GetShardOwner (context .Background (), "shard-1" )
@@ -166,12 +170,13 @@ func TestStreamReconnection(t *testing.T) {
166170 mockTimeSource := clock .NewMockedTimeSource ()
167171
168172 spectator := & spectatorImpl {
169- namespace : "test-ns" ,
170- client : mockClient ,
171- logger : log .NewNoop (),
172- scope : tally .NoopScope ,
173- timeSource : mockTimeSource ,
174- firstStateCh : make (chan struct {}),
173+ namespace : "test-ns" ,
174+ client : mockClient ,
175+ logger : log .NewNoop (),
176+ scope : tally .NoopScope ,
177+ timeSource : mockTimeSource ,
178+ firstStateSignal : csync .NewResettableSignal (),
179+ enabled : func () bool { return true },
175180 }
176181
177182 // First stream fails immediately
@@ -208,7 +213,7 @@ func TestStreamReconnection(t *testing.T) {
208213 mockTimeSource .BlockUntil (1 ) // Wait for 1 goroutine to be blocked in Sleep
209214 mockTimeSource .Advance (2 * time .Second )
210215
211- <- spectator .firstStateCh
216+ require . NoError ( t , spectator .firstStateSignal . Wait ( context . Background ()))
212217}
213218
214219func TestGetShardOwner_TimeoutBeforeFirstState (t * testing.T ) {
@@ -218,12 +223,13 @@ func TestGetShardOwner_TimeoutBeforeFirstState(t *testing.T) {
218223 mockClient := sharddistributor .NewMockClient (ctrl )
219224
220225 spectator := & spectatorImpl {
221- namespace : "test-ns" ,
222- client : mockClient ,
223- logger : log .NewNoop (),
224- scope : tally .NoopScope ,
225- timeSource : clock .NewRealTimeSource (),
226- firstStateCh : make (chan struct {}),
226+ namespace : "test-ns" ,
227+ client : mockClient ,
228+ logger : log .NewNoop (),
229+ scope : tally .NoopScope ,
230+ timeSource : clock .NewRealTimeSource (),
231+ firstStateSignal : csync .NewResettableSignal (),
232+ enabled : func () bool { return true },
227233 }
228234
229235 // Create a context with a short timeout
@@ -234,5 +240,49 @@ func TestGetShardOwner_TimeoutBeforeFirstState(t *testing.T) {
234240 // Should timeout and return an error
235241 _ , err := spectator .GetShardOwner (ctx , "shard-1" )
236242 assert .Error (t , err )
237- assert .Contains (t , err .Error (), "context cancelled while waiting for first state" )
243+ assert .Contains (t , err .Error (), "wait for first state" )
244+ }
245+
246+ func TestWatchLoopDisabled (t * testing.T ) {
247+ defer goleak .VerifyNone (t )
248+
249+ stateSignal := csync .NewResettableSignal ()
250+ timeSource := clock .NewMockedTimeSource ()
251+
252+ spectator := & spectatorImpl {
253+ firstStateSignal : stateSignal ,
254+ timeSource : timeSource ,
255+ logger : log .NewNoop (),
256+ enabled : func () bool { return false },
257+ }
258+
259+ spectator .Start (context .Background ())
260+ defer spectator .Stop ()
261+
262+ wg := sync.WaitGroup {}
263+ wg .Add (1 )
264+ go func () {
265+ defer wg .Done ()
266+ err := stateSignal .Wait (context .Background ())
267+ assert .Error (t , err )
268+
269+ // Second wait might return ErrReset (if it observes the second reset)
270+ // or nil (if Stop() is called first). Both are acceptable.
271+ _ = stateSignal .Wait (context .Background ())
272+ }()
273+
274+ // First sleep should reset the signal
275+ timeSource .BlockUntil (1 )
276+ timeSource .Advance (1200 * time .Millisecond )
277+
278+ // Second sleep should reset the signal
279+ timeSource .BlockUntil (1 )
280+
281+ // Ensure the loop is exited
282+ timeSource .Advance (1200 * time .Millisecond )
283+
284+ // Stop the spectator to unblock any waiting goroutines
285+ spectator .Stop ()
286+
287+ wg .Wait ()
238288}
0 commit comments