Precalculate index stats
This commit is contained in:
parent
d801debaf6
commit
be637f4540
13
constants/constants.go
Normal file
13
constants/constants.go
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package constants
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// time.Duration constants
|
||||||
|
const (
|
||||||
|
// RecalculateTimeWindowHighSharesJob interval
|
||||||
|
RecalculateTimeWindowHighSharesJobInterval = 1 * time.Minute
|
||||||
|
// RecalculateTopSharesJob interval
|
||||||
|
RecalculateTopSharesJobInterval = 30 * time.Second
|
||||||
|
// IngestorWatchInterval interval
|
||||||
|
IngestorWatchInterval = 30 * time.Second
|
||||||
|
)
|
@ -17,6 +17,8 @@ const (
|
|||||||
|
|
||||||
TopSharesCollectionName = "TopShares"
|
TopSharesCollectionName = "TopShares"
|
||||||
TopSharesAmount = 20
|
TopSharesAmount = 20
|
||||||
|
|
||||||
|
TimeWindowHighShareCollectionName = "TimeWindowHighShareStat"
|
||||||
)
|
)
|
||||||
|
|
||||||
func InitDatabase(path string) (*clover.DB, error) {
|
func InitDatabase(path string) (*clover.DB, error) {
|
||||||
@ -65,6 +67,21 @@ func InitDatabase(path string) (*clover.DB, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Init TimeWindowHighShareStat collection
|
||||||
|
hasTimeWindowCollection, err := db.HasCollection(TimeWindowHighShareCollectionName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to check TimeWindowHighShare collection: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hasTimeWindowCollection {
|
||||||
|
if err := db.CreateCollection(TimeWindowHighShareCollectionName); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create TimeWindowHighShare collection: %v", err)
|
||||||
|
}
|
||||||
|
if err := db.CreateIndex(TimeWindowHighShareCollectionName, "TimeWindowID"); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create index for TimeWindowHighShare: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return db, nil
|
return db, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -165,3 +182,37 @@ func ReplaceTopShares(db *clover.DB, shares []models.ShareLog) {
|
|||||||
|
|
||||||
log.Printf("Replaced TopShares with %d shares", len(shares))
|
log.Printf("Replaced TopShares with %d shares", len(shares))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetTimeWindowHighShares(db *clover.DB) []models.TimeWindowHighShare {
|
||||||
|
results, err := db.FindAll(
|
||||||
|
c.NewQuery(TimeWindowHighShareCollectionName).
|
||||||
|
Sort(c.SortOption{Field: "TimeWindowID", Direction: 1}),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("failed to list time window high shares: %v", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
timeWindowHighShares := make([]models.TimeWindowHighShare, len(results))
|
||||||
|
for idx, doc := range results {
|
||||||
|
var timeWindowHighShare models.TimeWindowHighShare
|
||||||
|
doc.Unmarshal(&timeWindowHighShare)
|
||||||
|
timeWindowHighShares[idx] = timeWindowHighShare
|
||||||
|
}
|
||||||
|
|
||||||
|
return timeWindowHighShares
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetTimeWindowHighShare(db *clover.DB, share models.TimeWindowHighShare) error {
|
||||||
|
doc := document.NewDocumentOf(&share)
|
||||||
|
|
||||||
|
existingDoc, _ := db.FindFirst(c.NewQuery(TimeWindowHighShareCollectionName).
|
||||||
|
Where(c.Field("TimeWindowID").Eq(share.TimeWindowID)))
|
||||||
|
if existingDoc != nil {
|
||||||
|
db.ReplaceById(TimeWindowHighShareCollectionName, existingDoc.ObjectId(), doc)
|
||||||
|
} else {
|
||||||
|
db.InsertOne(TimeWindowHighShareCollectionName, doc)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@ -35,5 +35,5 @@ func FormatCreateDate(createdate string) string {
|
|||||||
t := time.Unix(sec, nsec)
|
t := time.Unix(sec, nsec)
|
||||||
return t.Format(time.DateTime)
|
return t.Format(time.DateTime)
|
||||||
}
|
}
|
||||||
return ""
|
return "-"
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/ostafen/clover/v2"
|
"github.com/ostafen/clover/v2"
|
||||||
"github.com/ostafen/clover/v2/document"
|
"github.com/ostafen/clover/v2/document"
|
||||||
|
|
||||||
|
"pool-stats/constants"
|
||||||
"pool-stats/database"
|
"pool-stats/database"
|
||||||
"pool-stats/models"
|
"pool-stats/models"
|
||||||
)
|
)
|
||||||
@ -26,7 +27,7 @@ func NewIngestor(db *clover.DB, path string) *Ingestor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (this *Ingestor) WatchAndIngest() {
|
func (this *Ingestor) WatchAndIngest() {
|
||||||
ticker := time.NewTicker(30 * time.Second)
|
ticker := time.NewTicker(constants.IngestorWatchInterval)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
|
106
jobs/recalculateTimeWindowHighShares.go
Normal file
106
jobs/recalculateTimeWindowHighShares.go
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
package jobs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"pool-stats/constants"
|
||||||
|
"pool-stats/database"
|
||||||
|
"pool-stats/models"
|
||||||
|
"pool-stats/notlinq"
|
||||||
|
"sort"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ostafen/clover/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RecalculateTimeWindowHighSharesJob struct {
|
||||||
|
DB *clover.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRecalculateTimeWindowHighSharesJob(db *clover.DB) *RecalculateTimeWindowHighSharesJob {
|
||||||
|
return &RecalculateTimeWindowHighSharesJob{DB: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (job *RecalculateTimeWindowHighSharesJob) Run() error {
|
||||||
|
ticker := time.NewTicker(constants.RecalculateTimeWindowHighSharesJobInterval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
job.recalculateTimeWindowHighShares()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (job *RecalculateTimeWindowHighSharesJob) recalculateTimeWindowHighShares() {
|
||||||
|
topShares := database.ListTopShares(job.DB)
|
||||||
|
sort.Slice(topShares, func(i, j int) bool {
|
||||||
|
return topShares[i].SDiff > topShares[j].SDiff
|
||||||
|
})
|
||||||
|
|
||||||
|
// All time high share
|
||||||
|
if len(topShares) > 0 {
|
||||||
|
allTimeHighShare := topShares[0]
|
||||||
|
allTimeHighShareStat := &models.TimeWindowHighShare{
|
||||||
|
TimeWindowID: "0-all-time",
|
||||||
|
TimeWindowName: "All Time",
|
||||||
|
SDiff: allTimeHighShare.SDiff,
|
||||||
|
Time: allTimeHighShare.CreateDate,
|
||||||
|
}
|
||||||
|
database.SetTimeWindowHighShare(job.DB, *allTimeHighShareStat)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Other ranges
|
||||||
|
timeWindows := []struct {
|
||||||
|
ID string
|
||||||
|
Name string
|
||||||
|
Since time.Time
|
||||||
|
}{
|
||||||
|
{"1-hour", "Past Hour", time.Now().Add(-1 * time.Hour)},
|
||||||
|
{"2-day", "Past 24h", time.Now().Add(-24 * time.Hour)},
|
||||||
|
{"3-week", "Past 7d", time.Now().Add(-7 * 24 * time.Hour)},
|
||||||
|
}
|
||||||
|
for _, tw := range timeWindows {
|
||||||
|
// Can use one of top shares if in range,
|
||||||
|
// otherwise get highest share in range
|
||||||
|
var highestShare models.ShareLog
|
||||||
|
topSharesInRange := notlinq.
|
||||||
|
Where(topShares, func(s models.ShareLog) bool {
|
||||||
|
shareTime, err := s.ParseCreateDate()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return shareTime.After(tw.Since)
|
||||||
|
})
|
||||||
|
sort.Slice(topSharesInRange, func(i, j int) bool {
|
||||||
|
return topSharesInRange[i].SDiff > topSharesInRange[j].SDiff
|
||||||
|
})
|
||||||
|
if len(topSharesInRange) > 0 {
|
||||||
|
highestShare = topSharesInRange[0]
|
||||||
|
} else {
|
||||||
|
highestShareDocs, _ := database.GetHighestSharesInRange(
|
||||||
|
job.DB, database.CollectionName, tw.Since, 1)
|
||||||
|
if len(highestShareDocs) > 0 {
|
||||||
|
highestShare = highestShareDocs[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var timeWindowStat models.TimeWindowHighShare
|
||||||
|
if highestShare.SDiff > 0 {
|
||||||
|
timeWindowStat = models.TimeWindowHighShare{
|
||||||
|
TimeWindowID: tw.ID,
|
||||||
|
TimeWindowName: tw.Name,
|
||||||
|
SDiff: highestShare.SDiff,
|
||||||
|
Time: highestShare.CreateDate,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
timeWindowStat = models.TimeWindowHighShare{
|
||||||
|
TimeWindowID: tw.ID,
|
||||||
|
TimeWindowName: tw.Name,
|
||||||
|
SDiff: 0,
|
||||||
|
Time: "-",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
database.SetTimeWindowHighShare(job.DB, timeWindowStat)
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
package jobs
|
package jobs
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"pool-stats/constants"
|
||||||
"pool-stats/database"
|
"pool-stats/database"
|
||||||
"pool-stats/helpers"
|
"pool-stats/helpers"
|
||||||
"pool-stats/models"
|
"pool-stats/models"
|
||||||
@ -20,7 +21,7 @@ func NewRecalculateTopSharesJob(db *clover.DB) *RecalculateTopSharesJob {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (job *RecalculateTopSharesJob) Run() error {
|
func (job *RecalculateTopSharesJob) Run() error {
|
||||||
ticker := time.NewTicker(10 * time.Second)
|
ticker := time.NewTicker(constants.RecalculateTopSharesJobInterval)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
|
3
main.go
3
main.go
@ -29,6 +29,9 @@ func main() {
|
|||||||
topSharesRecalcJob := jobs.NewRecalculateTopSharesJob(db)
|
topSharesRecalcJob := jobs.NewRecalculateTopSharesJob(db)
|
||||||
go topSharesRecalcJob.Run()
|
go topSharesRecalcJob.Run()
|
||||||
|
|
||||||
|
timeWindowHighSharesRecalcJob := jobs.NewRecalculateTimeWindowHighSharesJob(db)
|
||||||
|
go timeWindowHighSharesRecalcJob.Run()
|
||||||
|
|
||||||
webServer := web.NewWebServer(db, config.Port)
|
webServer := web.NewWebServer(db, config.Port)
|
||||||
if err := webServer.Start(); err != nil {
|
if err := webServer.Start(); err != nil {
|
||||||
log.Fatalf("Failed to start web server: %v", err)
|
log.Fatalf("Failed to start web server: %v", err)
|
||||||
|
@ -29,10 +29,11 @@ type ShareLog struct {
|
|||||||
Agent string `json:"agent"` // Miner agent string (e.g., bitaxe/BM1370)
|
Agent string `json:"agent"` // Miner agent string (e.g., bitaxe/BM1370)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ShareStat struct {
|
type TimeWindowHighShare struct {
|
||||||
Label string
|
TimeWindowID string `json:"time_window_id"` // Unique ID for the time window
|
||||||
Diff string
|
TimeWindowName string `json:"time_window_name"` // Name of the time window (e.g., "Past Hour")
|
||||||
Time string
|
SDiff float64 `json:"share_diff"` // Difficulty of the highest share
|
||||||
|
Time string `json:"share_time"` // Time of the highest share
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseCreateDate can be used to convert ShareLog.CreateDate to time.Time
|
// ParseCreateDate can be used to convert ShareLog.CreateDate to time.Time
|
||||||
|
@ -12,3 +12,13 @@ func UniqueBy[T any, K comparable](items []T, keySelector func(T) K) []T {
|
|||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Where[T any](items []T, predicate func(T) bool) []T {
|
||||||
|
result := make([]T, 0)
|
||||||
|
for _, item := range items {
|
||||||
|
if predicate(item) {
|
||||||
|
result = append(result, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
@ -1,50 +0,0 @@
|
|||||||
package stats
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ostafen/clover/v2"
|
|
||||||
|
|
||||||
"pool-stats/database"
|
|
||||||
"pool-stats/helpers"
|
|
||||||
"pool-stats/models"
|
|
||||||
)
|
|
||||||
|
|
||||||
func GetStats(db *clover.DB) ([]models.ShareStat, error) {
|
|
||||||
now := time.Now()
|
|
||||||
ranges := []struct {
|
|
||||||
Label string
|
|
||||||
Since time.Time
|
|
||||||
}{
|
|
||||||
{"Past Hour", now.Add(-1 * time.Hour)},
|
|
||||||
{"Past 24h", now.Add(-24 * time.Hour)},
|
|
||||||
{"Past 7d", now.Add(-7 * 24 * time.Hour)},
|
|
||||||
}
|
|
||||||
|
|
||||||
stats := []models.ShareStat{}
|
|
||||||
|
|
||||||
// All-time highest
|
|
||||||
doc, _ := database.GetHighestSharesInRange(db, database.CollectionName, time.Unix(0, 0), 1)
|
|
||||||
if doc != nil {
|
|
||||||
stats = append(stats, models.ShareStat{
|
|
||||||
Label: "All Time",
|
|
||||||
Diff: helpers.HumanDiff(doc[0].SDiff),
|
|
||||||
Time: helpers.ParseCreateDate(doc[0].CreateDate).Format(time.RFC822),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, r := range ranges {
|
|
||||||
doc, _ := database.GetHighestSharesInRange(db, database.CollectionName, r.Since, 1)
|
|
||||||
if doc != nil {
|
|
||||||
stats = append(stats, models.ShareStat{
|
|
||||||
Label: r.Label,
|
|
||||||
Diff: helpers.HumanDiff(doc[0].SDiff),
|
|
||||||
Time: helpers.ParseCreateDate(doc[0].CreateDate).Format(time.RFC822),
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
stats = append(stats, models.ShareStat{Label: r.Label, Diff: "-", Time: "-"})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return stats, nil
|
|
||||||
}
|
|
@ -1,3 +1,4 @@
|
|||||||
|
{{define "index"}}
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
@ -45,13 +46,15 @@
|
|||||||
<th>Highest Share Diff</th>
|
<th>Highest Share Diff</th>
|
||||||
<th>Time</th>
|
<th>Time</th>
|
||||||
</tr>
|
</tr>
|
||||||
{{range .}}
|
{{ range .Stats }}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{.Label}}</td>
|
<td>{{ .TimeWindowName }}</td>
|
||||||
<td>{{.Diff}}</td>
|
<td>
|
||||||
<td>{{.Time}}</td>
|
{{ if ne .SDiff 0.0 }} {{ humanDiff .SDiff }} {{ else }} - {{ end }}
|
||||||
|
</td>
|
||||||
|
<td>{{ formatCreateDate .Time }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{end}}
|
{{ end }}
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
@ -61,3 +64,4 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
{{ end }}
|
||||||
|
@ -1,19 +0,0 @@
|
|||||||
package web
|
|
||||||
|
|
||||||
import (
|
|
||||||
"html/template"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"pool-stats/stats"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (ws *WebServer) IndexHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
shareStats, err := stats.GetStats(ws.db)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Failed to load stats", 500)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
tmpl := template.Must(template.ParseFiles("templates/index.html"))
|
|
||||||
tmpl.Execute(w, shareStats)
|
|
||||||
}
|
|
41
web/indexHandler.go
Normal file
41
web/indexHandler.go
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package web
|
||||||
|
|
||||||
|
import (
|
||||||
|
"html/template"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"pool-stats/database"
|
||||||
|
"pool-stats/helpers"
|
||||||
|
"pool-stats/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IndexPageData struct {
|
||||||
|
Stats []models.TimeWindowHighShare
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ws *WebServer) IndexHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
tmpl := template.New("index").Funcs(template.FuncMap{
|
||||||
|
"humanDiff": helpers.HumanDiff,
|
||||||
|
"formatCreateDate": helpers.FormatCreateDate,
|
||||||
|
})
|
||||||
|
tmpl, err := tmpl.ParseFiles("templates/index.html")
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Failed to load template", 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tws := database.GetTimeWindowHighShares(ws.db)
|
||||||
|
if tws == nil {
|
||||||
|
http.Error(w, "Failed to load time window high shares", 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
indexData := IndexPageData{
|
||||||
|
Stats: tws,
|
||||||
|
}
|
||||||
|
if err := tmpl.Execute(w, indexData); err != nil {
|
||||||
|
http.Error(w, "Failed to render template", 500)
|
||||||
|
println("Error rendering template:", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user