fiat: optimize token ticker timestamp lookups

This commit is contained in:
pragmaxim
2026-02-18 12:57:26 +01:00
parent 97eb405c8b
commit c6ddb8e017
2 changed files with 137 additions and 24 deletions

View File

@@ -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
}

View File

@@ -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)
}
}