Refactor gui cache

* Remove static cfg values from GUI cache.

Theres no need for these values to be copied into the cache when templates could simply access the config struct directly.

This also changes the cache accessor so it returns a copy of the cache rather than a pointer, which removes a potential race.

* Rename and move cache.

Cache code was previous in `homepage.go`, but its used in multiple places and not just on the homepage. Its enough code to go into its own dedicated `cache.go`.
This commit is contained in:
Jamie Holdstock 2021-06-12 09:54:53 +08:00 committed by GitHub
parent d1a838bf7f
commit fc131e926d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 125 additions and 129 deletions

View File

@ -1,4 +1,4 @@
// Copyright (c) 2020 The Decred developers
// Copyright (c) 2020-2021 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
@ -97,7 +97,8 @@ func statusJSON(c *gin.Context) {
// adminPage is the handler for "GET /admin".
func adminPage(c *gin.Context) {
c.HTML(http.StatusOK, "admin.html", gin.H{
"VspStats": getVSPStats(),
"WebApiCache": getCache(),
"WebApiCfg": cfg,
"WalletStatus": walletStatus(c),
})
}
@ -129,7 +130,8 @@ func ticketSearch(c *gin.Context) {
VoteChanges: voteChanges,
MaxVoteChanges: cfg.MaxVoteChangeRecords,
},
"VspStats": getVSPStats(),
"WebApiCache": getCache(),
"WebApiCfg": cfg,
"WalletStatus": walletStatus(c),
})
}
@ -142,7 +144,8 @@ func adminLogin(c *gin.Context) {
if password != cfg.AdminPass {
log.Warnf("Failed login attempt from %s", c.ClientIP())
c.HTML(http.StatusUnauthorized, "login.html", gin.H{
"VspStats": getVSPStats(),
"WebApiCache": getCache(),
"WebApiCfg": cfg,
"IncorrectPassword": true,
})
return

86
webapi/cache.go Normal file
View File

@ -0,0 +1,86 @@
// Copyright (c) 2020-2021 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package webapi
import (
"context"
"encoding/base64"
"sync"
"time"
"github.com/decred/dcrd/chaincfg/v3"
"github.com/decred/vspd/database"
"github.com/decred/vspd/rpc"
)
// apiCache is used to cache values which are commonly used by the API, so
// repeated web requests don't repeatedly trigger DB or RPC calls.
type apiCache struct {
UpdateTime string
PubKey string
Voting int64
Voted int64
Revoked int64
BlockHeight uint32
NetworkProportion float32
RevokedProportion float32
}
var cacheMtx sync.RWMutex
var cache apiCache
func getCache() apiCache {
cacheMtx.RLock()
defer cacheMtx.RUnlock()
return cache
}
// initCache creates the struct which holds the cached VSP stats, and
// initializes it with static values.
func initCache() {
cacheMtx.Lock()
defer cacheMtx.Unlock()
cache = apiCache{
PubKey: base64.StdEncoding.EncodeToString(signPubKey),
}
}
// updateCache updates the dynamic values in the cache (ticket counts and best
// block height).
func updateCache(ctx context.Context, db *database.VspDatabase,
dcrd rpc.DcrdConnect, netParams *chaincfg.Params) error {
// Get latest counts of voting, voted and revoked tickets.
voting, voted, revoked, err := db.CountTickets()
if err != nil {
return err
}
// Get latest best block height.
dcrdClient, err := dcrd.Client(ctx, netParams)
if err != nil {
return err
}
bestBlock, err := dcrdClient.GetBestBlockHeader()
if err != nil {
return err
}
cacheMtx.Lock()
defer cacheMtx.Unlock()
cache.UpdateTime = dateTime(time.Now().Unix())
cache.Voting = voting
cache.Voted = voted
cache.Revoked = revoked
cache.BlockHeight = bestBlock.Height
cache.NetworkProportion = float32(voting) / float32(bestBlock.PoolSize)
cache.RevokedProportion = float32(revoked) / float32(voted)
return nil
}

View File

@ -1,112 +1,18 @@
// Copyright (c) 2020 The Decred developers
// Copyright (c) 2020-2021 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package webapi
import (
"context"
"encoding/base64"
"net/http"
"sync"
"time"
"github.com/decred/dcrd/chaincfg/v3"
"github.com/decred/vspd/database"
"github.com/decred/vspd/rpc"
"github.com/gin-gonic/gin"
)
// vspStats is used to cache values which are commonly used by the API, so
// repeated web requests don't repeatedly trigger DB or RPC calls.
type vspStats struct {
PubKey string
Voting int64
Voted int64
Revoked int64
VSPFee float64
Network string
UpdateTime string
SupportEmail string
VspClosed bool
VspClosedMsg string
Debug bool
Designation string
BlockHeight uint32
NetworkProportion float32
RevokedProportion float32
VspdVersion string
}
var statsMtx sync.RWMutex
var stats *vspStats
func getVSPStats() *vspStats {
statsMtx.RLock()
defer statsMtx.RUnlock()
return stats
}
// initVSPStats creates the struct which holds the cached VSP stats, and
// initializes it with static values.
func initVSPStats() {
statsMtx.Lock()
defer statsMtx.Unlock()
stats = &vspStats{
PubKey: base64.StdEncoding.EncodeToString(signPubKey),
VSPFee: cfg.VSPFee,
Network: cfg.NetParams.Name,
SupportEmail: cfg.SupportEmail,
VspClosed: cfg.VspClosed,
VspClosedMsg: cfg.VspClosedMsg,
Debug: cfg.Debug,
Designation: cfg.Designation,
VspdVersion: cfg.VspdVersion,
}
}
// updateVSPStats updates the dynamic values in the cached VSP stats (ticket
// counts and best block height).
func updateVSPStats(ctx context.Context, db *database.VspDatabase,
dcrd rpc.DcrdConnect, netParams *chaincfg.Params) error {
// Update counts of voting, voted and revoked tickets.
voting, voted, revoked, err := db.CountTickets()
if err != nil {
return err
}
// Update best block height.
dcrdClient, err := dcrd.Client(ctx, netParams)
if err != nil {
return err
}
bestBlock, err := dcrdClient.GetBestBlockHeader()
if err != nil {
return err
}
statsMtx.Lock()
defer statsMtx.Unlock()
stats.UpdateTime = dateTime(time.Now().Unix())
stats.Voting = voting
stats.Voted = voted
stats.Revoked = revoked
stats.BlockHeight = bestBlock.Height
stats.NetworkProportion = float32(voting) / float32(bestBlock.PoolSize)
stats.RevokedProportion = float32(revoked) / float32(voted)
return nil
}
func homepage(c *gin.Context) {
c.HTML(http.StatusOK, "homepage.html", gin.H{
"VspStats": getVSPStats(),
"WebApiCache": getCache(),
"WebApiCfg": cfg,
})
}

View File

@ -1,4 +1,4 @@
// Copyright (c) 2020 The Decred developers
// Copyright (c) 2020-2021 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
@ -62,7 +62,8 @@ func requireAdmin() gin.HandlerFunc {
if admin == nil {
c.HTML(http.StatusUnauthorized, "login.html", gin.H{
"VspStats": getVSPStats(),
"WebApiCache": getCache(),
"WebApiCfg": cfg,
})
c.Abort()
return

View File

@ -16,7 +16,7 @@
</div>
{{ template "vsp-stats" .VspStats }}
{{ template "vsp-stats" . }}
</div>
</div>

View File

@ -5,11 +5,11 @@
<footer class="row m-0">
<div class="col-md-8 col-12 d-flex justify-content-center align-items-center">
<p class="py-4 m-0">
<strong>Stats&nbsp;updated:</strong>&nbsp;{{ .VspStats.UpdateTime }}
<strong>Stats&nbsp;updated:</strong>&nbsp;{{ .WebApiCache.UpdateTime }}
<br />
<strong>Support:</strong>&nbsp;<a href="mailto:{{ .VspStats.SupportEmail }}" rel="noopener noreferrer">{{ .VspStats.SupportEmail }}</a>
<strong>Support:</strong>&nbsp;<a href="mailto:{{ .WebApiCfg.SupportEmail }}" rel="noopener noreferrer">{{ .WebApiCfg.SupportEmail }}</a>
<br />
<strong>VSP&nbsp;public&nbsp;key:</strong>&nbsp;<span class="code">{{ .VspStats.PubKey }}</span>
<strong>VSP&nbsp;public&nbsp;key:</strong>&nbsp;<span class="code">{{ .WebApiCache.PubKey }}</span>
</p>
</div>

View File

@ -5,12 +5,12 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>Decred VSP - {{ .VspStats.Designation }}</title>
<title>Decred VSP - {{ .WebApiCfg.Designation }}</title>
<link rel="stylesheet" href="/public/css/vendor/bootstrap-4.5.0.min.css" />
<link rel="stylesheet" href="/public/css/vspd.css?v={{ .VspStats.VspdVersion }}" />
<link rel="stylesheet" href="/public/css/vspd.css?v={{ .WebApiCfg.VspdVersion }}" />
<!-- fonts.css should be last to ensure dcr fonts take precedence. -->
<link rel="stylesheet" href="/public/css/fonts.css?v={{ .VspStats.VspdVersion }}" />
<link rel="stylesheet" href="/public/css/fonts.css?v={{ .WebApiCfg.VspdVersion }}" />
<!-- Custom favicon -->
<!-- Apple PWA -->
@ -59,13 +59,13 @@
</div>
<div class="logo--text">
VSP<br>
<b>{{ .VspStats.Designation }}</b>
<b>{{ .WebApiCfg.Designation }}</b>
</div>
</a>
</div>
</nav>
{{ if .VspStats.Debug }}
{{ if .WebApiCfg.Debug }}
<div class="container">
<div class="alert alert-warning my-2">
Web server is running in debug mode - don't do this in production!

View File

@ -3,13 +3,13 @@
<div class="vsp-overview pt-4 pb-3 mb-3">
<div class="container">
{{ if .VspStats.VspClosed }}
{{ if .WebApiCfg.VspClosed }}
<div class="alert alert-danger">
<h4 class="alert-heading mb-3">
This Voting Service Provider is closed
</h4>
<p>
{{ .VspStats.VspClosedMsg }}
{{ .WebApiCfg.VspClosedMsg }}
</p>
<p>
A closed VSP will still vote on tickets with already paid fees, but will not accept new any tickets.
@ -27,7 +27,7 @@
to find out more about VSPs, tickets, and voting.
</p>
{{ template "vsp-stats" .VspStats }}
{{ template "vsp-stats" . }}
</div>
</div>

View File

@ -4,35 +4,35 @@
<div class="col-6 col-sm-4 col-lg-2 py-3">
<div class="stat-title">Live tickets</div>
<div class="stat-value">{{ .Voting }}</div>
<div class="stat-value">{{ .WebApiCache.Voting }}</div>
</div>
<div class="col-6 col-sm-4 col-lg-2 py-3">
<div class="stat-title">Voted tickets</div>
<div class="stat-value">{{ .Voted }}</div>
<div class="stat-value">{{ .WebApiCache.Voted }}</div>
</div>
<div class="col-6 col-sm-4 col-lg-2 py-3">
<div class="stat-title">Revoked tickets</div>
<div class="stat-value">
{{ .Revoked }}
<span class="text-muted">({{ float32ToPercent .RevokedProportion }})</span>
{{ .WebApiCache.Revoked }}
<span class="text-muted">({{ float32ToPercent .WebApiCache.RevokedProportion }})</span>
</div>
</div>
<div class="col-6 col-sm-4 col-lg-2 py-3">
<div class="stat-title">VSP Fee</div>
<div class="stat-value">{{ .VSPFee }}%</div>
<div class="stat-value">{{ .WebApiCfg.VSPFee }}%</div>
</div>
<div class="col-6 col-sm-4 col-lg-2 py-3">
<div class="stat-title">Network</div>
<div class="stat-value">{{ .Network }}</div>
<div class="stat-value">{{ .WebApiCfg.NetParams.Name }}</div>
</div>
<div class="col-6 col-sm-4 col-lg-2 py-3">
<div class="stat-title">Network Proportion</div>
<div class="stat-value">{{ float32ToPercent .NetworkProportion }}</div>
<div class="stat-value">{{ float32ToPercent .WebApiCache.NetworkProportion }}</div>
</div>
</div>

View File

@ -1,4 +1,4 @@
// Copyright (c) 2020 The Decred developers
// Copyright (c) 2020-2021 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
@ -13,7 +13,7 @@ import (
// vspInfo is the handler for "GET /api/v3/vspinfo".
func vspInfo(c *gin.Context) {
cachedStats := getVSPStats()
cachedStats := getCache()
sendJSONResponse(vspInfoResponse{
APIVersions: []int64{3},
Timestamp: time.Now().Unix(),

View File

@ -1,4 +1,4 @@
// Copyright (c) 2020 The Decred developers
// Copyright (c) 2020-2021 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
@ -69,8 +69,8 @@ func Start(ctx context.Context, requestShutdownChan chan struct{}, shutdownWg *s
}
// Populate cached VSP stats before starting webserver.
initVSPStats()
err = updateVSPStats(ctx, vdb, dcrd, config.NetParams)
initCache()
err = updateCache(ctx, vdb, dcrd, config.NetParams)
if err != nil {
log.Errorf("Could not initialize VSP stats cache: %v", err)
}
@ -130,7 +130,7 @@ func Start(ctx context.Context, requestShutdownChan chan struct{}, shutdownWg *s
// Start webserver.
go func() {
err = srv.Serve(listener)
err := srv.Serve(listener)
// If the server dies for any reason other than ErrServerClosed (from
// graceful server.Shutdown), log the error and request vspd be
// shutdown.
@ -157,7 +157,7 @@ func Start(ctx context.Context, requestShutdownChan chan struct{}, shutdownWg *s
shutdownWg.Done()
return
case <-ticker.C:
err = updateVSPStats(ctx, vdb, dcrd, config.NetParams)
err := updateCache(ctx, vdb, dcrd, config.NetParams)
if err != nil {
log.Errorf("Failed to update cached VSP stats: %v", err)
}