github.com/koko1123/flow-go-1@v0.29.6/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/koko1123/flow-go-1/admin/admin"
    23  	"github.com/koko1123/flow-go-1/module/component"
    24  	"github.com/koko1123/flow-go-1/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  	r.RegisterHandler("list-commands", func(ctx context.Context, req *CommandRequest) (interface{}, error) {
    80  		return commands, nil
    81  	})
    82  	for command, handler := range r.handlers {
    83  		handlers[command] = handler
    84  		commands = append(commands, command)
    85  	}
    86  
    87  	validators := make(map[string]CommandValidator)
    88  	for command, validator := range r.validators {
    89  		validators[command] = validator
    90  	}
    91  
    92  	commandRunner := &CommandRunner{
    93  		handlers:         handlers,
    94  		validators:       validators,
    95  		grpcAddress:      fmt.Sprintf("%s/flow-node-admin.sock", os.TempDir()),
    96  		httpAddress:      bindAddress,
    97  		logger:           logger.With().Str("admin", "command_runner").Logger(),
    98  		startupCompleted: make(chan struct{}),
    99  	}
   100  
   101  	for _, opt := range opts {
   102  		opt(commandRunner)
   103  	}
   104  
   105  	return commandRunner
   106  }
   107  
   108  func (r *CommandRunnerBootstrapper) RegisterHandler(command string, handler CommandHandler) bool {
   109  	if _, ok := r.handlers[command]; ok {
   110  		return false
   111  	}
   112  	r.handlers[command] = handler
   113  	return true
   114  }
   115  
   116  func (r *CommandRunnerBootstrapper) RegisterValidator(command string, validator CommandValidator) bool {
   117  	if _, ok := r.validators[command]; ok {
   118  		return false
   119  	}
   120  	r.validators[command] = validator
   121  	return true
   122  }
   123  
   124  type CommandRunner struct {
   125  	handlers    map[string]CommandHandler
   126  	validators  map[string]CommandValidator
   127  	grpcAddress string
   128  	httpAddress string
   129  	maxMsgSize  int
   130  	tlsConfig   *tls.Config
   131  	logger      zerolog.Logger
   132  
   133  	// wait for worker routines to be ready
   134  	workersStarted sync.WaitGroup
   135  
   136  	// wait for worker routines to exit
   137  	workersFinished sync.WaitGroup
   138  
   139  	// signals startup completion
   140  	startupCompleted chan struct{}
   141  }
   142  
   143  func (r *CommandRunner) getHandler(command string) CommandHandler {
   144  	return r.handlers[command]
   145  }
   146  
   147  func (r *CommandRunner) getValidator(command string) CommandValidator {
   148  	return r.validators[command]
   149  }
   150  
   151  func (r *CommandRunner) Start(ctx irrecoverable.SignalerContext) {
   152  	if err := r.runAdminServer(ctx); err != nil {
   153  		ctx.Throw(fmt.Errorf("failed to start admin server: %w", err))
   154  	}
   155  
   156  	close(r.startupCompleted)
   157  }
   158  
   159  func (r *CommandRunner) Ready() <-chan struct{} {
   160  	ready := make(chan struct{})
   161  
   162  	go func() {
   163  		<-r.startupCompleted
   164  		r.workersStarted.Wait()
   165  		close(ready)
   166  	}()
   167  
   168  	return ready
   169  }
   170  
   171  func (r *CommandRunner) Done() <-chan struct{} {
   172  	done := make(chan struct{})
   173  
   174  	go func() {
   175  		<-r.startupCompleted
   176  		r.workersFinished.Wait()
   177  		close(done)
   178  	}()
   179  
   180  	return done
   181  }
   182  
   183  func (r *CommandRunner) runAdminServer(ctx irrecoverable.SignalerContext) error {
   184  	select {
   185  	case <-ctx.Done():
   186  		return ctx.Err()
   187  	default:
   188  	}
   189  
   190  	r.logger.Info().Msg("admin server starting up")
   191  
   192  	listener, err := net.Listen("unix", r.grpcAddress)
   193  	if err != nil {
   194  		return fmt.Errorf("failed to listen on admin server address: %w", err)
   195  	}
   196  
   197  	opts := []grpc.ServerOption{
   198  		grpc.MaxRecvMsgSize(r.maxMsgSize),
   199  		grpc.MaxSendMsgSize(r.maxMsgSize),
   200  	}
   201  
   202  	grpcServer := grpc.NewServer(opts...)
   203  	pb.RegisterAdminServer(grpcServer, NewAdminServer(r))
   204  
   205  	r.workersStarted.Add(1)
   206  	r.workersFinished.Add(1)
   207  	go func() {
   208  		defer r.workersFinished.Done()
   209  		r.workersStarted.Done()
   210  
   211  		if err := grpcServer.Serve(listener); err != nil {
   212  			r.logger.Err(err).Msg("gRPC server encountered fatal error")
   213  			ctx.Throw(err)
   214  		}
   215  	}()
   216  
   217  	// Initialize gRPC and HTTP muxers
   218  	gwmux := runtime.NewServeMux()
   219  	dialOpts := []grpc.DialOption{
   220  		grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(r.maxMsgSize)),
   221  		grpc.WithTransportCredentials(insecure.NewCredentials()),
   222  	}
   223  	err = pb.RegisterAdminHandlerFromEndpoint(ctx, gwmux, "unix:///"+r.grpcAddress, dialOpts)
   224  	if err != nil {
   225  		return fmt.Errorf("failed to register http handlers for admin service: %w", err)
   226  	}
   227  
   228  	mux := http.NewServeMux()
   229  	mux.Handle("/", gwmux)
   230  
   231  	// This adds an ability to use standard go tooling for performance troubleshooting e.g.:
   232  	//  go tool pprof http://localhost:9002/debug/pprof/goroutine
   233  	for _, name := range []string{"allocs", "block", "goroutine", "heap", "mutex", "threadcreate"} {
   234  		mux.HandleFunc(fmt.Sprintf("/debug/pprof/%s", name), pprof.Handler(name).ServeHTTP)
   235  	}
   236  	mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
   237  
   238  	httpServer := &http.Server{
   239  		Addr:      r.httpAddress,
   240  		Handler:   mux,
   241  		TLSConfig: r.tlsConfig,
   242  	}
   243  
   244  	r.workersStarted.Add(1)
   245  	r.workersFinished.Add(1)
   246  	go func() {
   247  		defer r.workersFinished.Done()
   248  		r.workersStarted.Done()
   249  
   250  		// Start HTTP server (and proxy calls to gRPC server endpoint)
   251  		var err error
   252  		if r.tlsConfig == nil {
   253  			err = httpServer.ListenAndServe()
   254  		} else {
   255  			err = httpServer.ListenAndServeTLS("", "")
   256  		}
   257  
   258  		if err != nil && !errors.Is(err, http.ErrServerClosed) {
   259  			r.logger.Err(err).Msg("HTTP server encountered error")
   260  			ctx.Throw(err)
   261  		}
   262  	}()
   263  
   264  	r.workersStarted.Add(1)
   265  	r.workersFinished.Add(1)
   266  	go func() {
   267  		defer r.workersFinished.Done()
   268  		r.workersStarted.Done()
   269  
   270  		<-ctx.Done()
   271  		r.logger.Info().Msg("admin server shutting down")
   272  
   273  		grpcServer.Stop()
   274  
   275  		if httpServer != nil {
   276  			shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), CommandRunnerShutdownTimeout)
   277  			defer shutdownCancel()
   278  
   279  			if err := httpServer.Shutdown(shutdownCtx); err != nil {
   280  				r.logger.Err(err).Msg("failed to shutdown http server")
   281  				ctx.Throw(err)
   282  			}
   283  		}
   284  	}()
   285  
   286  	return nil
   287  }
   288  
   289  func (r *CommandRunner) runCommand(ctx context.Context, command string, data interface{}) (interface{}, error) {
   290  	r.logger.Info().Str("command", command).Msg("received new command")
   291  
   292  	req := &CommandRequest{Data: data}
   293  
   294  	if validator := r.getValidator(command); validator != nil {
   295  		if validationErr := validator(req); validationErr != nil {
   296  			// for expected validation errors, return code InvalidArgument and the error text
   297  			if IsInvalidAdminParameterError(validationErr) {
   298  				return nil, status.Error(codes.InvalidArgument, validationErr.Error())
   299  			}
   300  			// for unexpected errors, return code Internal and log a warning
   301  			r.logger.Err(validationErr).Msg("unexpected error validating admin request")
   302  			return nil, status.Error(codes.Internal, validationErr.Error())
   303  		}
   304  	}
   305  
   306  	var handleResult interface{}
   307  	var handleErr error
   308  
   309  	if handler := r.getHandler(command); handler != nil {
   310  		if handleResult, handleErr = handler(ctx, req); handleErr != nil {
   311  			if errors.Is(handleErr, context.Canceled) {
   312  				return nil, status.Error(codes.Canceled, "client canceled")
   313  			} else if errors.Is(handleErr, context.DeadlineExceeded) {
   314  				return nil, status.Error(codes.DeadlineExceeded, "request timed out")
   315  			} else {
   316  				r.logger.Err(handleErr).Msg("unexpected error handling admin request")
   317  				s, _ := status.FromError(handleErr)
   318  				return nil, s.Err()
   319  			}
   320  		}
   321  	} else {
   322  		return nil, status.Error(codes.Unimplemented, "invalid command")
   323  	}
   324  
   325  	return handleResult, nil
   326  }