github.com/koko1123/flow-go-1@v0.29.6/engine/access/rpc/backend/connection_factory.go (about) 1 package backend 2 3 import ( 4 "context" 5 "fmt" 6 "io" 7 "net" 8 "sync" 9 "time" 10 11 lru "github.com/hashicorp/golang-lru" 12 "github.com/onflow/flow/protobuf/go/flow/access" 13 "github.com/onflow/flow/protobuf/go/flow/execution" 14 "github.com/rs/zerolog" 15 "google.golang.org/grpc" 16 "google.golang.org/grpc/connectivity" 17 "google.golang.org/grpc/credentials/insecure" 18 "google.golang.org/grpc/keepalive" 19 20 "github.com/koko1123/flow-go-1/module" 21 "github.com/koko1123/flow-go-1/utils/grpcutils" 22 ) 23 24 // DefaultClientTimeout is used when making a GRPC request to a collection node or an execution node 25 const DefaultClientTimeout = 3 * time.Second 26 27 // ConnectionFactory is used to create an access api client 28 type ConnectionFactory interface { 29 GetAccessAPIClient(address string) (access.AccessAPIClient, io.Closer, error) 30 InvalidateAccessAPIClient(address string) 31 GetExecutionAPIClient(address string) (execution.ExecutionAPIClient, io.Closer, error) 32 InvalidateExecutionAPIClient(address string) 33 } 34 35 type ProxyConnectionFactory struct { 36 ConnectionFactory 37 targetAddress string 38 } 39 40 type noopCloser struct{} 41 42 func (c *noopCloser) Close() error { 43 return nil 44 } 45 46 func (p *ProxyConnectionFactory) GetAccessAPIClient(address string) (access.AccessAPIClient, io.Closer, error) { 47 return p.ConnectionFactory.GetAccessAPIClient(p.targetAddress) 48 } 49 50 func (p *ProxyConnectionFactory) GetExecutionAPIClient(address string) (execution.ExecutionAPIClient, io.Closer, error) { 51 return p.ConnectionFactory.GetExecutionAPIClient(p.targetAddress) 52 } 53 54 type ConnectionFactoryImpl struct { 55 CollectionGRPCPort uint 56 ExecutionGRPCPort uint 57 CollectionNodeGRPCTimeout time.Duration 58 ExecutionNodeGRPCTimeout time.Duration 59 ConnectionsCache *lru.Cache 60 CacheSize uint 61 AccessMetrics module.AccessMetrics 62 Log zerolog.Logger 63 mutex sync.Mutex 64 } 65 66 type CachedClient struct { 67 ClientConn *grpc.ClientConn 68 Address string 69 mutex sync.Mutex 70 timeout time.Duration 71 } 72 73 // createConnection creates new gRPC connections to remote node 74 func (cf *ConnectionFactoryImpl) createConnection(address string, timeout time.Duration) (*grpc.ClientConn, error) { 75 76 if timeout == 0 { 77 timeout = DefaultClientTimeout 78 } 79 80 keepaliveParams := keepalive.ClientParameters{ 81 // how long the client will wait before sending a keepalive to the server if there is no activity 82 Time: 10 * time.Second, 83 // how long the client will wait for a response from the keepalive before closing 84 Timeout: timeout, 85 } 86 87 // ClientConn's default KeepAlive on connections is indefinite, assuming the timeout isn't reached 88 // The connections should be safe to be persisted and reused 89 // https://pkg.go.dev/google.golang.org/grpc#WithKeepaliveParams 90 // https://grpc.io/blog/grpc-on-http2/#keeping-connections-alive 91 conn, err := grpc.Dial( 92 address, 93 grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(grpcutils.DefaultMaxMsgSize)), 94 grpc.WithTransportCredentials(insecure.NewCredentials()), 95 grpc.WithKeepaliveParams(keepaliveParams), 96 WithClientUnaryInterceptor(timeout)) 97 if err != nil { 98 return nil, fmt.Errorf("failed to connect to address %s: %w", address, err) 99 } 100 return conn, nil 101 } 102 103 func (cf *ConnectionFactoryImpl) retrieveConnection(grpcAddress string, timeout time.Duration) (*grpc.ClientConn, error) { 104 var conn *grpc.ClientConn 105 var store *CachedClient 106 cacheHit := false 107 cf.mutex.Lock() 108 if res, ok := cf.ConnectionsCache.Get(grpcAddress); ok { 109 cacheHit = true 110 store = res.(*CachedClient) 111 conn = store.ClientConn 112 } else { 113 store = &CachedClient{ 114 ClientConn: nil, 115 Address: grpcAddress, 116 timeout: timeout, 117 } 118 cf.Log.Debug().Str("cached_client_added", grpcAddress).Msg("adding new cached client to pool") 119 cf.ConnectionsCache.Add(grpcAddress, store) 120 if cf.AccessMetrics != nil { 121 cf.AccessMetrics.ConnectionAddedToPool() 122 } 123 } 124 cf.mutex.Unlock() 125 store.mutex.Lock() 126 defer store.mutex.Unlock() 127 128 if conn == nil || conn.GetState() == connectivity.Shutdown { 129 var err error 130 conn, err = cf.createConnection(grpcAddress, timeout) 131 if err != nil { 132 return nil, err 133 } 134 store.ClientConn = conn 135 if cf.AccessMetrics != nil { 136 if cacheHit { 137 cf.AccessMetrics.ConnectionFromPoolUpdated() 138 } 139 cf.AccessMetrics.NewConnectionEstablished() 140 cf.AccessMetrics.TotalConnectionsInPool(uint(cf.ConnectionsCache.Len()), cf.CacheSize) 141 } 142 } else if cf.AccessMetrics != nil { 143 cf.AccessMetrics.ConnectionFromPoolReused() 144 } 145 return conn, nil 146 } 147 148 func (cf *ConnectionFactoryImpl) GetAccessAPIClient(address string) (access.AccessAPIClient, io.Closer, error) { 149 150 grpcAddress, err := getGRPCAddress(address, cf.CollectionGRPCPort) 151 if err != nil { 152 return nil, nil, err 153 } 154 155 var conn *grpc.ClientConn 156 if cf.ConnectionsCache != nil { 157 conn, err = cf.retrieveConnection(grpcAddress, cf.CollectionNodeGRPCTimeout) 158 if err != nil { 159 return nil, nil, err 160 } 161 return access.NewAccessAPIClient(conn), &noopCloser{}, err 162 } 163 164 conn, err = cf.createConnection(grpcAddress, cf.CollectionNodeGRPCTimeout) 165 if err != nil { 166 return nil, nil, err 167 } 168 169 accessAPIClient := access.NewAccessAPIClient(conn) 170 closer := io.Closer(conn) 171 return accessAPIClient, closer, nil 172 } 173 174 func (cf *ConnectionFactoryImpl) InvalidateAccessAPIClient(address string) { 175 if cf.ConnectionsCache != nil { 176 cf.Log.Debug().Str("cached_access_client_invalidated", address).Msg("invalidating cached access client") 177 cf.invalidateAPIClient(address, cf.CollectionGRPCPort) 178 } 179 } 180 181 func (cf *ConnectionFactoryImpl) GetExecutionAPIClient(address string) (execution.ExecutionAPIClient, io.Closer, error) { 182 183 grpcAddress, err := getGRPCAddress(address, cf.ExecutionGRPCPort) 184 if err != nil { 185 return nil, nil, err 186 } 187 188 var conn *grpc.ClientConn 189 if cf.ConnectionsCache != nil { 190 conn, err = cf.retrieveConnection(grpcAddress, cf.ExecutionNodeGRPCTimeout) 191 if err != nil { 192 return nil, nil, err 193 } 194 return execution.NewExecutionAPIClient(conn), &noopCloser{}, nil 195 } 196 197 conn, err = cf.createConnection(grpcAddress, cf.ExecutionNodeGRPCTimeout) 198 if err != nil { 199 return nil, nil, err 200 } 201 202 executionAPIClient := execution.NewExecutionAPIClient(conn) 203 closer := io.Closer(conn) 204 return executionAPIClient, closer, nil 205 } 206 207 func (cf *ConnectionFactoryImpl) InvalidateExecutionAPIClient(address string) { 208 if cf.ConnectionsCache != nil { 209 cf.Log.Debug().Str("cached_execution_client_invalidated", address).Msg("invalidating cached execution client") 210 cf.invalidateAPIClient(address, cf.ExecutionGRPCPort) 211 } 212 } 213 214 func (cf *ConnectionFactoryImpl) invalidateAPIClient(address string, port uint) { 215 grpcAddress, _ := getGRPCAddress(address, port) 216 if res, ok := cf.ConnectionsCache.Get(grpcAddress); ok { 217 store := res.(*CachedClient) 218 store.Close() 219 if cf.AccessMetrics != nil { 220 cf.AccessMetrics.ConnectionFromPoolInvalidated() 221 } 222 } 223 } 224 225 func (s *CachedClient) Close() { 226 s.mutex.Lock() 227 conn := s.ClientConn 228 s.ClientConn = nil 229 s.mutex.Unlock() 230 if conn == nil { 231 return 232 } 233 // allow time for any existing requests to finish before closing the connection 234 time.Sleep(s.timeout + 1*time.Second) 235 conn.Close() 236 } 237 238 // getExecutionNodeAddress translates flow.Identity address to the GRPC address of the node by switching the port to the 239 // GRPC port from the libp2p port 240 func getGRPCAddress(address string, grpcPort uint) (string, error) { 241 // split hostname and port 242 hostnameOrIP, _, err := net.SplitHostPort(address) 243 if err != nil { 244 return "", err 245 } 246 // use the hostname from identity list and port number as the one passed in as argument 247 grpcAddress := fmt.Sprintf("%s:%d", hostnameOrIP, grpcPort) 248 249 return grpcAddress, nil 250 } 251 252 func WithClientUnaryInterceptor(timeout time.Duration) grpc.DialOption { 253 254 clientTimeoutInterceptor := func( 255 ctx context.Context, 256 method string, 257 req interface{}, 258 reply interface{}, 259 cc *grpc.ClientConn, 260 invoker grpc.UnaryInvoker, 261 opts ...grpc.CallOption, 262 ) error { 263 264 // create a context that expires after timeout 265 ctxWithTimeout, cancel := context.WithTimeout(ctx, timeout) 266 267 defer cancel() 268 269 // call the remote GRPC using the short context 270 err := invoker(ctxWithTimeout, method, req, reply, cc, opts...) 271 272 return err 273 } 274 275 return grpc.WithUnaryInterceptor(clientTimeoutInterceptor) 276 }