mirror of
https://github.com/trezor/blockbook.git
synced 2026-02-20 00:51:39 +01:00
fiat: optimize token ticker timestamp lookups
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -60,6 +61,10 @@ type FiatRates struct {
|
||||
dailyTickersTo int64
|
||||
}
|
||||
|
||||
var fiatRatesFindTicker = func(d *db.RocksDB, tickerTime *time.Time, vsCurrency string, token string) (*common.CurrencyRatesTicker, error) {
|
||||
return d.FiatRatesFindTicker(tickerTime, vsCurrency, token)
|
||||
}
|
||||
|
||||
// NewFiatRates initializes the FiatRates handler
|
||||
func NewFiatRates(db *db.RocksDB, config *common.Config, metrics *common.Metrics, callback OnNewFiatRatesTicker) (*FiatRates, error) {
|
||||
|
||||
@@ -162,36 +167,60 @@ func (fr *FiatRates) GetCurrentTicker(vsCurrency string, token string) *common.C
|
||||
func (fr *FiatRates) getTokenTickersForTimestamps(timestamps []int64, vsCurrency string, token string) (*[]*common.CurrencyRatesTicker, error) {
|
||||
currentTicker := fr.GetCurrentTicker("", token)
|
||||
tickers := make([]*common.CurrencyRatesTicker, len(timestamps))
|
||||
if currentTicker == nil {
|
||||
// If token is missing in current ticker, keep nil entries and skip
|
||||
// expensive DB lookups; this preserves the existing response shape.
|
||||
return &tickers, nil
|
||||
}
|
||||
|
||||
// Query unique timestamps in ascending order so adjacent points can reuse the
|
||||
// previously resolved ticker and avoid repeated DB scans.
|
||||
uniqueMap := make(map[int64]struct{}, len(timestamps))
|
||||
uniqueTimestamps := make([]int64, 0, len(timestamps))
|
||||
for _, ts := range timestamps {
|
||||
if _, found := uniqueMap[ts]; found {
|
||||
continue
|
||||
}
|
||||
uniqueMap[ts] = struct{}{}
|
||||
uniqueTimestamps = append(uniqueTimestamps, ts)
|
||||
}
|
||||
sort.Slice(uniqueTimestamps, func(i, j int) bool {
|
||||
return uniqueTimestamps[i] < uniqueTimestamps[j]
|
||||
})
|
||||
|
||||
var prevTicker *common.CurrencyRatesTicker
|
||||
var prevTs int64
|
||||
resolvedTickers := make(map[int64]*common.CurrencyRatesTicker, len(uniqueTimestamps))
|
||||
var err error
|
||||
for i, t := range timestamps {
|
||||
// check if the token is available in the current ticker - if not, return nil ticker instead of wasting time in costly DB searches
|
||||
if currentTicker != nil {
|
||||
var ticker *common.CurrencyRatesTicker
|
||||
date := time.Unix(t, 0)
|
||||
// if previously found ticker is newer than this one (token tickers may not be in DB for every day), skip search in DB
|
||||
if prevTicker != nil && t >= prevTs && !date.After(prevTicker.Timestamp) {
|
||||
ticker = prevTicker
|
||||
prevTs = t
|
||||
} else {
|
||||
ticker, err = fr.db.FiatRatesFindTicker(&date, vsCurrency, token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
prevTicker = ticker
|
||||
prevTs = t
|
||||
}
|
||||
// if ticker not found in DB, use current ticker
|
||||
if ticker == nil {
|
||||
tickers[i] = currentTicker
|
||||
prevTicker = currentTicker
|
||||
prevTs = t
|
||||
} else {
|
||||
tickers[i] = ticker
|
||||
for _, t := range uniqueTimestamps {
|
||||
var ticker *common.CurrencyRatesTicker
|
||||
date := time.Unix(t, 0)
|
||||
// if previously found ticker is newer than this one (token tickers may not be in DB for every day), skip search in DB
|
||||
if prevTicker != nil && t >= prevTs && !date.After(prevTicker.Timestamp) {
|
||||
ticker = prevTicker
|
||||
prevTs = t
|
||||
} else {
|
||||
ticker, err = fiatRatesFindTicker(fr.db, &date, vsCurrency, token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
prevTicker = ticker
|
||||
prevTs = t
|
||||
}
|
||||
// if ticker not found in DB, use current ticker
|
||||
if ticker == nil {
|
||||
resolvedTickers[t] = currentTicker
|
||||
prevTicker = currentTicker
|
||||
prevTs = t
|
||||
} else {
|
||||
resolvedTickers[t] = ticker
|
||||
}
|
||||
}
|
||||
|
||||
for i, t := range timestamps {
|
||||
tickers[i] = resolvedTickers[t]
|
||||
}
|
||||
|
||||
return &tickers, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -334,3 +334,87 @@ func TestGetTickersForTimestamps_UsesGranularityAndFallback(t *testing.T) {
|
||||
t.Fatalf("unexpected rates: got %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetTokenTickersForTimestamps_QueriesUniqueSortedTimestamps(t *testing.T) {
|
||||
originalFindTicker := fiatRatesFindTicker
|
||||
defer func() {
|
||||
fiatRatesFindTicker = originalFindTicker
|
||||
}()
|
||||
|
||||
lookupCalls := make([]int64, 0)
|
||||
fiatRatesFindTicker = func(_ *db.RocksDB, tickerTime *time.Time, _, _ string) (*common.CurrencyRatesTicker, error) {
|
||||
ts := tickerTime.UTC().Unix()
|
||||
lookupCalls = append(lookupCalls, ts)
|
||||
return &common.CurrencyRatesTicker{
|
||||
Timestamp: time.Unix(ts, 0).UTC(),
|
||||
Rates: map[string]float32{"usd": float32(ts)},
|
||||
TokenRates: map[string]float32{"token": 1},
|
||||
}, nil
|
||||
}
|
||||
|
||||
fr := &FiatRates{
|
||||
currentTicker: &common.CurrencyRatesTicker{
|
||||
Timestamp: time.Unix(999, 0).UTC(),
|
||||
Rates: map[string]float32{"usd": 1},
|
||||
TokenRates: map[string]float32{"token": 1},
|
||||
},
|
||||
}
|
||||
input := []int64{300, 100, 200, 100, 250}
|
||||
tickers, err := fr.getTokenTickersForTimestamps(input, "", "token")
|
||||
if err != nil {
|
||||
t.Fatalf("getTokenTickersForTimestamps returned error: %v", err)
|
||||
}
|
||||
if tickers == nil {
|
||||
t.Fatal("expected non-nil tickers")
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(lookupCalls, []int64{100, 200, 250, 300}) {
|
||||
t.Fatalf("unexpected DB lookup order: got %v", lookupCalls)
|
||||
}
|
||||
|
||||
got := make([]float32, len(input))
|
||||
for i := range input {
|
||||
if (*tickers)[i] == nil {
|
||||
t.Fatalf("ticker at index %d is nil", i)
|
||||
}
|
||||
got[i] = (*tickers)[i].Rates["usd"]
|
||||
}
|
||||
want := []float32{300, 100, 200, 100, 250}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("unexpected returned rates: got %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetTokenTickersForTimestamps_SkipsDBLookupWhenCurrentTickerHasNoToken(t *testing.T) {
|
||||
originalFindTicker := fiatRatesFindTicker
|
||||
defer func() {
|
||||
fiatRatesFindTicker = originalFindTicker
|
||||
}()
|
||||
|
||||
lookupCalls := 0
|
||||
fiatRatesFindTicker = func(_ *db.RocksDB, _ *time.Time, _, _ string) (*common.CurrencyRatesTicker, error) {
|
||||
lookupCalls++
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
fr := &FiatRates{
|
||||
currentTicker: &common.CurrencyRatesTicker{
|
||||
Timestamp: time.Unix(999, 0).UTC(),
|
||||
Rates: map[string]float32{"usd": 1},
|
||||
TokenRates: map[string]float32{"another-token": 1},
|
||||
},
|
||||
}
|
||||
tickers, err := fr.getTokenTickersForTimestamps([]int64{100, 200}, "", "token")
|
||||
if err != nil {
|
||||
t.Fatalf("getTokenTickersForTimestamps returned error: %v", err)
|
||||
}
|
||||
if lookupCalls != 0 {
|
||||
t.Fatalf("expected 0 DB lookups, got %d", lookupCalls)
|
||||
}
|
||||
if tickers == nil || len(*tickers) != 2 {
|
||||
t.Fatalf("unexpected ticker result shape: %+v", tickers)
|
||||
}
|
||||
if (*tickers)[0] != nil || (*tickers)[1] != nil {
|
||||
t.Fatalf("expected nil tickers when current ticker does not include token, got %+v", *tickers)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user