This commit is contained in:
Eugene 2025-10-15 21:57:21 -07:00 committed by GitHub
commit d2c4256ca6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 67 additions and 13 deletions

View file

@ -188,6 +188,7 @@ var (
utils.AllowUnprotectedTxs, utils.AllowUnprotectedTxs,
utils.BatchRequestLimit, utils.BatchRequestLimit,
utils.BatchResponseMaxSize, utils.BatchResponseMaxSize,
utils.ShutdownDelay,
} }
metricsFlags = []cli.Flag{ metricsFlags = []cli.Flag{

View file

@ -791,6 +791,12 @@ var (
Value: node.DefaultConfig.BatchResponseMaxSize, Value: node.DefaultConfig.BatchResponseMaxSize,
Category: flags.APICategory, Category: flags.APICategory,
} }
ShutdownDelay = &cli.DurationFlag{
Name: "shutdown-delay",
Usage: "Sets a delay between receiving SIGTERM and shutting down the node",
Value: node.DefaultConfig.ShutdownDelay,
Category: flags.APICategory,
}
// Network Settings // Network Settings
MaxPeersFlag = &cli.IntFlag{ MaxPeersFlag = &cli.IntFlag{
@ -1235,6 +1241,10 @@ func setHTTP(ctx *cli.Context, cfg *node.Config) {
if ctx.IsSet(BatchResponseMaxSize.Name) { if ctx.IsSet(BatchResponseMaxSize.Name) {
cfg.BatchResponseMaxSize = ctx.Int(BatchResponseMaxSize.Name) cfg.BatchResponseMaxSize = ctx.Int(BatchResponseMaxSize.Name)
} }
if ctx.IsSet(ShutdownDelay.Name) {
cfg.ShutdownDelay = ctx.Duration(ShutdownDelay.Name)
}
} }
// setGraphQL creates the GraphQL listener interface string from the set // setGraphQL creates the GraphQL listener interface string from the set

View file

@ -24,6 +24,7 @@ import (
"path/filepath" "path/filepath"
"runtime" "runtime"
"strings" "strings"
"time"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
@ -211,6 +212,9 @@ type Config struct {
EnablePersonal bool `toml:"-"` EnablePersonal bool `toml:"-"`
DBEngine string `toml:",omitempty"` DBEngine string `toml:",omitempty"`
// ShutdownDelay is the delay between receiving SIGTERM and shutting down the node.
ShutdownDelay time.Duration `toml:",omitempty"`
} }
// IPCEndpoint resolves an IPC endpoint based on a configured value, taking into // IPCEndpoint resolves an IPC endpoint based on a configured value, taking into

View file

@ -21,6 +21,7 @@ import (
"os/user" "os/user"
"path/filepath" "path/filepath"
"runtime" "runtime"
"time"
"github.com/ethereum/go-ethereum/p2p" "github.com/ethereum/go-ethereum/p2p"
"github.com/ethereum/go-ethereum/p2p/nat" "github.com/ethereum/go-ethereum/p2p/nat"
@ -73,6 +74,7 @@ var DefaultConfig = Config{
NAT: nat.Any(), NAT: nat.Any(),
}, },
DBEngine: "", // Use whatever exists, will default to Pebble if non-existent and supported DBEngine: "", // Use whatever exists, will default to Pebble if non-existent and supported
ShutdownDelay: 0 * time.Second,
} }
// DefaultDataDir is the default data directory to use for the databases and other // DefaultDataDir is the default data directory to use for the databases and other

View file

@ -150,10 +150,10 @@ func New(conf *Config) (*Node, error) {
} }
// Configure RPC servers. // Configure RPC servers.
node.http = newHTTPServer(node.log, conf.HTTPTimeouts) node.http = newHTTPServer(node.log, conf.HTTPTimeouts, conf.ShutdownDelay)
node.httpAuth = newHTTPServer(node.log, conf.HTTPTimeouts) node.httpAuth = newHTTPServer(node.log, conf.HTTPTimeouts, conf.ShutdownDelay)
node.ws = newHTTPServer(node.log, rpc.DefaultHTTPTimeouts) node.ws = newHTTPServer(node.log, rpc.DefaultHTTPTimeouts, conf.ShutdownDelay)
node.wsAuth = newHTTPServer(node.log, rpc.DefaultHTTPTimeouts) node.wsAuth = newHTTPServer(node.log, rpc.DefaultHTTPTimeouts, conf.ShutdownDelay)
node.ipc = newIPCServer(node.log, conf.IPCEndpoint()) node.ipc = newIPCServer(node.log, conf.IPCEndpoint())
return node, nil return node, nil
@ -519,6 +519,12 @@ func (n *Node) stopRPC() {
n.ws.stop() n.ws.stop()
n.httpAuth.stop() n.httpAuth.stop()
n.wsAuth.stop() n.wsAuth.stop()
n.http.wait()
n.ws.wait()
n.httpAuth.wait()
n.wsAuth.wait()
n.ipc.stop() n.ipc.stop()
n.stopInProc() n.stopInProc()
} }

View file

@ -71,12 +71,14 @@ type httpServer struct {
timeouts rpc.HTTPTimeouts timeouts rpc.HTTPTimeouts
mux http.ServeMux // registered handlers go here mux http.ServeMux // registered handlers go here
mu sync.Mutex mu sync.Mutex
server *http.Server server *http.Server
listener net.Listener // non-nil when server is running listener net.Listener // non-nil when server is running
ready bool
shutdownWG sync.WaitGroup // WG to wait for shutdown
shutdownDelay time.Duration
// HTTP RPC handler things. // HTTP RPC handler things.
httpConfig httpConfig httpConfig httpConfig
httpHandler atomic.Pointer[rpcHandler] httpHandler atomic.Pointer[rpcHandler]
@ -96,8 +98,8 @@ const (
shutdownTimeout = 5 * time.Second shutdownTimeout = 5 * time.Second
) )
func newHTTPServer(log log.Logger, timeouts rpc.HTTPTimeouts) *httpServer { func newHTTPServer(log log.Logger, timeouts rpc.HTTPTimeouts, shutdownDelay time.Duration) *httpServer {
h := &httpServer{log: log, timeouts: timeouts, handlerNames: make(map[string]string)} h := &httpServer{log: log, timeouts: timeouts, handlerNames: make(map[string]string), shutdownDelay: shutdownDelay}
return h return h
} }
@ -165,6 +167,7 @@ func (h *httpServer) start() error {
} }
h.log.Info("WebSocket enabled", "url", url) h.log.Info("WebSocket enabled", "url", url)
} }
h.ready = true
// if server is websocket only, return after logging // if server is websocket only, return after logging
if !h.rpcAllowed() { if !h.rpcAllowed() {
return nil return nil
@ -195,6 +198,22 @@ func (h *httpServer) start() error {
} }
func (h *httpServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (h *httpServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// server health probe endpoints
if r.Method == http.MethodGet {
// readiness probe fails during shutdown
if r.URL.Path == "/readyz" {
if h.ready {
w.WriteHeader(http.StatusNoContent)
} else {
w.WriteHeader(http.StatusServiceUnavailable)
}
return
// liveness probe always succeeds
} else if r.URL.Path == "/livez" {
w.WriteHeader(http.StatusNoContent)
return
}
}
// check if ws request and serve if ws enabled // check if ws request and serve if ws enabled
ws := h.wsHandler.Load() ws := h.wsHandler.Load()
if ws != nil && isWebsocket(r) { if ws != nil && isWebsocket(r) {
@ -255,8 +274,20 @@ func validatePrefix(what, path string) error {
// stop shuts down the HTTP server. // stop shuts down the HTTP server.
func (h *httpServer) stop() { func (h *httpServer) stop() {
h.mu.Lock() h.mu.Lock()
defer h.mu.Unlock() // unit test executes stop multiple times, so we cannot increment the WG in the start method
h.doStop() h.shutdownWG = sync.WaitGroup{}
h.shutdownWG.Add(1)
h.ready = false
time.AfterFunc(h.shutdownDelay, func() {
defer h.mu.Unlock()
h.doStop()
h.shutdownWG.Done()
})
}
// wait waits for the server to shutdown.
func (h *httpServer) wait() {
h.shutdownWG.Wait()
} }
func (h *httpServer) doStop() { func (h *httpServer) doStop() {

View file

@ -241,7 +241,7 @@ func createAndStartServer(t *testing.T, conf *httpConfig, ws bool, wsConf *wsCon
if timeouts == nil { if timeouts == nil {
timeouts = &rpc.DefaultHTTPTimeouts timeouts = &rpc.DefaultHTTPTimeouts
} }
srv := newHTTPServer(testlog.Logger(t, log.LvlDebug), *timeouts) srv := newHTTPServer(testlog.Logger(t, log.LvlDebug), *timeouts, 0*time.Second)
assert.NoError(t, srv.enableRPC(apis(), *conf)) assert.NoError(t, srv.enableRPC(apis(), *conf))
if ws { if ws {
assert.NoError(t, srv.enableWS(nil, *wsConf)) assert.NoError(t, srv.enableWS(nil, *wsConf))