snapshot of https://github.com/grpc/grpc-go.git (master, 2026-09-15; blobs > 131072B omitted)
Some checks failed
codecov / upload (push) Failing after 13m59s
CodeQL / Analyze (push) Failing after 1s
Testing / vet-proto (push) Failing after 1s
Testing / tests (arm, latest) (push) Waiting to run
Testing / extras (latest) (push) Failing after 1s
Testing / static checks (latest-1) (push) Failing after 1s
Testing / tests (-race, latest) (push) Failing after 1s
Testing / tests (i386, latest) (push) Failing after 1s
Testing / tests (latest) (push) Failing after 1s
Testing / tests (latest-1) (push) Failing after 1s
Lock Threads / lock (push) Failing after 1s
govulncheck / govulncheck (.) (push) Failing after 1s
govulncheck / govulncheck (./cmd/protoc-gen-go-grpc) (push) Failing after 1s
govulncheck / govulncheck (./gcp/observability) (push) Failing after 1s
govulncheck / govulncheck (./security/advancedtls) (push) Failing after 1s
govulncheck / govulncheck (./stats/opencensus) (push) Failing after 1s
Stale bot / stale (push) Failing after 1s
Some checks failed
codecov / upload (push) Failing after 13m59s
CodeQL / Analyze (push) Failing after 1s
Testing / vet-proto (push) Failing after 1s
Testing / tests (arm, latest) (push) Waiting to run
Testing / extras (latest) (push) Failing after 1s
Testing / static checks (latest-1) (push) Failing after 1s
Testing / tests (-race, latest) (push) Failing after 1s
Testing / tests (i386, latest) (push) Failing after 1s
Testing / tests (latest) (push) Failing after 1s
Testing / tests (latest-1) (push) Failing after 1s
Lock Threads / lock (push) Failing after 1s
govulncheck / govulncheck (.) (push) Failing after 1s
govulncheck / govulncheck (./cmd/protoc-gen-go-grpc) (push) Failing after 1s
govulncheck / govulncheck (./gcp/observability) (push) Failing after 1s
govulncheck / govulncheck (./security/advancedtls) (push) Failing after 1s
govulncheck / govulncheck (./stats/opencensus) (push) Failing after 1s
Stale bot / stale (push) Failing after 1s
This commit is contained in:
832
test/clientconn_state_transition_test.go
Normal file
832
test/clientconn_state_transition_test.go
Normal file
@@ -0,0 +1,832 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2018 gRPC authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/http2"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/backoff"
|
||||
"google.golang.org/grpc/balancer"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/connectivity"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/internal"
|
||||
"google.golang.org/grpc/internal/balancer/stub"
|
||||
"google.golang.org/grpc/internal/grpcsync"
|
||||
"google.golang.org/grpc/internal/stubserver"
|
||||
"google.golang.org/grpc/internal/testutils"
|
||||
"google.golang.org/grpc/resolver"
|
||||
"google.golang.org/grpc/resolver/manual"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
testgrpc "google.golang.org/grpc/interop/grpc_testing"
|
||||
testpb "google.golang.org/grpc/interop/grpc_testing"
|
||||
)
|
||||
|
||||
// Keep reading until something causes the connection to die (EOF, server
|
||||
// closed, etc). Useful as a tool for mindlessly keeping the connection
|
||||
// healthy, since the client will error if things like client prefaces are not
|
||||
// accepted in a timely fashion.
|
||||
func keepReading(conn net.Conn) {
|
||||
io.Copy(io.Discard, conn)
|
||||
}
|
||||
|
||||
type funcConnectivityStateSubscriber struct {
|
||||
onMsg func(connectivity.State)
|
||||
}
|
||||
|
||||
func (f *funcConnectivityStateSubscriber) OnMessage(msg any) {
|
||||
f.onMsg(msg.(connectivity.State))
|
||||
}
|
||||
|
||||
func waitForState(ctx context.Context, t *testing.T, stateCh <-chan connectivity.State, want connectivity.State) {
|
||||
t.Helper()
|
||||
select {
|
||||
case gotState := <-stateCh:
|
||||
if gotState != want {
|
||||
t.Fatalf("State is %s; want %s", gotState, want)
|
||||
}
|
||||
t.Logf("State is %s as expected", gotState)
|
||||
case <-ctx.Done():
|
||||
t.Fatalf("Timed out waiting for state update: %s", want)
|
||||
}
|
||||
}
|
||||
|
||||
// Tests for state transitions in various scenarios with a single address.
|
||||
func (s) TestStateTransitions_SingleAddress(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
desc string
|
||||
wantStates []connectivity.State
|
||||
server func(net.Listener) net.Conn
|
||||
}{
|
||||
{
|
||||
desc: "ServerSendsPreface",
|
||||
wantStates: []connectivity.State{
|
||||
connectivity.Connecting,
|
||||
connectivity.Ready,
|
||||
},
|
||||
server: func(lis net.Listener) net.Conn {
|
||||
conn, err := lis.Accept()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return nil
|
||||
}
|
||||
|
||||
go keepReading(conn)
|
||||
|
||||
framer := http2.NewFramer(conn, conn)
|
||||
if err := framer.WriteSettings(http2.Setting{}); err != nil {
|
||||
t.Errorf("Error while writing settings frame. %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
return conn
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "ConnectionClosesBeforeServerPreface",
|
||||
wantStates: []connectivity.State{
|
||||
connectivity.Connecting,
|
||||
connectivity.TransientFailure,
|
||||
},
|
||||
server: func(lis net.Listener) net.Conn {
|
||||
conn, err := lis.Accept()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return nil
|
||||
}
|
||||
|
||||
conn.Close()
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "ConnectionClosesBeforeClientPreface",
|
||||
wantStates: []connectivity.State{
|
||||
connectivity.Connecting,
|
||||
connectivity.TransientFailure,
|
||||
},
|
||||
server: func(lis net.Listener) net.Conn {
|
||||
conn, err := lis.Accept()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return nil
|
||||
}
|
||||
|
||||
framer := http2.NewFramer(conn, conn)
|
||||
if err := framer.WriteSettings(http2.Setting{}); err != nil {
|
||||
t.Errorf("Error while writing settings frame. %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
conn.Close()
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "ServerNeverSendsPreface",
|
||||
wantStates: []connectivity.State{
|
||||
connectivity.Connecting,
|
||||
connectivity.TransientFailure,
|
||||
},
|
||||
server: func(lis net.Listener) net.Conn {
|
||||
conn, err := lis.Accept()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return nil
|
||||
}
|
||||
|
||||
go keepReading(conn)
|
||||
|
||||
return conn
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
testStateTransitionSingleAddress(t, test.wantStates, test.server)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testStateTransitionSingleAddress(t *testing.T, wantStates []connectivity.State, server func(net.Listener) net.Conn) {
|
||||
pl := testutils.NewPipeListener()
|
||||
defer pl.Close()
|
||||
|
||||
// Launch the server.
|
||||
var conn net.Conn
|
||||
var connMu sync.Mutex
|
||||
go func() {
|
||||
connMu.Lock()
|
||||
conn = server(pl)
|
||||
connMu.Unlock()
|
||||
}()
|
||||
|
||||
dopts := []grpc.DialOption{
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
grpc.WithDialer(pl.Dialer()),
|
||||
grpc.WithConnectParams(grpc.ConnectParams{
|
||||
Backoff: backoff.Config{},
|
||||
MinConnectTimeout: 100 * time.Millisecond,
|
||||
}),
|
||||
}
|
||||
cc, err := grpc.NewClient("passthrough:///", dopts...)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer cc.Close()
|
||||
|
||||
// Ensure that the client is in IDLE before connecting.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
testutils.AwaitState(ctx, t, cc, connectivity.Idle)
|
||||
|
||||
// Subscribe to state updates.
|
||||
stateCh := make(chan connectivity.State, 1)
|
||||
s := &funcConnectivityStateSubscriber{
|
||||
onMsg: func(s connectivity.State) {
|
||||
select {
|
||||
case stateCh <- s:
|
||||
case <-ctx.Done():
|
||||
}
|
||||
},
|
||||
}
|
||||
internal.SubscribeToConnectivityStateChanges.(func(cc *grpc.ClientConn, s grpcsync.Subscriber) func())(cc, s)
|
||||
|
||||
cc.Connect()
|
||||
for _, wantState := range wantStates {
|
||||
waitForState(ctx, t, stateCh, wantState)
|
||||
}
|
||||
|
||||
connMu.Lock()
|
||||
defer connMu.Unlock()
|
||||
if conn != nil {
|
||||
err = conn.Close()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tests for state transitions when the READY connection is closed.
|
||||
func (s) TestStateTransitions_ReadyToConnecting(t *testing.T) {
|
||||
lis, err := net.Listen("tcp", "localhost:0")
|
||||
if err != nil {
|
||||
t.Fatalf("Error while listening. Err: %v", err)
|
||||
}
|
||||
defer lis.Close()
|
||||
|
||||
sawReady := make(chan struct{}, 1)
|
||||
defer close(sawReady)
|
||||
|
||||
// Launch the server.
|
||||
go func() {
|
||||
conn, err := lis.Accept()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
go keepReading(conn)
|
||||
|
||||
framer := http2.NewFramer(conn, conn)
|
||||
if err := framer.WriteSettings(http2.Setting{}); err != nil {
|
||||
t.Errorf("Error while writing settings frame. %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Prevents race between onPrefaceReceipt and onClose.
|
||||
<-sawReady
|
||||
|
||||
conn.Close()
|
||||
}()
|
||||
|
||||
cc, err := grpc.NewClient(lis.Addr().String(), grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer cc.Close()
|
||||
|
||||
// Ensure that the client is in IDLE before connecting.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
testutils.AwaitState(ctx, t, cc, connectivity.Idle)
|
||||
|
||||
// Subscribe to state updates.
|
||||
stateCh := make(chan connectivity.State, 1)
|
||||
s := &funcConnectivityStateSubscriber{
|
||||
onMsg: func(s connectivity.State) {
|
||||
select {
|
||||
case stateCh <- s:
|
||||
case <-ctx.Done():
|
||||
}
|
||||
},
|
||||
}
|
||||
internal.SubscribeToConnectivityStateChanges.(func(cc *grpc.ClientConn, s grpcsync.Subscriber) func())(cc, s)
|
||||
|
||||
cc.Connect()
|
||||
wantStates := []connectivity.State{
|
||||
connectivity.Connecting,
|
||||
connectivity.Ready,
|
||||
connectivity.Idle,
|
||||
connectivity.Connecting,
|
||||
}
|
||||
for _, wantState := range wantStates {
|
||||
waitForState(ctx, t, stateCh, wantState)
|
||||
if wantState == connectivity.Ready {
|
||||
sawReady <- struct{}{}
|
||||
}
|
||||
if wantState == connectivity.Idle {
|
||||
cc.Connect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tests for state transitions when there are multiple addresses and all the
|
||||
// addresses fail.
|
||||
func (s) TestStateTransitions_TriesAllAddrsBeforeTransientFailure(t *testing.T) {
|
||||
lis1, err := net.Listen("tcp", "localhost:0")
|
||||
if err != nil {
|
||||
t.Fatalf("Error while listening. Err: %v", err)
|
||||
}
|
||||
defer lis1.Close()
|
||||
|
||||
lis2, err := net.Listen("tcp", "localhost:0")
|
||||
if err != nil {
|
||||
t.Fatalf("Error while listening. Err: %v", err)
|
||||
}
|
||||
defer lis2.Close()
|
||||
|
||||
server1Done := make(chan struct{})
|
||||
server2Done := make(chan struct{})
|
||||
|
||||
// Launch server 1.
|
||||
go func() {
|
||||
conn, err := lis1.Accept()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
conn.Close()
|
||||
close(server1Done)
|
||||
}()
|
||||
// Launch server 2.
|
||||
go func() {
|
||||
conn, err := lis2.Accept()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
conn.Close()
|
||||
close(server2Done)
|
||||
}()
|
||||
|
||||
rb := manual.NewBuilderWithScheme("whatever")
|
||||
rb.InitialState(resolver.State{Addresses: []resolver.Address{
|
||||
{Addr: lis1.Addr().String()},
|
||||
{Addr: lis2.Addr().String()},
|
||||
}})
|
||||
|
||||
dopts := []grpc.DialOption{
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
grpc.WithResolvers(rb),
|
||||
grpc.WithConnectParams(grpc.ConnectParams{
|
||||
// Set a really long back-off delay to ensure the subchannels stay
|
||||
// in TRANSIENT_FAILURE and not enter IDLE.
|
||||
Backoff: backoff.Config{BaseDelay: 1 * time.Hour},
|
||||
}),
|
||||
}
|
||||
cc, err := grpc.NewClient("whatever:///this-gets-overwritten", dopts...)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer cc.Close()
|
||||
|
||||
// Ensure that the client is in IDLE before connecting.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
testutils.AwaitState(ctx, t, cc, connectivity.Idle)
|
||||
|
||||
// Subscribe to state updates.
|
||||
stateCh := make(chan connectivity.State, 1)
|
||||
s := &funcConnectivityStateSubscriber{
|
||||
onMsg: func(s connectivity.State) {
|
||||
select {
|
||||
case stateCh <- s:
|
||||
case <-ctx.Done():
|
||||
}
|
||||
},
|
||||
}
|
||||
internal.SubscribeToConnectivityStateChanges.(func(cc *grpc.ClientConn, s grpcsync.Subscriber) func())(cc, s)
|
||||
|
||||
cc.Connect()
|
||||
wantStates := []connectivity.State{
|
||||
connectivity.Connecting,
|
||||
connectivity.TransientFailure,
|
||||
}
|
||||
for _, wantState := range wantStates {
|
||||
waitForState(ctx, t, stateCh, wantState)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatal("saw the correct state transitions, but timed out waiting for client to finish interactions with server 1")
|
||||
case <-server1Done:
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatal("saw the correct state transitions, but timed out waiting for client to finish interactions with server 2")
|
||||
case <-server2Done:
|
||||
}
|
||||
}
|
||||
|
||||
// Tests for state transitions with multiple addresses when the READY connection
|
||||
// is closed.
|
||||
func (s) TestStateTransitions_MultipleAddrsEntersReady(t *testing.T) {
|
||||
lis1, err := net.Listen("tcp", "localhost:0")
|
||||
if err != nil {
|
||||
t.Fatalf("Error while listening. Err: %v", err)
|
||||
}
|
||||
defer lis1.Close()
|
||||
|
||||
// Never actually gets used; we just want it to be alive so that the
|
||||
// resolver has two addresses to target.
|
||||
lis2, err := net.Listen("tcp", "localhost:0")
|
||||
if err != nil {
|
||||
t.Fatalf("Error while listening. Err: %v", err)
|
||||
}
|
||||
defer lis2.Close()
|
||||
|
||||
server1Done := make(chan struct{})
|
||||
sawReady := make(chan struct{}, 1)
|
||||
defer close(sawReady)
|
||||
|
||||
// Launch server 1.
|
||||
go func() {
|
||||
conn, err := lis1.Accept()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
go keepReading(conn)
|
||||
|
||||
framer := http2.NewFramer(conn, conn)
|
||||
if err := framer.WriteSettings(http2.Setting{}); err != nil {
|
||||
t.Errorf("Error while writing settings frame. %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
<-sawReady
|
||||
|
||||
conn.Close()
|
||||
|
||||
close(server1Done)
|
||||
}()
|
||||
|
||||
rb := manual.NewBuilderWithScheme("whatever")
|
||||
rb.InitialState(resolver.State{Addresses: []resolver.Address{
|
||||
{Addr: lis1.Addr().String()},
|
||||
{Addr: lis2.Addr().String()},
|
||||
}})
|
||||
cc, err := grpc.NewClient("whatever:///this-gets-overwritten", grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithResolvers(rb))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer cc.Close()
|
||||
|
||||
// Ensure that the client is in IDLE before connecting.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
testutils.AwaitState(ctx, t, cc, connectivity.Idle)
|
||||
|
||||
// Subscribe to state updates.
|
||||
stateCh := make(chan connectivity.State, 1)
|
||||
s := &funcConnectivityStateSubscriber{
|
||||
onMsg: func(s connectivity.State) {
|
||||
select {
|
||||
case stateCh <- s:
|
||||
case <-ctx.Done():
|
||||
}
|
||||
},
|
||||
}
|
||||
internal.SubscribeToConnectivityStateChanges.(func(cc *grpc.ClientConn, s grpcsync.Subscriber) func())(cc, s)
|
||||
|
||||
cc.Connect()
|
||||
wantStates := []connectivity.State{
|
||||
connectivity.Connecting,
|
||||
connectivity.Ready,
|
||||
connectivity.Idle,
|
||||
connectivity.Connecting,
|
||||
}
|
||||
for _, wantState := range wantStates {
|
||||
waitForState(ctx, t, stateCh, wantState)
|
||||
if wantState == connectivity.Ready {
|
||||
sawReady <- struct{}{}
|
||||
}
|
||||
if wantState == connectivity.Idle {
|
||||
cc.Connect()
|
||||
}
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatal("saw the correct state transitions, but timed out waiting for client to finish interactions with server 1")
|
||||
case <-server1Done:
|
||||
}
|
||||
}
|
||||
|
||||
// TestConnectivityStateSubscriber confirms updates sent by the balancer in
|
||||
// rapid succession are not missed by the subscriber.
|
||||
func (s) TestConnectivityStateSubscriber(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
|
||||
sendStates := []connectivity.State{
|
||||
connectivity.Connecting,
|
||||
connectivity.Ready,
|
||||
connectivity.Idle,
|
||||
connectivity.Connecting,
|
||||
connectivity.Idle,
|
||||
connectivity.Connecting,
|
||||
connectivity.Ready,
|
||||
}
|
||||
wantStates := append(sendStates, connectivity.Shutdown)
|
||||
|
||||
const testBalName = "any"
|
||||
bf := stub.BalancerFuncs{
|
||||
UpdateClientConnState: func(bd *stub.BalancerData, _ balancer.ClientConnState) error {
|
||||
// Send the expected states in rapid succession.
|
||||
for _, s := range sendStates {
|
||||
t.Logf("Sending state update %s", s)
|
||||
bd.ClientConn.UpdateState(balancer.State{ConnectivityState: s})
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
stub.Register(testBalName, bf)
|
||||
|
||||
// Create the ClientConn.
|
||||
const testResName = "any"
|
||||
rb := manual.NewBuilderWithScheme(testResName)
|
||||
cc, err := grpc.NewClient(testResName+":///",
|
||||
grpc.WithResolvers(rb),
|
||||
grpc.WithDefaultServiceConfig(fmt.Sprintf(`{"loadBalancingConfig": [{"%s":{}}]}`, testBalName)),
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("grpc.NewClient() failed: %v", err)
|
||||
}
|
||||
cc.Connect()
|
||||
// Subscribe to state updates. Use a buffer size of 1 to allow the
|
||||
// Shutdown state to go into the channel when Close()ing.
|
||||
connCh := make(chan connectivity.State, 1)
|
||||
s := &funcConnectivityStateSubscriber{
|
||||
onMsg: func(s connectivity.State) {
|
||||
select {
|
||||
case connCh <- s:
|
||||
case <-ctx.Done():
|
||||
}
|
||||
if s == connectivity.Shutdown {
|
||||
close(connCh)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
internal.SubscribeToConnectivityStateChanges.(func(cc *grpc.ClientConn, s grpcsync.Subscriber) func())(cc, s)
|
||||
|
||||
// Send an update from the resolver that will trigger the LB policy's UpdateClientConnState.
|
||||
go rb.UpdateState(resolver.State{})
|
||||
|
||||
// Verify the resulting states.
|
||||
for i, want := range wantStates {
|
||||
if i == len(sendStates) {
|
||||
// Trigger Shutdown to be sent by the channel. Use a goroutine to
|
||||
// ensure the operation does not block.
|
||||
cc.Close()
|
||||
}
|
||||
select {
|
||||
case got := <-connCh:
|
||||
if got != want {
|
||||
t.Errorf("Update %v was %s; want %s", i, got, want)
|
||||
} else {
|
||||
t.Logf("Update %v was %s as expected", i, got)
|
||||
}
|
||||
case <-ctx.Done():
|
||||
t.Fatalf("Timed out waiting for state update %v: %s", i, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test verifies that a channel starts off in IDLE and transitions to CONNECTING
|
||||
// when Connect() is called, and stays there when there are no resolver updates.
|
||||
func (s) TestStateTransitions_WithConnect_NoResolverUpdate(t *testing.T) {
|
||||
backend := stubserver.StartTestService(t, nil)
|
||||
defer backend.Stop()
|
||||
|
||||
mr := manual.NewBuilderWithScheme("e2e-test")
|
||||
defer mr.Close()
|
||||
|
||||
cc, err := grpc.NewClient(mr.Scheme()+":///", grpc.WithResolvers(mr), grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create new client: %v", err)
|
||||
}
|
||||
defer cc.Close()
|
||||
|
||||
if state := cc.GetState(); state != connectivity.Idle {
|
||||
t.Fatalf("Expected initial state to be IDLE, got %v", state)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
|
||||
// The channel should transition to CONNECTING automatically when Connect()
|
||||
// is called.
|
||||
cc.Connect()
|
||||
testutils.AwaitState(ctx, t, cc, connectivity.Connecting)
|
||||
|
||||
// Verify that the channel remains in CONNECTING state for a short time.
|
||||
shortCtx, shortCancel := context.WithTimeout(ctx, defaultTestShortTimeout)
|
||||
defer shortCancel()
|
||||
testutils.AwaitNoStateChange(shortCtx, t, cc, connectivity.Connecting)
|
||||
}
|
||||
|
||||
// Test verifies that a channel starts off in IDLE and transitions to CONNECTING
|
||||
// when Connect() is called, and stays there when there are no resolver updates.
|
||||
func (s) TestStateTransitions_WithRPC_NoResolverUpdate(t *testing.T) {
|
||||
backend := stubserver.StartTestService(t, nil)
|
||||
defer backend.Stop()
|
||||
|
||||
mr := manual.NewBuilderWithScheme("e2e-test")
|
||||
defer mr.Close()
|
||||
|
||||
cc, err := grpc.NewClient(mr.Scheme()+":///", grpc.WithResolvers(mr), grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create new client: %v", err)
|
||||
}
|
||||
defer cc.Close()
|
||||
|
||||
if state := cc.GetState(); state != connectivity.Idle {
|
||||
t.Fatalf("Expected initial state to be IDLE, got %v", state)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
|
||||
// Make an RPC call to transition the channel to CONNECTING.
|
||||
go func() {
|
||||
if _, err := testgrpc.NewTestServiceClient(cc).EmptyCall(ctx, &testpb.Empty{}); err == nil {
|
||||
t.Errorf("Expected RPC to fail, but it succeeded")
|
||||
}
|
||||
}()
|
||||
|
||||
// The channel should transition to CONNECTING automatically when an RPC
|
||||
// is made.
|
||||
testutils.AwaitState(ctx, t, cc, connectivity.Connecting)
|
||||
|
||||
// The channel remains in CONNECTING state for a short time.
|
||||
shortCtx, shortCancel := context.WithTimeout(ctx, defaultTestShortTimeout)
|
||||
defer shortCancel()
|
||||
testutils.AwaitNoStateChange(shortCtx, t, cc, connectivity.Connecting)
|
||||
}
|
||||
|
||||
const testResolverBuildFailureScheme = "test-resolver-build-failure"
|
||||
|
||||
// testResolverBuilder is a resolver builder that fails the first time its
|
||||
// Build method is called, and succeeds thereafter.
|
||||
type testResolverBuilder struct {
|
||||
logger interface {
|
||||
Logf(format string, args ...any)
|
||||
}
|
||||
buildCalled bool
|
||||
manualR *manual.Resolver
|
||||
}
|
||||
|
||||
func (b *testResolverBuilder) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (resolver.Resolver, error) {
|
||||
b.logger.Logf("testResolverBuilder: Build called with target: %v", target)
|
||||
if !b.buildCalled {
|
||||
b.buildCalled = true
|
||||
b.logger.Logf("testResolverBuilder: returning build failure")
|
||||
return nil, fmt.Errorf("simulated resolver build failure")
|
||||
}
|
||||
return b.manualR.Build(target, cc, opts)
|
||||
}
|
||||
|
||||
func (b *testResolverBuilder) Scheme() string {
|
||||
return testResolverBuildFailureScheme
|
||||
}
|
||||
|
||||
// Tests for state transitions when the resolver initially fails to build.
|
||||
func (s) TestStateTransitions_ResolverBuildFailure(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
exitIdleWithRPC bool
|
||||
}{
|
||||
{
|
||||
name: "exitIdleByConnecting",
|
||||
exitIdleWithRPC: false,
|
||||
},
|
||||
{
|
||||
name: "exitIdleByRPC",
|
||||
exitIdleWithRPC: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mr := manual.NewBuilderWithScheme("whatever" + tt.name)
|
||||
backend := stubserver.StartTestService(t, nil)
|
||||
defer backend.Stop()
|
||||
mr.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: backend.Address}}})
|
||||
|
||||
dopts := []grpc.DialOption{
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
grpc.WithResolvers(&testResolverBuilder{logger: t, manualR: mr}),
|
||||
}
|
||||
|
||||
cc, err := grpc.NewClient(testResolverBuildFailureScheme+":///", dopts...)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create new client: %v", err)
|
||||
}
|
||||
defer cc.Close()
|
||||
|
||||
// Ensure that the client is in IDLE before connecting.
|
||||
if state := cc.GetState(); state != connectivity.Idle {
|
||||
t.Fatalf("Expected initial state to be IDLE, got %v", state)
|
||||
}
|
||||
|
||||
// Subscribe to state updates.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
stateCh := make(chan connectivity.State, 1)
|
||||
s := &funcConnectivityStateSubscriber{
|
||||
onMsg: func(s connectivity.State) {
|
||||
select {
|
||||
case stateCh <- s:
|
||||
case <-ctx.Done():
|
||||
}
|
||||
},
|
||||
}
|
||||
internal.SubscribeToConnectivityStateChanges.(func(cc *grpc.ClientConn, s grpcsync.Subscriber) func())(cc, s)
|
||||
|
||||
if tt.exitIdleWithRPC {
|
||||
// The first attempt to kick the channel is expected to return
|
||||
// the resolver build error to the RPC.
|
||||
const wantErr = "simulated resolver build failure"
|
||||
for range 2 {
|
||||
_, err := testgrpc.NewTestServiceClient(cc).EmptyCall(ctx, &testpb.Empty{})
|
||||
if code := status.Code(err); code != codes.Unavailable {
|
||||
t.Fatalf("EmptyCall RPC failed with code %v, want %v", err, codes.Unavailable)
|
||||
}
|
||||
if err == nil || !strings.Contains(err.Error(), wantErr) {
|
||||
t.Fatalf("EmptyCall RPC failed with error: %q, want %q", err, wantErr)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
cc.Connect()
|
||||
}
|
||||
|
||||
wantStates := []connectivity.State{
|
||||
connectivity.Connecting, // When channel exits IDLE for the first time.
|
||||
connectivity.TransientFailure, // Resolver build failure.
|
||||
connectivity.Idle, // After idle timeout.
|
||||
connectivity.Connecting, // When channel exits IDLE again.
|
||||
connectivity.Ready, // Successful resolver build and connection to backend.
|
||||
}
|
||||
for _, wantState := range wantStates {
|
||||
waitForState(ctx, t, stateCh, wantState)
|
||||
switch wantState {
|
||||
case connectivity.TransientFailure:
|
||||
internal.EnterIdleModeForTesting.(func(*grpc.ClientConn))(cc)
|
||||
case connectivity.Idle:
|
||||
if tt.exitIdleWithRPC {
|
||||
if _, err := testgrpc.NewTestServiceClient(cc).EmptyCall(ctx, &testpb.Empty{}); err != nil {
|
||||
t.Fatalf("EmptyCall RPC failed: %v", err)
|
||||
}
|
||||
} else {
|
||||
cc.Connect()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Tests for state transitions when the resolver reports no addresses.
|
||||
func (s) TestStateTransitions_WithRPC_ResolverUpdateContainsNoAddresses(t *testing.T) {
|
||||
mr := manual.NewBuilderWithScheme("e2e-test")
|
||||
mr.InitialState(resolver.State{})
|
||||
defer mr.Close()
|
||||
|
||||
cc, err := grpc.NewClient(mr.Scheme()+":///", grpc.WithResolvers(mr), grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create new client: %v", err)
|
||||
}
|
||||
defer cc.Close()
|
||||
|
||||
if state := cc.GetState(); state != connectivity.Idle {
|
||||
t.Fatalf("Expected initial state to be IDLE, got %v", state)
|
||||
}
|
||||
|
||||
// Subscribe to state updates.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
stateCh := make(chan connectivity.State, 1)
|
||||
s := &funcConnectivityStateSubscriber{
|
||||
onMsg: func(s connectivity.State) {
|
||||
select {
|
||||
case stateCh <- s:
|
||||
case <-ctx.Done():
|
||||
}
|
||||
},
|
||||
}
|
||||
internal.SubscribeToConnectivityStateChanges.(func(cc *grpc.ClientConn, s grpcsync.Subscriber) func())(cc, s)
|
||||
|
||||
// Make an RPC call to transition the channel to CONNECTING.
|
||||
const wantErr = "name resolver error: produced zero addresses"
|
||||
for range 2 {
|
||||
_, err := testgrpc.NewTestServiceClient(cc).EmptyCall(ctx, &testpb.Empty{})
|
||||
if code := status.Code(err); code != codes.Unavailable {
|
||||
t.Errorf("EmptyCall RPC failed with code %v, want %v", err, codes.Unavailable)
|
||||
}
|
||||
if err == nil || !strings.Contains(err.Error(), wantErr) {
|
||||
t.Errorf("EmptyCall RPC failed with error: %q, want %q", err, wantErr)
|
||||
}
|
||||
}
|
||||
|
||||
wantStates := []connectivity.State{
|
||||
connectivity.Connecting, // When channel exits IDLE for the first time.
|
||||
connectivity.TransientFailure, // No endpoints from the resolver
|
||||
connectivity.Idle, // After idle timeout.
|
||||
}
|
||||
for _, wantState := range wantStates {
|
||||
waitForState(ctx, t, stateCh, wantState)
|
||||
if wantState == connectivity.TransientFailure {
|
||||
internal.EnterIdleModeForTesting.(func(*grpc.ClientConn))(cc)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user