Skip to content

Commit 3e81b7d

Browse files
cmd/dcrdata: add cache headers to improve page load on select pages (#2030)
* cmd/dcrdata: add cache headers to improve page load on select pages This PR adds the ETag, Last-Modified and Cache-Control headers on select explorer changes to improve user experience and save resources. The ETag and Last-Modified are reset when *explorerUI receives a new block or mempool data. In the future, they might be reset in other call sites where update to backend cached data are made and API endpoints or pages depending on cached data should then use the ETagAndLastModifiedIntercept middleware. Signed-off-by: Philemon Ukane <ukanephilemon@gmail.com> * format main.go Signed-off-by: Philemon Ukane <ukanephilemon@gmail.com> --------- Signed-off-by: Philemon Ukane <ukanephilemon@gmail.com>
1 parent 8a05e79 commit 3e81b7d

File tree

3 files changed

+80
-9
lines changed

3 files changed

+80
-9
lines changed

cmd/dcrdata/internal/explorer/explorer.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ package explorer
88

99
import (
1010
"context"
11+
"crypto/rand"
1112
"fmt"
1213
"math"
14+
"math/big"
1315
"net/http"
1416
"os"
1517
"os/signal"
@@ -201,6 +203,8 @@ type pageData struct {
201203
BlockInfo *types.BlockInfo
202204
BlockchainInfo *chainjson.GetBlockChainInfoResult
203205
HomeInfo *types.HomeInfo
206+
eTag string
207+
lastModified time.Time
204208
}
205209

206210
type explorerUI struct {
@@ -359,6 +363,7 @@ func New(cfg *ExplorerConfig) *explorerUI {
359363
},
360364
},
361365
}
366+
exp.resetETagAndLastModified()
362367

363368
log.Infof("Mean Voting Blocks calculated: %d", exp.pageData.HomeInfo.Params.MeanVotingBlocks)
364369

@@ -447,6 +452,9 @@ func (exp *explorerUI) StoreMPData(_ *mempool.StakeData, _ []types.MempoolTx, in
447452
exp.invsMtx.Lock()
448453
exp.invs = inv
449454
exp.invsMtx.Unlock()
455+
456+
exp.resetETagAndLastModified()
457+
450458
log.Debugf("Updated mempool details for the explorerUI.")
451459
}
452460

@@ -616,6 +624,8 @@ func (exp *explorerUI) Store(blockData *blockdata.BlockData, msgBlock *wire.MsgB
616624
}()
617625
}
618626

627+
exp.resetETagAndLastModified()
628+
619629
return nil
620630
}
621631

@@ -629,6 +639,21 @@ func (exp *explorerUI) ChartsUpdated() {
629639
exp.pageData.Unlock()
630640
}
631641

642+
// resetETagAndLastModified resets the eTag and last modified time to new
643+
// values and is protected by the exp.pageData mutex.
644+
func (exp *explorerUI) resetETagAndLastModified() {
645+
exp.pageData.Lock()
646+
exp.pageData.eTag = generateRandomString()
647+
exp.pageData.lastModified = time.Now()
648+
exp.pageData.Unlock()
649+
}
650+
651+
func (exp *explorerUI) eTagAndLastModified() (eTag string, lastModified time.Time) {
652+
exp.pageData.RLock()
653+
defer exp.pageData.RUnlock()
654+
return exp.pageData.eTag, exp.pageData.lastModified
655+
}
656+
632657
func (exp *explorerUI) updateDevFundBalance() {
633658
// yield processor to other goroutines
634659
runtime.Gosched()
@@ -879,3 +904,16 @@ func indexPrice(index exchanges.CurrencyPair, indices map[string]map[exchanges.C
879904
}
880905
return price / nSources
881906
}
907+
908+
const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
909+
const nLetters = len(letters)
910+
911+
// generateRandomString creates a random alphanumeric string of length 16.
912+
func generateRandomString() string {
913+
bytes := make([]byte, 16)
914+
for i := range bytes {
915+
num, _ := rand.Int(rand.Reader, big.NewInt(int64(nLetters)))
916+
bytes[i] = letters[num.Int64()]
917+
}
918+
return string(bytes)
919+
}

cmd/dcrdata/internal/explorer/explorermiddleware.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,33 @@ func (exp *explorerUI) SyncStatusFileIntercept(next http.Handler) http.Handler {
189189
})
190190
}
191191

192+
// ETagAndLastModifiedIntercept handles ETag and Last-Modified headers for caching purposes.
193+
func (exp *explorerUI) ETagAndLastModifiedIntercept(next http.Handler) http.Handler {
194+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
195+
eTag, lastModified := exp.eTagAndLastModified()
196+
if match := r.Header.Get("If-None-Match"); match != "" {
197+
if match == eTag {
198+
w.WriteHeader(http.StatusNotModified)
199+
return
200+
}
201+
} else if modifiedSince := r.Header.Get("If-Modified-Since"); modifiedSince != "" {
202+
if t, err := time.Parse(http.TimeFormat, modifiedSince); err == nil {
203+
if lastModified.Before(t.Add(1 * time.Second)) {
204+
w.WriteHeader(http.StatusNotModified)
205+
return
206+
}
207+
}
208+
}
209+
210+
// Set ETag and Last-Modified headers.
211+
w.Header().Set("ETag", eTag)
212+
w.Header().Set("Last-Modified", lastModified.Format(http.TimeFormat))
213+
w.Header().Set("Cache-Control", "private")
214+
215+
next.ServeHTTP(w, r)
216+
})
217+
}
218+
192219
func getBlockHashCtx(r *http.Request) string {
193220
hash, ok := r.Context().Value(ctxBlockHash).(string)
194221
if !ok {

cmd/dcrdata/main.go

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -761,23 +761,15 @@ func _main(ctx context.Context) error {
761761
r.Get("/rejects", func(w http.ResponseWriter, r *http.Request) {
762762
http.Redirect(w, r, "/disapproved", http.StatusPermanentRedirect)
763763
})
764-
r.Get("/disapproved", explore.DisapprovedBlocks)
765-
r.Get("/mempool", explore.Mempool)
766-
r.Get("/parameters", explore.ParametersPage)
767764
r.With(explore.BlockHashPathOrIndexCtx).Get("/block/{blockhash}", explore.Block)
768765
r.With(explorer.TransactionHashCtx).Get("/tx/{txid}", explore.TxPage)
769766
r.With(explorer.TransactionHashCtx, explorer.TransactionIoIndexCtx).Get("/tx/{txid}/{inout}/{inoutid}", explore.TxPage)
770767
r.With(explorer.AddressPathCtx).Get("/address/{address}", explore.AddressPage)
771768
r.With(explorer.AddressPathCtx).Get("/addresstable/{address}", explore.AddressTable)
772-
r.Get("/treasury", explore.TreasuryPage)
773-
r.Get("/treasurytable", explore.TreasuryTable)
774-
r.Get("/agendas", explore.AgendasPage)
775-
r.With(explorer.AgendaPathCtx).Get("/agenda/{agendaid}", explore.AgendaPage)
776769
r.Get("/proposals", explore.ProposalsPage)
777770
r.With(explorer.ProposalPathCtx).Get("/proposal/{proposaltoken}", explore.ProposalPage)
778771
r.Get("/decodetx", explore.DecodeTxPage)
779772
r.Get("/search", explore.Search)
780-
r.Get("/charts", explore.Charts)
781773
r.Get("/ticketpool", explore.Ticketpool)
782774
r.Get("/market", explore.MarketPage)
783775
r.Get("/stats", func(w http.ResponseWriter, r *http.Request) {
@@ -786,9 +778,23 @@ func _main(ctx context.Context) error {
786778
// MenuFormParser will typically redirect, but going to the homepage as a
787779
// fallback.
788780
r.With(explorer.MenuFormParser).Post("/set", explore.Home)
789-
r.Get("/attack-cost", explore.AttackCost)
790781
r.Get("/verify-message", explore.VerifyMessagePage)
791782
r.With(mw.Tollbooth(limiter)).Post("/verify-message", explore.VerifyMessageHandler)
783+
784+
// Pages that can be cached because they depend on block and/or mempool data cached by
785+
// *explorer.explorerUI. This middleware sets ETag and Last-Modified headers that are
786+
// reset if a new block or mempool change is detected.
787+
withCache := r.With(explore.ETagAndLastModifiedIntercept)
788+
withCache.Get("/", explore.Home)
789+
withCache.Get("/disapproved", explore.DisapprovedBlocks)
790+
withCache.Get("/mempool", explore.Mempool)
791+
withCache.Get("/charts", explore.Charts)
792+
withCache.Get("/treasury", explore.TreasuryPage)
793+
withCache.Get("/treasurytable", explore.TreasuryTable)
794+
withCache.Get("/parameters", explore.ParametersPage)
795+
withCache.Get("/agendas", explore.AgendasPage)
796+
withCache.With(explorer.AgendaPathCtx).Get("/agenda/{agendaid}", explore.AgendaPage)
797+
withCache.Get("/attack-cost", explore.AttackCost)
792798
})
793799

794800
// Configure a page for the bare "/insight" path. This mounts the static

0 commit comments

Comments
 (0)