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 }