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 }