github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/module/grpcserver/server_builder.go (about)

     1  package grpcserver
     2  
     3  import (
     4  	"context"
     5  
     6  	grpc_prometheus "github.com/grpc-ecosystem/go-grpc-prometheus"
     7  	"github.com/rs/zerolog"
     8  	"go.uber.org/atomic"
     9  	"google.golang.org/grpc"
    10  	"google.golang.org/grpc/credentials"
    11  
    12  	"github.com/onflow/flow-go/engine/common/rpc"
    13  	"github.com/onflow/flow-go/module/irrecoverable"
    14  )
    15  
    16  type Option func(*GrpcServerBuilder)
    17  
    18  // WithTransportCredentials sets the transport credentials parameters for a grpc server builder.
    19  func WithTransportCredentials(transportCredentials credentials.TransportCredentials) Option {
    20  	return func(c *GrpcServerBuilder) {
    21  		c.transportCredentials = transportCredentials
    22  	}
    23  }
    24  
    25  // WithStreamInterceptor sets the StreamInterceptor option to grpc server.
    26  func WithStreamInterceptor() Option {
    27  	return func(c *GrpcServerBuilder) {
    28  		c.stateStreamInterceptorEnable = true
    29  	}
    30  }
    31  
    32  // GrpcServerBuilder created for separating the creation and starting GrpcServer,
    33  // cause services need to be registered before the server starts.
    34  type GrpcServerBuilder struct {
    35  	log            zerolog.Logger
    36  	gRPCListenAddr string
    37  	server         *grpc.Server
    38  	signalerCtx    *atomic.Pointer[irrecoverable.SignalerContext]
    39  
    40  	transportCredentials         credentials.TransportCredentials // the GRPC credentials
    41  	stateStreamInterceptorEnable bool
    42  }
    43  
    44  // NewGrpcServerBuilder creates a new builder for configuring and initializing a gRPC server.
    45  //
    46  // The builder is configured with the provided parameters such as logger, gRPC server address, maximum message size,
    47  // API rate limits, and additional options. The builder also sets up the necessary interceptors, including handling
    48  // irrecoverable errors using the irrecoverable.SignalerContext. The gRPC server can be configured with options such
    49  // as maximum message sizes and interceptors for handling RPC calls.
    50  //
    51  // If RPC metrics are enabled, the builder adds the gRPC Prometheus interceptor for collecting metrics. Additionally,
    52  // it can enable a state stream interceptor based on the configuration. Rate limiting interceptors can be added based
    53  // on specified API rate limits. Logging and custom interceptors are applied, and the final gRPC server is returned.
    54  //
    55  // If transport credentials are provided, a secure gRPC server is created; otherwise, an unsecured server is initialized.
    56  //
    57  // Note: The gRPC server is created with the specified options and is ready for further configuration or starting.
    58  func NewGrpcServerBuilder(log zerolog.Logger,
    59  	gRPCListenAddr string,
    60  	maxMsgSize uint,
    61  	rpcMetricsEnabled bool,
    62  	apiRateLimits map[string]int, // the api rate limit (max calls per second) for each of the Access API e.g. Ping->100, GetTransaction->300
    63  	apiBurstLimits map[string]int, // the api burst limit (max calls at the same time) for each of the Access API e.g. Ping->50, GetTransaction->10
    64  	opts ...Option,
    65  ) *GrpcServerBuilder {
    66  	log = log.With().Str("component", "grpc_server").Logger()
    67  
    68  	grpcServerBuilder := &GrpcServerBuilder{
    69  		gRPCListenAddr: gRPCListenAddr,
    70  	}
    71  
    72  	for _, applyOption := range opts {
    73  		applyOption(grpcServerBuilder)
    74  	}
    75  
    76  	// we use an atomic pointer to setup an interceptor for handling irrecoverable errors, the necessity of this approach
    77  	// is dictated by complex startup order of grpc server and other services. At the point where we need to register
    78  	// an interceptor we don't have an `irrecoverable.SignalerContext`, it becomes available only when we start
    79  	// the server but at that point we can't register interceptors anymore, so we inject it using this approach.
    80  	signalerCtx := atomic.NewPointer[irrecoverable.SignalerContext](nil)
    81  
    82  	// create a GRPC server to serve GRPC clients
    83  	grpcOpts := []grpc.ServerOption{
    84  		grpc.MaxRecvMsgSize(int(maxMsgSize)),
    85  		grpc.MaxSendMsgSize(int(maxMsgSize)),
    86  	}
    87  	var interceptors []grpc.UnaryServerInterceptor // ordered list of interceptors
    88  	// This interceptor is responsible for ensuring that irrecoverable errors are properly propagated using
    89  	// the irrecoverable.SignalerContext. It replaces the original gRPC context with a new one that includes
    90  	// the irrecoverable.SignalerContextKey if available, allowing the server to handle error conditions indicating
    91  	// an inconsistent or corrupted node state. If no irrecoverable.SignalerContext is present, the original context
    92  	// is used to process the gRPC request.
    93  	//
    94  	// The interceptor follows the grpc.UnaryServerInterceptor signature, where it takes the incoming gRPC context,
    95  	// request, unary server information, and handler function. It returns the response and error after handling
    96  	// the request. This mechanism ensures consistent error handling for gRPC requests across the server.
    97  	interceptors = append(interceptors, func(ctx context.Context, req any, _ *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp any, err error) {
    98  		if sigCtx := signalerCtx.Load(); sigCtx != nil {
    99  			resp, err = handler(irrecoverable.WithSignalerContext(ctx, *sigCtx), req)
   100  		} else {
   101  			resp, err = handler(ctx, req)
   102  		}
   103  		return
   104  	})
   105  
   106  	// if rpc metrics is enabled, first create the grpc metrics interceptor
   107  	if rpcMetricsEnabled {
   108  		interceptors = append(interceptors, grpc_prometheus.UnaryServerInterceptor)
   109  
   110  		if grpcServerBuilder.stateStreamInterceptorEnable {
   111  			// note: intentionally not adding logging or rate limit interceptors for streams.
   112  			// rate limiting is done in the handler, and we don't need log events for every message as
   113  			// that would be too noisy.
   114  			log.Info().Msg("stateStreamInterceptorEnable true")
   115  			grpcOpts = append(grpcOpts, grpc.StreamInterceptor(grpc_prometheus.StreamServerInterceptor))
   116  		} else {
   117  			log.Info().Msg("stateStreamInterceptorEnable false")
   118  		}
   119  	}
   120  	if len(apiRateLimits) > 0 {
   121  		// create a rate limit interceptor
   122  		rateLimitInterceptor := rpc.NewRateLimiterInterceptor(log, apiRateLimits, apiBurstLimits).UnaryServerInterceptor
   123  		// append the rate limit interceptor to the list of interceptors
   124  		interceptors = append(interceptors, rateLimitInterceptor)
   125  	}
   126  	// add the logging interceptor, ensure it is innermost wrapper
   127  	interceptors = append(interceptors, rpc.LoggingInterceptor(log)...)
   128  	// create a chained unary interceptor
   129  	// create an unsecured grpc server
   130  	grpcOpts = append(grpcOpts, grpc.ChainUnaryInterceptor(interceptors...))
   131  
   132  	if grpcServerBuilder.transportCredentials != nil {
   133  		log = log.With().Str("endpoint", "secure").Logger()
   134  		// create a secure server by using the secure grpc credentials that are passed in as part of config
   135  		grpcOpts = append(grpcOpts, grpc.Creds(grpcServerBuilder.transportCredentials))
   136  	} else {
   137  		log = log.With().Str("endpoint", "unsecure").Logger()
   138  	}
   139  	grpcServerBuilder.log = log
   140  	grpcServerBuilder.server = grpc.NewServer(grpcOpts...)
   141  	grpcServerBuilder.signalerCtx = signalerCtx
   142  
   143  	return grpcServerBuilder
   144  }
   145  
   146  func (b *GrpcServerBuilder) Build() *GrpcServer {
   147  	return NewGrpcServer(b.log, b.gRPCListenAddr, b.server, b.signalerCtx)
   148  }