diff --git a/node/http.go b/node/http.go index 84f3d99c48..4226564b0a 100644 --- a/node/http.go +++ b/node/http.go @@ -11,11 +11,13 @@ import ( "strings" "time" + "github.com/NethermindEth/juno/blockchain" "github.com/NethermindEth/juno/db" junogrpc "github.com/NethermindEth/juno/grpc" "github.com/NethermindEth/juno/grpc/gen" "github.com/NethermindEth/juno/jsonrpc" "github.com/NethermindEth/juno/service" + "github.com/NethermindEth/juno/sync" "github.com/NethermindEth/juno/utils" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" @@ -72,7 +74,7 @@ func exactPathServer(path string, handler http.Handler) http.HandlerFunc { } func makeRPCOverHTTP(host string, port uint16, servers map[string]*jsonrpc.Server, - log utils.SimpleLogger, metricsEnabled bool, corsEnabled bool, + httpHandlers map[string]http.HandlerFunc, log utils.SimpleLogger, metricsEnabled bool, corsEnabled bool, ) *httpService { var listener jsonrpc.NewRequestListener if metricsEnabled { @@ -87,6 +89,9 @@ func makeRPCOverHTTP(host string, port uint16, servers map[string]*jsonrpc.Serve } mux.Handle(path, exactPathServer(path, httpHandler)) } + for path, handler := range httpHandlers { + mux.HandleFunc(path, handler) + } var handler http.Handler = mux if corsEnabled { @@ -110,6 +115,7 @@ func makeRPCOverWebsocket(host string, port uint16, servers map[string]*jsonrpc. wsHandler = wsHandler.WithListener(listener) } mux.Handle(path, exactPathServer(path, wsHandler)) + wsPrefixedPath := strings.TrimSuffix("/ws"+path, "/") mux.Handle(wsPrefixedPath, exactPathServer(wsPrefixedPath, wsHandler)) } @@ -179,3 +185,43 @@ func makePPROF(host string, port uint16) *httpService { mux.HandleFunc("/debug/pprof/trace", pprof.Trace) return makeHTTPService(host, port, mux) } + +const SyncBlockRange = 6 + +type readinessHandlers struct { + bcReader blockchain.Reader + syncReader sync.Reader +} + +func NewReadinessHandlers(bcReader blockchain.Reader, syncReader sync.Reader) *readinessHandlers { + return &readinessHandlers{ + bcReader: bcReader, + syncReader: syncReader, + } +} + +func (h *readinessHandlers) HandleReadySync(w http.ResponseWriter, r *http.Request) { + if !h.isSynced() { + w.WriteHeader(http.StatusServiceUnavailable) + return + } + + w.WriteHeader(http.StatusOK) +} + +func (h *readinessHandlers) isSynced() bool { + head, err := h.bcReader.HeadsHeader() + if err != nil { + return false + } + highestBlockHeader := h.syncReader.HighestBlockHeader() + if highestBlockHeader == nil { + return false + } + + if head.Number > highestBlockHeader.Number { + return false + } + + return head.Number+SyncBlockRange >= highestBlockHeader.Number +} diff --git a/node/http_test.go b/node/http_test.go new file mode 100644 index 0000000000..8c6795a0c8 --- /dev/null +++ b/node/http_test.go @@ -0,0 +1,75 @@ +package node_test + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + "github.com/NethermindEth/juno/core" + "github.com/NethermindEth/juno/core/felt" + "github.com/NethermindEth/juno/mocks" + "github.com/NethermindEth/juno/node" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" +) + +func TestHandleReadySync(t *testing.T) { + mockCtrl := gomock.NewController(t) + t.Cleanup(mockCtrl.Finish) + + synchronizer := mocks.NewMockSyncReader(mockCtrl) + mockReader := mocks.NewMockReader(mockCtrl) + readinessHandlers := node.NewReadinessHandlers(mockReader, synchronizer) + ctx := context.Background() + + t.Run("ready and blockNumber outside blockRange to highestBlock", func(t *testing.T) { + blockNum := uint64(2) + highestBlock := blockNum + node.SyncBlockRange + 1 + mockReader.EXPECT().HeadsHeader().Return(&core.Header{Number: blockNum}, nil) + synchronizer.EXPECT().HighestBlockHeader().Return(&core.Header{Number: highestBlock, Hash: new(felt.Felt).SetUint64(highestBlock)}) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, "/ready/sync", http.NoBody) + assert.Nil(t, err) + + rr := httptest.NewRecorder() + + readinessHandlers.HandleReadySync(rr, req) + + assert.Equal(t, http.StatusServiceUnavailable, rr.Code) + }) + + t.Run("ready & blockNumber is larger than highestBlock", func(t *testing.T) { + blockNum := uint64(2) + highestBlock := uint64(1) + + mockReader.EXPECT().HeadsHeader().Return(&core.Header{Number: blockNum}, nil) + synchronizer.EXPECT().HighestBlockHeader().Return(&core.Header{Number: highestBlock, Hash: new(felt.Felt).SetUint64(highestBlock)}) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, "/ready/sync", http.NoBody) + assert.Nil(t, err) + + rr := httptest.NewRecorder() + + readinessHandlers.HandleReadySync(rr, req) + + assert.Equal(t, http.StatusServiceUnavailable, rr.Code) + }) + + t.Run("ready & blockNumber is in blockRange of highestBlock", func(t *testing.T) { + blockNum := uint64(3) + highestBlock := blockNum + node.SyncBlockRange + + mockReader.EXPECT().HeadsHeader().Return(&core.Header{Number: blockNum}, nil) + synchronizer.EXPECT().HighestBlockHeader().Return(&core.Header{Number: highestBlock, Hash: new(felt.Felt).SetUint64(highestBlock)}) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, "/ready/sync", http.NoBody) + assert.Nil(t, err) + + rr := httptest.NewRecorder() + + readinessHandlers.HandleReadySync(rr, req) + + assert.Equal(t, http.StatusOK, rr.Code) + }) +} diff --git a/node/node.go b/node/node.go index 1d61267b35..2b1fa2d5c3 100644 --- a/node/node.go +++ b/node/node.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "net/http" "net/url" "reflect" "runtime" @@ -209,10 +210,15 @@ func New(cfg *Config, version string) (*Node, error) { //nolint:gocyclo,funlen "/rpc" + legacyPath: jsonrpcServerLegacy, } if cfg.HTTP { - services = append(services, makeRPCOverHTTP(cfg.HTTPHost, cfg.HTTPPort, rpcServers, log, cfg.Metrics, cfg.RPCCorsEnable)) + readinessHandlers := NewReadinessHandlers(chain, synchronizer) + httpHandlers := map[string]http.HandlerFunc{ + "/ready/sync": readinessHandlers.HandleReadySync, + } + services = append(services, makeRPCOverHTTP(cfg.HTTPHost, cfg.HTTPPort, rpcServers, httpHandlers, log, cfg.Metrics, cfg.RPCCorsEnable)) } if cfg.Websocket { - services = append(services, makeRPCOverWebsocket(cfg.WebsocketHost, cfg.WebsocketPort, rpcServers, log, cfg.Metrics, cfg.RPCCorsEnable)) + services = append(services, + makeRPCOverWebsocket(cfg.WebsocketHost, cfg.WebsocketPort, rpcServers, log, cfg.Metrics, cfg.RPCCorsEnable)) } var metricsService service.Service if cfg.Metrics {