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  }