From c5015c8e2782bd496ca631f1655e6a1b38f09cae Mon Sep 17 00:00:00 2001 From: Philemon Ukane Date: Fri, 21 Feb 2025 09:16:42 +0100 Subject: [PATCH 1/3] handle dcrd disconnect properly Signed-off-by: Philemon Ukane --- cmd/dcrdata/internal/api/apiroutes.go | 21 ++++++++-- cmd/dcrdata/internal/explorer/explorer.go | 10 ++++- .../internal/explorer/explorermiddleware.go | 39 +++++++++++++------ .../internal/explorer/explorerroutes.go | 12 +++++- cmd/dcrdata/main.go | 17 ++++---- cmd/dcrdata/views/extras.tmpl | 6 +++ 6 files changed, 80 insertions(+), 25 deletions(-) diff --git a/cmd/dcrdata/internal/api/apiroutes.go b/cmd/dcrdata/internal/api/apiroutes.go index 5dd518871..5cfe98875 100644 --- a/cmd/dcrdata/internal/api/apiroutes.go +++ b/cmd/dcrdata/internal/api/apiroutes.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2022, The Decred developers +// Copyright (c) 2018-2025, The Decred developers // Copyright (c) 2017, The dcrdata developers // See LICENSE for details. @@ -46,6 +46,11 @@ import ( // once. const maxBlockRangeCount = 1000 +// noConnectionError is the error message returned by updateNodeConnections when +// there are no connection to a dcrd node. This error means dcrdata has to be +// restarted because auto reconnect has been disabled at the time of writing. +var noConnectionError = errors.New("failed to get connection count") + // DataSource specifies an interface for advanced data collection using the // auxiliary DB (e.g. PostgreSQL). type DataSource interface { @@ -169,9 +174,10 @@ func NewContext(cfg *AppContextConfig) *appContext { func (c *appContext) updateNodeConnections() error { nodeConnections, err := c.nodeClient.GetConnectionCount(context.TODO()) if err != nil { - // Assume there arr no connections if RPC had an error. + // Assume there are no connections if RPC had an error. + c.Status.SetReady(false) c.Status.SetConnections(0) - return fmt.Errorf("failed to get connection count: %v", err) + return fmt.Errorf("%w: %v", noConnectionError, err) } // Before updating connections, get the previous connection count. @@ -223,7 +229,14 @@ out: case <-rpcCheckTicker.C: if err := c.updateNodeConnections(); err != nil { log.Warn("updateNodeConnections: ", err) - break keepon + + if !errors.Is(err, noConnectionError) { + break keepon + } + + log.Warn("Exiting block connected handler for STATUS monitor.") + rpcCheckTicker.Stop() + break out } case height, ok := <-wireHeightChan: diff --git a/cmd/dcrdata/internal/explorer/explorer.go b/cmd/dcrdata/internal/explorer/explorer.go index 660338544..91e98f7aa 100644 --- a/cmd/dcrdata/internal/explorer/explorer.go +++ b/cmd/dcrdata/internal/explorer/explorer.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2024, The Decred developers +// Copyright (c) 2018-2025, The Decred developers // Copyright (c) 2017, The dcrdata developers // See LICENSE for details. @@ -28,6 +28,7 @@ import ( "github.com/decred/dcrdata/exchanges/v3" "github.com/decred/dcrdata/gov/v6/agendas" pitypes "github.com/decred/dcrdata/gov/v6/politeia/types" + apitypes "github.com/decred/dcrdata/v8/api/types" "github.com/decred/dcrdata/v8/blockdata" "github.com/decred/dcrdata/v8/db/dbtypes" "github.com/decred/dcrdata/v8/explorer/types" @@ -228,6 +229,7 @@ type explorerUI struct { invsMtx sync.RWMutex invs *types.MempoolInfo premine int64 + status *apitypes.Status } // AreDBsSyncing is a thread-safe way to fetch the boolean in dbsSyncing. @@ -276,6 +278,12 @@ func (exp *explorerUI) StopWebsocketHub() { close(exp.xcDone) } +// SetStatus updates exp.status and MUST not be used in a goroutine to avoid +// data races. +func (exp *explorerUI) SetStatus(status *apitypes.Status) { + exp.status = status +} + // ExplorerConfig is the configuration settings for explorerUI. type ExplorerConfig struct { DataSource explorerDataSource diff --git a/cmd/dcrdata/internal/explorer/explorermiddleware.go b/cmd/dcrdata/internal/explorer/explorermiddleware.go index a5a071650..fff437a9f 100644 --- a/cmd/dcrdata/internal/explorer/explorermiddleware.go +++ b/cmd/dcrdata/internal/explorer/explorermiddleware.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2021, The Decred developers +// Copyright (c) 2018-2025, The Decred developers // Copyright (c) 2017, The dcrdata developers // See LICENSE for details. @@ -146,39 +146,54 @@ func (exp *explorerUI) BlockHashPathOrIndexCtx(next http.Handler) http.Handler { }) } -// SyncStatusPageIntercept serves only the syncing status page until it is -// deactivated when ShowingSyncStatusPage is set to false. This page is served -// for all the possible routes supported until the background syncing is done. -func (exp *explorerUI) SyncStatusPageIntercept(next http.Handler) http.Handler { +// StatusPageIntercept serves the syncing status page when +// exp.ShowingSyncStatusPage is set to true until when exp.ShowingSyncStatusPage +// is set to false. This page is served for all the possible routes supported +// until the background syncing is done. If exp.Ready is false, an error +// StatusPage is served. +func (exp *explorerUI) StatusPageIntercept(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if exp.ShowingSyncStatusPage() { exp.StatusPage(w, "Database Update Running. Please Wait.", "Blockchain sync is running. Please wait.", "", ExpStatusSyncing) return } + + if !exp.status.Ready() { + exp.StatusPage(w, defaultErrorCode, "Uh Oh. Something unexpected happened, try again later. If you see this error message, please reach out to us via matrix or other communication channel.", "", ExpStatusError) + return + } + // Otherwise, proceed to the next http handler. next.ServeHTTP(w, r) }) } -// SyncStatusAPIIntercept returns a json response back instead of a web page -// when display sync status is active for the api endpoints supported. -func (exp *explorerUI) SyncStatusAPIIntercept(next http.Handler) http.Handler { +// APIStatusIntercept returns a json response back instead of a web page for the +// api endpoints supported when display sync status is active or explore is not +// ready. +func (exp *explorerUI) APIStatusIntercept(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if exp.ShowingSyncStatusPage() { exp.HandleApiRequestsOnSync(w, r) return } + + if !exp.status.Ready() { + exp.HandleAPiRequestWhenNotReady(w) + return + } + // Otherwise, proceed to the next http handler. next.ServeHTTP(w, r) }) } -// SyncStatusFileIntercept triggers an HTTP error if a file is requested for -// download before the DB is synced. -func (exp *explorerUI) SyncStatusFileIntercept(next http.Handler) http.Handler { +// ExplorerStatusFileIntercept triggers an HTTP error if a file is requested for +// download before the DB is synced or when explorer is not ready. +func (exp *explorerUI) ExplorerStatusFileIntercept(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if exp.ShowingSyncStatusPage() { + if exp.ShowingSyncStatusPage() || !exp.status.Ready() { http.Error(w, http.StatusText(http.StatusServiceUnavailable), http.StatusServiceUnavailable) return } diff --git a/cmd/dcrdata/internal/explorer/explorerroutes.go b/cmd/dcrdata/internal/explorer/explorerroutes.go index 323601806..ce8f194d0 100644 --- a/cmd/dcrdata/internal/explorer/explorerroutes.go +++ b/cmd/dcrdata/internal/explorer/explorerroutes.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2024, The Decred developers +// Copyright (c) 2018-2025, The Decred developers // Copyright (c) 2017, The dcrdata developers // See LICENSE for details. @@ -73,6 +73,7 @@ type CommonPageData struct { BaseURL string // scheme + "://" + "host" Path string RequestURI string // path?query + Ready bool } // FullURL constructs the page's complete URL. @@ -2458,6 +2459,14 @@ func (exp *explorerUI) HandleApiRequestsOnSync(w http.ResponseWriter, r *http.Re io.WriteString(w, str) } +// HandleAPiRequestWhenNotReady handles all API request when the explorer is not +// ready. +func (exp *explorerUI) HandleAPiRequestWhenNotReady(w http.ResponseWriter) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusServiceUnavailable) + io.WriteString(w, `{"message": "Something went wrong, explorer is not ready. Please try again later."}`) +} + // MarketPage is the page handler for the "/market" path. func (exp *explorerUI) MarketPage(w http.ResponseWriter, r *http.Request) { str, err := exp.templates.exec("market", struct { @@ -2519,6 +2528,7 @@ func (exp *explorerUI) commonData(r *http.Request) *CommonPageData { BaseURL: baseURL, Path: r.URL.Path, RequestURI: r.URL.RequestURI(), + Ready: exp.status.Ready() || exp.ShowingSyncStatusPage(), } } diff --git a/cmd/dcrdata/main.go b/cmd/dcrdata/main.go index ca925862d..217dc2186 100644 --- a/cmd/dcrdata/main.go +++ b/cmd/dcrdata/main.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2024, The Decred developers +// Copyright (c) 2018-2025, The Decred developers // Copyright (c) 2017, Jonathan Chappelow // See LICENSE for details. @@ -651,6 +651,10 @@ func _main(ctx context.Context) error { chainDB.SignalHeight(uint32(chainDBHeight)) } + // Set explore status. This will enable tracking wheter or not we are still + // connected to a node. + explore.SetStatus(app.Status) + // Configure the URL path to http handler router for the API. apiMux := api.NewAPIRouter(app, cfg.IndentJSON, cfg.UseRealIP, cfg.CompressAPI) @@ -688,7 +692,7 @@ func _main(ctx context.Context) error { webMux.Use(explorer.AllowedHosts(cfg.AllowedHosts)) } - webMux.With(explore.SyncStatusPageIntercept).Group(func(r chi.Router) { + webMux.With(explore.StatusPageIntercept).Group(func(r chi.Router) { r.Get("/", explore.Home) r.Get("/visualblocks", explore.VisualBlocks) }) @@ -724,9 +728,8 @@ func _main(ctx context.Context) error { webMux.Mount(profPath, http.StripPrefix(profPath, http.DefaultServeMux)) } - // SyncStatusAPIIntercept returns a json response if the sync status page is - // enabled (no the full explorer while syncing). - webMux.With(explore.SyncStatusAPIIntercept).Group(func(r chi.Router) { + // APIStatusIntercept returns a json response if the status page if enabled. + webMux.With(explore.APIStatusIntercept).Group(func(r chi.Router) { // Mount the dcrdata's REST API. r.Mount("/api", apiMux.Mux) // Setup and mount the Insight API. @@ -743,11 +746,11 @@ func _main(ctx context.Context) error { }) // HTTP Error 503 StatusServiceUnavailable for file requests before sync. - webMux.With(explore.SyncStatusFileIntercept).Group(func(r chi.Router) { + webMux.With(explore.ExplorerStatusFileIntercept).Group(func(r chi.Router) { r.Mount("/download", fileMux.Mux) }) - webMux.With(explore.SyncStatusPageIntercept).Group(func(r chi.Router) { + webMux.With(explore.StatusPageIntercept).Group(func(r chi.Router) { r.NotFound(explore.NotFound) r.Mount("/explorer", explore.Mux) // legacy diff --git a/cmd/dcrdata/views/extras.tmpl b/cmd/dcrdata/views/extras.tmpl index 1330ffbc5..c0f2c0abe 100644 --- a/cmd/dcrdata/views/extras.tmpl +++ b/cmd/dcrdata/views/extras.tmpl @@ -176,6 +176,7 @@ rel="noopener noreferrer" >© {{currentYear}} The Decred developers (ISC) + {{if .Ready }} Connecting to WebSocket...
+ {{else}} + + Not ready... + + {{end}}