Skip to content

Commit 1466283

Browse files
authored
internal/idle: add a test that invokes ClientConn methods concurrently (#6659)
1 parent fd9ef72 commit 1466283

File tree

3 files changed

+115
-25
lines changed

3 files changed

+115
-25
lines changed

clientconn.go

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -337,8 +337,8 @@ func (cc *ClientConn) exitIdleMode() error {
337337
return errConnClosing
338338
}
339339
if cc.idlenessState != ccIdlenessStateIdle {
340-
cc.mu.Unlock()
341340
channelz.Infof(logger, cc.channelzID, "ClientConn asked to exit idle mode, current mode is %v", cc.idlenessState)
341+
cc.mu.Unlock()
342342
return nil
343343
}
344344

@@ -404,13 +404,13 @@ func (cc *ClientConn) exitIdleMode() error {
404404
// name resolver, load balancer and any subchannels.
405405
func (cc *ClientConn) enterIdleMode() error {
406406
cc.mu.Lock()
407+
defer cc.mu.Unlock()
408+
407409
if cc.conns == nil {
408-
cc.mu.Unlock()
409410
return ErrClientConnClosing
410411
}
411412
if cc.idlenessState != ccIdlenessStateActive {
412-
channelz.Errorf(logger, cc.channelzID, "ClientConn asked to enter idle mode, current mode is %v", cc.idlenessState)
413-
cc.mu.Unlock()
413+
channelz.Warningf(logger, cc.channelzID, "ClientConn asked to enter idle mode, current mode is %v", cc.idlenessState)
414414
return nil
415415
}
416416

@@ -431,14 +431,14 @@ func (cc *ClientConn) enterIdleMode() error {
431431
cc.balancerWrapper.enterIdleMode()
432432
cc.csMgr.updateState(connectivity.Idle)
433433
cc.idlenessState = ccIdlenessStateIdle
434-
cc.mu.Unlock()
434+
cc.addTraceEvent("entering idle mode")
435435

436436
go func() {
437-
cc.addTraceEvent("entering idle mode")
438437
for ac := range conns {
439438
ac.tearDown(errConnIdling)
440439
}
441440
}()
441+
442442
return nil
443443
}
444444

@@ -804,6 +804,12 @@ func init() {
804804
internal.SubscribeToConnectivityStateChanges = func(cc *ClientConn, s grpcsync.Subscriber) func() {
805805
return cc.csMgr.pubSub.Subscribe(s)
806806
}
807+
internal.EnterIdleModeForTesting = func(cc *ClientConn) error {
808+
return cc.enterIdleMode()
809+
}
810+
internal.ExitIdleModeForTesting = func(cc *ClientConn) error {
811+
return cc.exitIdleMode()
812+
}
807813
}
808814

809815
func (cc *ClientConn) maybeApplyDefaultServiceConfig(addrs []resolver.Address) {

internal/idle/idle_e2e_test.go

Lines changed: 97 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"fmt"
2424
"io"
2525
"strings"
26+
"sync"
2627
"testing"
2728
"time"
2829

@@ -32,6 +33,7 @@ import (
3233
"google.golang.org/grpc/codes"
3334
"google.golang.org/grpc/connectivity"
3435
"google.golang.org/grpc/credentials/insecure"
36+
"google.golang.org/grpc/internal"
3537
"google.golang.org/grpc/internal/balancer/stub"
3638
"google.golang.org/grpc/internal/channelz"
3739
"google.golang.org/grpc/internal/grpctest"
@@ -132,11 +134,11 @@ func (s) TestChannelIdleness_Disabled_NoActivity(t *testing.T) {
132134
if err != nil {
133135
t.Fatalf("grpc.Dial() failed: %v", err)
134136
}
135-
t.Cleanup(func() { cc.Close() })
137+
defer cc.Close()
136138

137139
// Start a test backend and push an address update via the resolver.
138140
backend := stubserver.StartTestService(t, nil)
139-
t.Cleanup(backend.Stop)
141+
defer backend.Stop()
140142
r.UpdateState(resolver.State{Addresses: []resolver.Address{{Addr: backend.Address}}})
141143

142144
// Verify that the ClientConn moves to READY.
@@ -178,12 +180,12 @@ func (s) TestChannelIdleness_Enabled_NoActivity(t *testing.T) {
178180
if err != nil {
179181
t.Fatalf("grpc.Dial() failed: %v", err)
180182
}
181-
t.Cleanup(func() { cc.Close() })
183+
defer cc.Close()
182184

183185
// Start a test backend and push an address update via the resolver.
184186
lis := testutils.NewListenerWrapper(t, nil)
185187
backend := stubserver.StartTestService(t, &stubserver.StubServer{Listener: lis})
186-
t.Cleanup(backend.Stop)
188+
defer backend.Stop()
187189
r.UpdateState(resolver.State{Addresses: []resolver.Address{{Addr: backend.Address}}})
188190

189191
// Verify that the ClientConn moves to READY.
@@ -266,11 +268,10 @@ func (s) TestChannelIdleness_Enabled_OngoingCall(t *testing.T) {
266268
if err != nil {
267269
t.Fatalf("grpc.Dial() failed: %v", err)
268270
}
269-
t.Cleanup(func() { cc.Close() })
271+
defer cc.Close()
270272

271-
// Start a test backend which keeps a unary RPC call active by blocking on a
272-
// channel that is closed by the test later on. Also push an address update
273-
// via the resolver.
273+
// Start a test backend that keeps the RPC call active by blocking
274+
// on a channel that is closed by the test later on.
274275
blockCh := make(chan struct{})
275276
backend := &stubserver.StubServer{
276277
EmptyCallF: func(ctx context.Context, in *testpb.Empty) (*testpb.Empty, error) {
@@ -285,16 +286,19 @@ func (s) TestChannelIdleness_Enabled_OngoingCall(t *testing.T) {
285286
if err := backend.StartServer(); err != nil {
286287
t.Fatalf("Failed to start backend: %v", err)
287288
}
288-
t.Cleanup(backend.Stop)
289+
defer backend.Stop()
290+
291+
// Push an address update containing the address of the above
292+
// backend via the manual resolver.
289293
r.UpdateState(resolver.State{Addresses: []resolver.Address{{Addr: backend.Address}}})
290294

291295
// Verify that the ClientConn moves to READY.
292296
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
293297
defer cancel()
294298
testutils.AwaitState(ctx, t, cc, connectivity.Ready)
295299

296-
// Spawn a goroutine which checks expected state transitions and idleness
297-
// channelz trace events.
300+
// Spawn a goroutine to check for expected behavior while a blocking
301+
// RPC all is made from the main test goroutine.
298302
errCh := make(chan error, 1)
299303
go func() {
300304
defer close(blockCh)
@@ -353,11 +357,11 @@ func (s) TestChannelIdleness_Enabled_ActiveSinceLastCheck(t *testing.T) {
353357
if err != nil {
354358
t.Fatalf("grpc.Dial() failed: %v", err)
355359
}
356-
t.Cleanup(func() { cc.Close() })
360+
defer cc.Close()
357361

358362
// Start a test backend and push an address update via the resolver.
359363
backend := stubserver.StartTestService(t, nil)
360-
t.Cleanup(backend.Stop)
364+
defer backend.Stop()
361365
r.UpdateState(resolver.State{Addresses: []resolver.Address{{Addr: backend.Address}}})
362366

363367
// Verify that the ClientConn moves to READY.
@@ -408,7 +412,7 @@ func (s) TestChannelIdleness_Enabled_ExitIdleOnRPC(t *testing.T) {
408412
// restarted when exiting idle, it will push the same address to grpc again.
409413
r := manual.NewBuilderWithScheme("whatever")
410414
backend := stubserver.StartTestService(t, nil)
411-
t.Cleanup(backend.Stop)
415+
defer backend.Stop()
412416
r.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: backend.Address}}})
413417

414418
// Create a ClientConn with a short idle_timeout.
@@ -422,7 +426,7 @@ func (s) TestChannelIdleness_Enabled_ExitIdleOnRPC(t *testing.T) {
422426
if err != nil {
423427
t.Fatalf("grpc.Dial() failed: %v", err)
424428
}
425-
t.Cleanup(func() { cc.Close() })
429+
defer cc.Close()
426430

427431
// Verify that the ClientConn moves to READY.
428432
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
@@ -473,7 +477,7 @@ func (s) TestChannelIdleness_Enabled_IdleTimeoutRacesWithRPCs(t *testing.T) {
473477
// restarted when exiting idle, it will push the same address to grpc again.
474478
r := manual.NewBuilderWithScheme("whatever")
475479
backend := stubserver.StartTestService(t, nil)
476-
t.Cleanup(backend.Stop)
480+
defer backend.Stop()
477481
r.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: backend.Address}}})
478482

479483
// Create a ClientConn with a short idle_timeout.
@@ -487,7 +491,7 @@ func (s) TestChannelIdleness_Enabled_IdleTimeoutRacesWithRPCs(t *testing.T) {
487491
if err != nil {
488492
t.Fatalf("grpc.Dial() failed: %v", err)
489493
}
490-
t.Cleanup(func() { cc.Close() })
494+
defer cc.Close()
491495

492496
// Verify that the ClientConn moves to READY.
493497
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
@@ -516,7 +520,7 @@ func (s) TestChannelIdleness_Connect(t *testing.T) {
516520
// restarted when exiting idle, it will push the same address to grpc again.
517521
r := manual.NewBuilderWithScheme("whatever")
518522
backend := stubserver.StartTestService(t, nil)
519-
t.Cleanup(backend.Stop)
523+
defer backend.Stop()
520524
r.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: backend.Address}}})
521525

522526
// Create a ClientConn with a short idle_timeout.
@@ -530,7 +534,7 @@ func (s) TestChannelIdleness_Connect(t *testing.T) {
530534
if err != nil {
531535
t.Fatalf("grpc.Dial() failed: %v", err)
532536
}
533-
t.Cleanup(func() { cc.Close() })
537+
defer cc.Close()
534538

535539
// Verify that the ClientConn moves to IDLE.
536540
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
@@ -544,3 +548,77 @@ func (s) TestChannelIdleness_Connect(t *testing.T) {
544548
// Verify that the ClientConn moves back to READY.
545549
testutils.AwaitState(ctx, t, cc, connectivity.Ready)
546550
}
551+
552+
// runFunc runs f repeatedly until the context expires.
553+
func runFunc(ctx context.Context, f func()) {
554+
for {
555+
select {
556+
case <-ctx.Done():
557+
return
558+
case <-time.After(10 * time.Millisecond):
559+
f()
560+
}
561+
}
562+
}
563+
564+
// Tests the scenario where there are concurrent calls to exit and enter idle
565+
// mode on the ClientConn. Verifies that there is no race under this scenario.
566+
func (s) TestChannelIdleness_RaceBetweenEnterAndExitIdleMode(t *testing.T) {
567+
// Start a test backend and set the bootstrap state of the resolver to
568+
// include this address. This will ensure that when the resolver is
569+
// restarted when exiting idle, it will push the same address to grpc again.
570+
r := manual.NewBuilderWithScheme("whatever")
571+
backend := stubserver.StartTestService(t, nil)
572+
defer backend.Stop()
573+
r.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: backend.Address}}})
574+
575+
// Create a ClientConn with a long idle_timeout. We will explicitly trigger
576+
// entering and exiting IDLE mode from the test.
577+
dopts := []grpc.DialOption{
578+
grpc.WithTransportCredentials(insecure.NewCredentials()),
579+
grpc.WithResolvers(r),
580+
grpc.WithIdleTimeout(30 * time.Minute),
581+
grpc.WithDefaultServiceConfig(`{"loadBalancingConfig": [{"pick_first":{}}]}`),
582+
}
583+
cc, err := grpc.Dial(r.Scheme()+":///test.server", dopts...)
584+
if err != nil {
585+
t.Fatalf("grpc.Dial() failed: %v", err)
586+
}
587+
defer cc.Close()
588+
589+
enterIdle := internal.EnterIdleModeForTesting.(func(*grpc.ClientConn) error)
590+
enterIdleFunc := func() {
591+
if err := enterIdle(cc); err != nil {
592+
t.Errorf("Failed to enter idle mode: %v", err)
593+
}
594+
}
595+
exitIdle := internal.ExitIdleModeForTesting.(func(*grpc.ClientConn) error)
596+
exitIdleFunc := func() {
597+
if err := exitIdle(cc); err != nil {
598+
t.Errorf("Failed to exit idle mode: %v", err)
599+
}
600+
}
601+
// Spawn goroutines that call methods on the ClientConn to enter and exit
602+
// idle mode concurrently for one second.
603+
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
604+
defer cancel()
605+
var wg sync.WaitGroup
606+
wg.Add(4)
607+
go func() {
608+
runFunc(ctx, enterIdleFunc)
609+
wg.Done()
610+
}()
611+
go func() {
612+
runFunc(ctx, enterIdleFunc)
613+
wg.Done()
614+
}()
615+
go func() {
616+
runFunc(ctx, exitIdleFunc)
617+
wg.Done()
618+
}()
619+
go func() {
620+
runFunc(ctx, exitIdleFunc)
621+
wg.Done()
622+
}()
623+
wg.Wait()
624+
}

internal/internal.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,12 @@ var (
175175
// GRPCResolverSchemeExtraMetadata determines when gRPC will add extra
176176
// metadata to RPCs.
177177
GRPCResolverSchemeExtraMetadata string = "xds"
178+
179+
// EnterIdleModeForTesting gets the ClientConn to enter IDLE mode.
180+
EnterIdleModeForTesting any // func(*grpc.ClientConn) error
181+
182+
// ExitIdleModeForTesting gets the ClientConn to exit IDLE mode.
183+
ExitIdleModeForTesting any // func(*grpc.ClientConn) error
178184
)
179185

180186
// HealthChecker defines the signature of the client-side LB channel health checking function.

0 commit comments

Comments
 (0)