go.temporal.io/server@v1.23.0/common/membership/grpc_resolver_test.go (about)

     1  // The MIT License
     2  //
     3  // Copyright (c) 2020 Temporal Technologies Inc.  All rights reserved.
     4  //
     5  // Copyright (c) 2020 Uber Technologies, Inc.
     6  //
     7  // Permission is hereby granted, free of charge, to any person obtaining a copy
     8  // of this software and associated documentation files (the "Software"), to deal
     9  // in the Software without restriction, including without limitation the rights
    10  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    11  // copies of the Software, and to permit persons to whom the Software is
    12  // furnished to do so, subject to the following conditions:
    13  //
    14  // The above copyright notice and this permission notice shall be included in
    15  // all copies or substantial portions of the Software.
    16  //
    17  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    18  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    19  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    20  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    21  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    22  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    23  // THE SOFTWARE.
    24  
    25  package membership
    26  
    27  import (
    28  	"context"
    29  	"net"
    30  	"testing"
    31  	"time"
    32  
    33  	"github.com/golang/mock/gomock"
    34  	"github.com/stretchr/testify/assert"
    35  	"github.com/stretchr/testify/require"
    36  	"go.temporal.io/server/common/primitives"
    37  	"go.temporal.io/server/internal/nettest"
    38  	"google.golang.org/grpc"
    39  	"google.golang.org/grpc/credentials/insecure"
    40  )
    41  
    42  func TestGRPCBuilder(t *testing.T) {
    43  	t.Parallel()
    44  
    45  	// There's a lot of channel stuff in this test, so we use a context to make sure we don't hang forever if something
    46  	// goes wrong.
    47  	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    48  	defer cancel()
    49  
    50  	ctrl := gomock.NewController(t)
    51  	sr := NewMockServiceResolver(ctrl)
    52  
    53  	// On the first call to [ServiceResolver.Members], return an empty list of members
    54  	sr.EXPECT().Members().Return([]HostInfo{})
    55  	// Once our resolver registers a listener to membership changes, get a hold of the channel it's listening on.
    56  	sr.EXPECT().AddListener(gomock.Any(), gomock.Any()).Do(func(_ string, ch chan<- *ChangedEvent) {
    57  		// Return a single member on the next call to [ServiceResolver.Members]. This simulates a temporary network
    58  		// partition where we can't find any hosts for the frontend for a short period of time, but then we get a host.
    59  		sr.EXPECT().Members().Return([]HostInfo{
    60  			NewHostInfoFromAddress("localhost:1234"),
    61  		}).MinTimes(1) // MinTimes(1) because we don't control when ResolveNow is called
    62  
    63  		// After the first call to [ServiceResolver.Members] returns an empty list, expect our resolver to request a
    64  		// refresh of the members list. When it does, notify the listener that the members list has changed
    65  		sr.EXPECT().RequestRefresh().Do(func() {
    66  			select {
    67  			case <-ctx.Done():
    68  			case ch <- &ChangedEvent{}:
    69  			}
    70  		})
    71  	})
    72  
    73  	monitor := NewMockMonitor(ctrl)
    74  	monitor.EXPECT().GetResolver(primitives.FrontendService).Return(sr, nil)
    75  
    76  	// Start a fake local server and then dial it.
    77  	serverErrs := make(chan error)
    78  	p := nettest.NewPipe()
    79  
    80  	// This is our fake server. It accepts a connection and then immediately closes it.
    81  	go func() {
    82  		conn, _ := p.Accept(ctx.Done())
    83  		serverErrs <- conn.Close()
    84  	}()
    85  
    86  	// This is where we invoke the code under test. We dial the frontend service. The URL should use our custom
    87  	// protocol, and then our resolver should resolve this to the localhost:1234 address.
    88  	resolverBuilder := &grpcBuilder{}
    89  	resolverBuilder.monitor.Store(monitor)
    90  
    91  	url := (&GRPCResolver{}).MakeURL(primitives.FrontendService)
    92  	assert.Equal(t, "membership://frontend", url)
    93  
    94  	// dialedAddress is the actual address that the gRPC framework dialed after resolving the URL using our resolver.
    95  	var dialedAddress string
    96  
    97  	conn, err := grpc.Dial(
    98  		url,
    99  		grpc.WithTransportCredentials(insecure.NewCredentials()),
   100  		grpc.WithResolvers(resolverBuilder),
   101  		grpc.WithContextDialer(func(ctx context.Context, s string) (net.Conn, error) {
   102  			dialedAddress = s
   103  			return p.Connect(ctx.Done())
   104  		}),
   105  	)
   106  	require.NoError(t, err)
   107  
   108  	require.NoError(t, <-serverErrs)
   109  
   110  	// The gRPC library calls [resolver.Resolver.Close] when the connection is closed in a background goroutine, so we
   111  	// can't synchronously assert that [ServiceResolver.RemoveListener] was called right after the connection is closed.
   112  	// Instead, we use a channel to signal that the listener was removed.
   113  	listenerRemoved := make(chan struct{})
   114  
   115  	sr.EXPECT().RemoveListener(gomock.Any()).Do(func(string) {
   116  		close(listenerRemoved)
   117  	})
   118  	assert.NoError(t, conn.Close())
   119  	select {
   120  	case <-ctx.Done():
   121  		t.Fatal("timed out waiting for resolver to be removed")
   122  	case <-listenerRemoved:
   123  	}
   124  
   125  	// Verify that the address we dialed was the address of the single host in the members list.
   126  	assert.Equal(t, "localhost:1234", dialedAddress)
   127  }