Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle dcrd disconnect properly #2012

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 17 additions & 4 deletions cmd/dcrdata/internal/api/apiroutes.go
Original file line number Diff line number Diff line change
@@ -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.

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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:
Expand Down
10 changes: 9 additions & 1 deletion cmd/dcrdata/internal/explorer/explorer.go
Original file line number Diff line number Diff line change
@@ -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.

Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
39 changes: 27 additions & 12 deletions cmd/dcrdata/internal/explorer/explorermiddleware.go
Original file line number Diff line number Diff line change
@@ -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.

Expand Down Expand Up @@ -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
}
Expand Down
12 changes: 11 additions & 1 deletion cmd/dcrdata/internal/explorer/explorerroutes.go
Original file line number Diff line number Diff line change
@@ -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.

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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(),
}
}

Expand Down
3 changes: 3 additions & 0 deletions cmd/dcrdata/internal/explorer/explorerroutes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

"github.com/decred/dcrd/chaincfg/v3"
"github.com/decred/dcrdata/db/dcrpg/v8"
apitypes "github.com/decred/dcrdata/v8/api/types"
"github.com/decred/dcrdata/v8/explorer/types"
)

Expand Down Expand Up @@ -71,6 +72,8 @@ func TestStatusPageResponseCodes(t *testing.T) {
TestnetLink: "/",
})

exp.SetStatus(new(apitypes.Status))

// handler := http.HandlerFunc()
// handler.ServeHTTP(rr, req)

Expand Down
8 changes: 4 additions & 4 deletions cmd/dcrdata/internal/explorer/templates.go
Original file line number Diff line number Diff line change
@@ -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.

Expand Down Expand Up @@ -78,14 +78,14 @@ func (t *templates) reloadTemplates() error {
if errorStrings == nil {
return nil
}
return fmt.Errorf(strings.Join(errorStrings, " | "))
return fmt.Errorf("%s", strings.Join(errorStrings, " | "))
}

// execTemplateToString executes the associated input template using the
// supplied data, and writes the result into a string. If the template fails to
// execute or isn't found, a non-nil error will be returned. Check it before
// writing to theclient, otherwise you might as well execute directly into
// your response writer instead of the internal buffer of this function.
// writing to the client, otherwise you might as well execute directly into your
// response writer instead of the internal buffer of this function.
func (t *templates) execTemplateToString(name string, data interface{}) (string, error) {
temp, ok := t.templates[name]
if !ok {
Expand Down
17 changes: 10 additions & 7 deletions cmd/dcrdata/main.go
Original file line number Diff line number Diff line change
@@ -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.

Expand Down Expand Up @@ -651,6 +651,10 @@ func _main(ctx context.Context) error {
chainDB.SignalHeight(uint32(chainDBHeight))
}

// Set explore status. This will enable tracking whether 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)

Expand Down Expand Up @@ -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)
})
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down
6 changes: 6 additions & 0 deletions cmd/dcrdata/views/extras.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@
rel="noopener noreferrer"
>© {{currentYear}} The Decred developers (ISC)</a>
</span>
{{if .Ready }}
<span
id="connection"
class="text-nowrap align-items-center clickable d-inline-block"
Expand All @@ -186,6 +187,11 @@
title="While connected, you will receive live page updates and, if enabled, desktop notifications (click to enable)."
><span class="d-none d-sm-inline" data-connection-target="status">Connecting <span class="d-none d-md-inline-block">to WebSocket...</span></span><div></div>
</span>
{{else}}
<span class="text-nowrap align-items-center d-inline-block">
Not ready...
</span>
{{end}}
</div>
<script
src="/dist/js/4.06b06a4759f2b4ad.bundle.js"
Expand Down