github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/admin/command_runner.go (about)

     1  package admin
     2  
     3  import (
     4  	"context"
     5  	"crypto/tls"
     6  	"errors"
     7  	"fmt"
     8  	"net"
     9  	"net/http"
    10  	"net/http/pprof"
    11  	"os"
    12  	"sync"
    13  	"time"
    14  
    15  	"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
    16  	"github.com/rs/zerolog"
    17  	"google.golang.org/grpc"
    18  	"google.golang.org/grpc/codes"
    19  	"google.golang.org/grpc/credentials/insecure"
    20  	"google.golang.org/grpc/status"
    21  
    22  	pb "github.com/onflow/flow-go/admin/admin"
    23  	"github.com/onflow/flow-go/module/component"
    24  	"github.com/onflow/flow-go/module/irrecoverable"
    25  )
    26  
    27  var _ component.Component = (*CommandRunner)(nil)
    28  
    29  const CommandRunnerShutdownTimeout = 5 * time.Second
    30  
    31  type CommandHandler func(ctx context.Context, request *CommandRequest) (interface{}, error)
    32  type CommandValidator func(request *CommandRequest) error
    33  type CommandRunnerOption func(*CommandRunner)
    34  
    35  // CommandRequest is the structure of an admin command request.
    36  type CommandRequest struct {
    37  	// Data is the payload of the request, generated by the request initiator.
    38  	// This is populated by the admin command framework and is available to both
    39  	// Validator and Handler functions.
    40  	Data interface{}
    41  	// ValidatorData may be optionally set by the Validator function, and will
    42  	// then be available for use in the Handler function.
    43  	ValidatorData interface{}
    44  }
    45  
    46  func WithTLS(config *tls.Config) CommandRunnerOption {
    47  	return func(r *CommandRunner) {
    48  		r.tlsConfig = config
    49  	}
    50  }
    51  
    52  func WithGRPCAddress(address string) CommandRunnerOption {
    53  	return func(r *CommandRunner) {
    54  		r.grpcAddress = address
    55  	}
    56  }
    57  
    58  func WithMaxMsgSize(size int) CommandRunnerOption {
    59  	return func(r *CommandRunner) {
    60  		r.maxMsgSize = size
    61  	}
    62  }
    63  
    64  type CommandRunnerBootstrapper struct {
    65  	handlers   map[string]CommandHandler
    66  	validators map[string]CommandValidator
    67  }
    68  
    69  func NewCommandRunnerBootstrapper() *CommandRunnerBootstrapper {
    70  	return &CommandRunnerBootstrapper{
    71  		handlers:   make(map[string]CommandHandler),
    72  		validators: make(map[string]CommandValidator),
    73  	}
    74  }
    75  
    76  func (r *CommandRunnerBootstrapper) Bootstrap(logger zerolog.Logger, bindAddress string, opts ...CommandRunnerOption) *CommandRunner {
    77  	handlers := make(map[string]CommandHandler)
    78  	commands := make([]interface{}, 0, len(r.handlers))
    79  
    80  	r.RegisterHandler("ping", func(ctx context.Context, req *CommandRequest) (interface{}, error) {
    81  		return "pong", nil
    82  	})
    83  
    84  	r.RegisterHandler("list-commands", func(ctx context.Context, req *CommandRequest) (interface{}, error) {
    85  		return commands, nil
    86  	})
    87  
    88  	for command, handler := range r.handlers {
    89  		handlers[command] = handler
    90  		commands = append(commands, command)
    91  	}
    92  
    93  	validators := make(map[string]CommandValidator)
    94  	for command, validator := range r.validators {
    95  		validators[command] = validator
    96  	}
    97  
    98  	commandRunner := &CommandRunner{
    99  		handlers:         handlers,
   100  		validators:       validators,
   101  		grpcAddress:      fmt.Sprintf("%s/flow-node-admin.sock", os.TempDir()),
   102  		httpAddress:      bindAddress,
   103  		logger:           logger.With().Str("admin", "command_runner").Logger(),
   104  		startupCompleted: make(chan struct{}),
   105  	}
   106  
   107  	for _, opt := range opts {
   108  		opt(commandRunner)
   109  	}
   110  
   111  	return commandRunner
   112  }
   113  
   114  func (r *CommandRunnerBootstrapper) RegisterHandler(command string, handler CommandHandler) bool {
   115  	if _, ok := r.handlers[command]; ok {
   116  		return false
   117  	}
   118  	r.handlers[command] = handler
   119  	return true
   120  }
   121  
   122  func (r *CommandRunnerBootstrapper) RegisterValidator(command string, validator CommandValidator) bool {
   123  	if _, ok := r.validators[command]; ok {
   124  		return false
   125  	}
   126  	r.validators[command] = validator
   127  	return true
   128  }
   129  
   130  type CommandRunner struct {
   131  	handlers    map[string]CommandHandler
   132  	validators  map[string]CommandValidator
   133  	grpcAddress string
   134  	httpAddress string
   135  	maxMsgSize  int
   136  	tlsConfig   *tls.Config
   137  	logger      zerolog.Logger
   138  
   139  	// wait for worker routines to be ready
   140  	workersStarted sync.WaitGroup
   141  
   142  	// wait for worker routines to exit
   143  	workersFinished sync.WaitGroup
   144  
   145  	// signals startup completion
   146  	startupCompleted chan struct{}
   147  }
   148  
   149  func (r *CommandRunner) getHandler(command string) CommandHandler {
   150  	return r.handlers[command]
   151  }
   152  
   153  func (r *CommandRunner) getValidator(command string) CommandValidator {
   154  	return r.validators[command]
   155  }
   156  
   157  func (r *CommandRunner) Start(ctx irrecoverable.SignalerContext) {
   158  	if err := r.runAdminServer(ctx); err != nil {
   159  		ctx.Throw(fmt.Errorf("failed to start admin server: %w", err))
   160  	}
   161  
   162  	close(r.startupCompleted)
   163  }
   164  
   165  func (r *CommandRunner) Ready() <-chan struct{} {
   166  	ready := make(chan struct{})
   167  
   168  	go func() {
   169  		<-r.startupCompleted
   170  		r.workersStarted.Wait()
   171  		close(ready)
   172  	}()
   173  
   174  	return ready
   175  }
   176  
   177  func (r *CommandRunner) Done() <-chan struct{} {
   178  	done := make(chan struct{})
   179  
   180  	go func() {
   181  		<-r.startupCompleted
   182  		r.workersFinished.Wait()
   183  		close(done)
   184  	}()
   185  
   186  	return done
   187  }
   188  
   189  func (r *CommandRunner) runAdminServer(ctx irrecoverable.SignalerContext) error {
   190  	select {
   191  	case <-ctx.Done():
   192  		return ctx.Err()
   193  	default:
   194  	}
   195  
   196  	r.logger.Info().Msg("admin server starting up")
   197  
   198  	listener, err := net.Listen("unix", r.grpcAddress)
   199  	if err != nil {
   200  		return fmt.Errorf("failed to listen on admin server address: %w", err)
   201  	}
   202  
   203  	opts := []grpc.ServerOption{
   204  		grpc.MaxRecvMsgSize(r.maxMsgSize),
   205  		grpc.MaxSendMsgSize(r.maxMsgSize),
   206  	}
   207  
   208  	grpcServer := grpc.NewServer(opts...)
   209  	pb.RegisterAdminServer(grpcServer, NewAdminServer(r))
   210  
   211  	r.workersStarted.Add(1)
   212  	r.workersFinished.Add(1)
   213  	go func() {
   214  		defer r.workersFinished.Done()
   215  		r.workersStarted.Done()
   216  
   217  		if err := grpcServer.Serve(listener); err != nil {
   218  			r.logger.Err(err).Msg("gRPC server encountered fatal error")
   219  			ctx.Throw(err)
   220  		}
   221  	}()
   222  
   223  	// Initialize gRPC and HTTP muxers
   224  	gwmux := runtime.NewServeMux()
   225  	dialOpts := []grpc.DialOption{
   226  		grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(r.maxMsgSize)),
   227  		grpc.WithTransportCredentials(insecure.NewCredentials()),
   228  	}
   229  	err = pb.RegisterAdminHandlerFromEndpoint(ctx, gwmux, "unix:///"+r.grpcAddress, dialOpts)
   230  	if err != nil {
   231  		return fmt.Errorf("failed to register http handlers for admin service: %w", err)
   232  	}
   233  
   234  	mux := http.NewServeMux()
   235  	mux.Handle("/", gwmux)
   236  
   237  	// This adds an ability to use standard go tooling for performance troubleshooting e.g.:
   238  	//  go tool pprof http://localhost:9002/debug/pprof/goroutine
   239  	for _, name := range []string{"allocs", "block", "goroutine", "heap", "mutex", "threadcreate"} {
   240  		mux.HandleFunc(fmt.Sprintf("/debug/pprof/%s", name), pprof.Handler(name).ServeHTTP)
   241  	}
   242  	mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
   243  	mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
   244  
   245  	httpServer := &http.Server{
   246  		Addr:      r.httpAddress,
   247  		Handler:   mux,
   248  		TLSConfig: r.tlsConfig,
   249  	}
   250  
   251  	r.workersStarted.Add(1)
   252  	r.workersFinished.Add(1)
   253  	go func() {
   254  		defer r.workersFinished.Done()
   255  		r.workersStarted.Done()
   256  
   257  		// Start HTTP server (and proxy calls to gRPC server endpoint)
   258  		var err error
   259  		if r.tlsConfig == nil {
   260  			err = httpServer.ListenAndServe()
   261  		} else {
   262  			err = httpServer.ListenAndServeTLS("", "")
   263  		}
   264  
   265  		if err != nil && !errors.Is(err, http.ErrServerClosed) {
   266  			r.logger.Err(err).Msg("HTTP server encountered error")
   267  			ctx.Throw(err)
   268  		}
   269  	}()
   270  
   271  	r.workersStarted.Add(1)
   272  	r.workersFinished.Add(1)
   273  	go func() {
   274  		defer r.workersFinished.Done()
   275  		r.workersStarted.Done()
   276  
   277  		<-ctx.Done()
   278  		r.logger.Info().Msg("admin server shutting down")
   279  
   280  		grpcServer.Stop()
   281  
   282  		if httpServer != nil {
   283  			shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), CommandRunnerShutdownTimeout)
   284  			defer shutdownCancel()
   285  
   286  			if err := httpServer.Shutdown(shutdownCtx); err != nil {
   287  				r.logger.Err(err).Msg("failed to shutdown http server")
   288  				ctx.Throw(err)
   289  			}
   290  		}
   291  	}()
   292  
   293  	return nil
   294  }
   295  
   296  func (r *CommandRunner) runCommand(ctx context.Context, command string, data interface{}) (interface{}, error) {
   297  	r.logger.Info().Str("command", command).Msg("received new command")
   298  
   299  	req := &CommandRequest{Data: data}
   300  
   301  	if validator := r.getValidator(command); validator != nil {
   302  		if validationErr := validator(req); validationErr != nil {
   303  			// for expected validation errors, return code InvalidArgument and the error text
   304  			if IsInvalidAdminParameterError(validationErr) {
   305  				return nil, status.Error(codes.InvalidArgument, validationErr.Error())
   306  			}
   307  			// for unexpected errors, return code Internal and log a warning
   308  			r.logger.Err(validationErr).Msg("unexpected error validating admin request")
   309  			return nil, status.Error(codes.Internal, validationErr.Error())
   310  		}
   311  	}
   312  
   313  	var handleResult interface{}
   314  	var handleErr error
   315  
   316  	if handler := r.getHandler(command); handler != nil {
   317  		if handleResult, handleErr = handler(ctx, req); handleErr != nil {
   318  			if errors.Is(handleErr, context.Canceled) {
   319  				return nil, status.Error(codes.Canceled, "client canceled")
   320  			} else if errors.Is(handleErr, context.DeadlineExceeded) {
   321  				return nil, status.Error(codes.DeadlineExceeded, "request timed out")
   322  			} else {
   323  				r.logger.Err(handleErr).Msg("unexpected error handling admin request")
   324  				s, _ := status.FromError(handleErr)
   325  				return nil, s.Err()
   326  			}
   327  		}
   328  	} else {
   329  		return nil, status.Error(codes.Unimplemented, "invalid command")
   330  	}
   331  
   332  	return handleResult, nil
   333  }
   334  
   335  func (r *CommandRunner) GrpcAddress() string {
   336  	return r.grpcAddress
   337  }