perf(cdi): cache em memória das taxas BCB com expiração às 23:59 do dia do fetch
- Cache map[date]rate compartilhado entre chamadas do mesmo processo - Expira às 23:59:59 do dia corrente (não TTL fixo de 24h) - Double-checked locking evita múltiplas chamadas concorrentes ao BCB - Cobre múltiplas contas com start dates diferentes sem chamadas redundantes
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"financeiro-carvalho/internal/model"
|
"financeiro-carvalho/internal/model"
|
||||||
@@ -13,10 +14,58 @@ import (
|
|||||||
|
|
||||||
const bcbCDIURL = "https://api.bcb.gov.br/dados/serie/bcdata.sgs.12/dados?formato=json&dataInicial=%s&dataFinal=%s"
|
const bcbCDIURL = "https://api.bcb.gov.br/dados/serie/bcdata.sgs.12/dados?formato=json&dataInicial=%s&dataFinal=%s"
|
||||||
|
|
||||||
|
// cdiRateCache guarda as taxas BCB em memória até as 23:59:59 do dia do fetch.
|
||||||
|
type cdiRateCache struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
rates map[string]float64 // YYYY-MM-DD → taxa diária
|
||||||
|
minDate string
|
||||||
|
maxDate string
|
||||||
|
expiresAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *cdiRateCache) valid() bool {
|
||||||
|
return !c.expiresAt.IsZero() && time.Now().Before(c.expiresAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *cdiRateCache) covers(startDate, endDate string) bool {
|
||||||
|
return c.valid() && c.minDate != "" && startDate >= c.minDate && endDate <= c.maxDate
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *cdiRateCache) get(startDate, endDate string) []bcbEntry {
|
||||||
|
start, _ := time.Parse("2006-01-02", startDate)
|
||||||
|
end, _ := time.Parse("2006-01-02", endDate)
|
||||||
|
var out []bcbEntry
|
||||||
|
for d := start; !d.After(end); d = d.AddDate(0, 0, 1) {
|
||||||
|
key := d.Format("2006-01-02")
|
||||||
|
if r, ok := c.rates[key]; ok {
|
||||||
|
out = append(out, bcbEntry{date: key, value: r})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *cdiRateCache) put(entries []bcbEntry, startDate, endDate string) {
|
||||||
|
if c.rates == nil {
|
||||||
|
c.rates = make(map[string]float64, len(entries))
|
||||||
|
}
|
||||||
|
for _, e := range entries {
|
||||||
|
c.rates[e.date] = e.value
|
||||||
|
}
|
||||||
|
if c.minDate == "" || startDate < c.minDate {
|
||||||
|
c.minDate = startDate
|
||||||
|
}
|
||||||
|
if c.maxDate == "" || endDate > c.maxDate {
|
||||||
|
c.maxDate = endDate
|
||||||
|
}
|
||||||
|
now := time.Now()
|
||||||
|
c.expiresAt = time.Date(now.Year(), now.Month(), now.Day(), 23, 59, 59, 0, now.Location())
|
||||||
|
}
|
||||||
|
|
||||||
type CDIYieldService struct {
|
type CDIYieldService struct {
|
||||||
accountRepo repository.AccountRepoWithYield
|
accountRepo repository.AccountRepoWithYield
|
||||||
txRepo repository.ManualTransactionRepository
|
txRepo repository.ManualTransactionRepository
|
||||||
client *http.Client
|
client *http.Client
|
||||||
|
cache cdiRateCache
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCDIYieldService(accountRepo repository.AccountRepoWithYield, txRepo repository.ManualTransactionRepository) *CDIYieldService {
|
func NewCDIYieldService(accountRepo repository.AccountRepoWithYield, txRepo repository.ManualTransactionRepository) *CDIYieldService {
|
||||||
@@ -109,7 +158,21 @@ type bcbEntry struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *CDIYieldService) fetchCDIRates(startDate, endDate string) ([]bcbEntry, error) {
|
func (s *CDIYieldService) fetchCDIRates(startDate, endDate string) ([]bcbEntry, error) {
|
||||||
// BCB API uses DD/MM/YYYY format
|
s.cache.mu.RLock()
|
||||||
|
if s.cache.covers(startDate, endDate) {
|
||||||
|
entries := s.cache.get(startDate, endDate)
|
||||||
|
s.cache.mu.RUnlock()
|
||||||
|
return entries, nil
|
||||||
|
}
|
||||||
|
s.cache.mu.RUnlock()
|
||||||
|
|
||||||
|
s.cache.mu.Lock()
|
||||||
|
defer s.cache.mu.Unlock()
|
||||||
|
// double-check after acquiring write lock
|
||||||
|
if s.cache.covers(startDate, endDate) {
|
||||||
|
return s.cache.get(startDate, endDate), nil
|
||||||
|
}
|
||||||
|
|
||||||
start, _ := time.Parse("2006-01-02", startDate)
|
start, _ := time.Parse("2006-01-02", startDate)
|
||||||
end, _ := time.Parse("2006-01-02", endDate)
|
end, _ := time.Parse("2006-01-02", endDate)
|
||||||
url := fmt.Sprintf(bcbCDIURL, start.Format("02/01/2006"), end.Format("02/01/2006"))
|
url := fmt.Sprintf(bcbCDIURL, start.Format("02/01/2006"), end.Format("02/01/2006"))
|
||||||
@@ -128,16 +191,17 @@ func (s *CDIYieldService) fetchCDIRates(startDate, endDate string) ([]bcbEntry,
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var out []bcbEntry
|
var entries []bcbEntry
|
||||||
for _, r := range raw {
|
for _, r := range raw {
|
||||||
// BCB date format: DD/MM/YYYY → convert to YYYY-MM-DD
|
|
||||||
t, err := time.Parse("02/01/2006", r.Data)
|
t, err := time.Parse("02/01/2006", r.Data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
var v float64
|
var v float64
|
||||||
fmt.Sscanf(r.Valor, "%f", &v)
|
fmt.Sscanf(r.Valor, "%f", &v)
|
||||||
out = append(out, bcbEntry{date: t.Format("2006-01-02"), value: v})
|
entries = append(entries, bcbEntry{date: t.Format("2006-01-02"), value: v})
|
||||||
}
|
}
|
||||||
return out, nil
|
|
||||||
|
s.cache.put(entries, startDate, endDate)
|
||||||
|
return entries, nil
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user