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 }