fix(cdi): throttle de 15min em erro BCB em vez de cachear vazio até 23:59
- Erro BCB (dado não publicado): bloqueia retry por 15 min, não até EOD - Sucesso BCB: cache válido até 23:59 como antes - Garante que o CDI do dia seja calculado quando BCB publicar (~9h)
This commit is contained in:
@@ -15,13 +15,17 @@ 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.
|
// cdiRateCache guarda as taxas BCB em memória.
|
||||||
|
// Dados válidos: expiram às 23:59:59 do dia do fetch.
|
||||||
|
// Erro BCB (dado ainda não publicado): retry após 15 minutos.
|
||||||
type cdiRateCache struct {
|
type cdiRateCache struct {
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
rates map[string]float64 // YYYY-MM-DD → taxa diária
|
rates map[string]float64 // YYYY-MM-DD → taxa diária
|
||||||
minDate string
|
minDate string
|
||||||
maxDate string
|
maxDate string
|
||||||
expiresAt time.Time
|
expiresAt time.Time // EOD — só setado em sucesso
|
||||||
|
retryAfter time.Time // curto prazo — setado em erro BCB
|
||||||
|
errorRange [2]string // range que retornou erro
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *cdiRateCache) valid() bool {
|
func (c *cdiRateCache) valid() bool {
|
||||||
@@ -32,6 +36,14 @@ func (c *cdiRateCache) covers(startDate, endDate string) bool {
|
|||||||
return c.valid() && c.minDate != "" && startDate >= c.minDate && endDate <= c.maxDate
|
return c.valid() && c.minDate != "" && startDate >= c.minDate && endDate <= c.maxDate
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// throttled retorna true se uma chamada BCB recente falhou e ainda não passou o retry window.
|
||||||
|
func (c *cdiRateCache) throttled(startDate, endDate string) bool {
|
||||||
|
return !c.retryAfter.IsZero() &&
|
||||||
|
time.Now().Before(c.retryAfter) &&
|
||||||
|
startDate >= c.errorRange[0] &&
|
||||||
|
endDate <= c.errorRange[1]
|
||||||
|
}
|
||||||
|
|
||||||
func (c *cdiRateCache) get(startDate, endDate string) []bcbEntry {
|
func (c *cdiRateCache) get(startDate, endDate string) []bcbEntry {
|
||||||
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)
|
||||||
@@ -60,6 +72,13 @@ func (c *cdiRateCache) put(entries []bcbEntry, startDate, endDate string) {
|
|||||||
}
|
}
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
c.expiresAt = time.Date(now.Year(), now.Month(), now.Day(), 23, 59, 59, 0, now.Location())
|
c.expiresAt = time.Date(now.Year(), now.Month(), now.Day(), 23, 59, 59, 0, now.Location())
|
||||||
|
// limpa throttle ao ter sucesso
|
||||||
|
c.retryAfter = time.Time{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *cdiRateCache) markError(startDate, endDate string) {
|
||||||
|
c.retryAfter = time.Now().Add(15 * time.Minute)
|
||||||
|
c.errorRange = [2]string{startDate, endDate}
|
||||||
}
|
}
|
||||||
|
|
||||||
type CDIYieldService struct {
|
type CDIYieldService struct {
|
||||||
@@ -176,6 +195,11 @@ func (s *CDIYieldService) fetchCDIRates(startDate, endDate string) ([]bcbEntry,
|
|||||||
log.Printf("[cdi] cache HIT [%s, %s] → %d entries", startDate, endDate, len(entries))
|
log.Printf("[cdi] cache HIT [%s, %s] → %d entries", startDate, endDate, len(entries))
|
||||||
return entries, nil
|
return entries, nil
|
||||||
}
|
}
|
||||||
|
if s.cache.throttled(startDate, endDate) {
|
||||||
|
s.cache.mu.RUnlock()
|
||||||
|
log.Printf("[cdi] throttled [%s, %s] — retry após %s", startDate, endDate, s.cache.retryAfter.Format("15:04:05"))
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
log.Printf("[cdi] cache MISS [%s, %s] (minDate=%s maxDate=%s valid=%v)", startDate, endDate, s.cache.minDate, s.cache.maxDate, s.cache.valid())
|
log.Printf("[cdi] cache MISS [%s, %s] (minDate=%s maxDate=%s valid=%v)", startDate, endDate, s.cache.minDate, s.cache.maxDate, s.cache.valid())
|
||||||
s.cache.mu.RUnlock()
|
s.cache.mu.RUnlock()
|
||||||
|
|
||||||
@@ -185,6 +209,9 @@ func (s *CDIYieldService) fetchCDIRates(startDate, endDate string) ([]bcbEntry,
|
|||||||
if s.cache.covers(startDate, endDate) {
|
if s.cache.covers(startDate, endDate) {
|
||||||
return s.cache.get(startDate, endDate), nil
|
return s.cache.get(startDate, endDate), nil
|
||||||
}
|
}
|
||||||
|
if s.cache.throttled(startDate, endDate) {
|
||||||
|
return nil, 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)
|
||||||
@@ -202,10 +229,9 @@ func (s *CDIYieldService) fetchCDIRates(startDate, endDate string) ([]bcbEntry,
|
|||||||
Valor string `json:"valor"`
|
Valor string `json:"valor"`
|
||||||
}
|
}
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&raw); err != nil {
|
if err := json.NewDecoder(resp.Body).Decode(&raw); err != nil {
|
||||||
// BCB retornou objeto de erro em vez de array (dado ainda não publicado)
|
// BCB retornou objeto (dado ainda não publicado) — retry em 15 min
|
||||||
// cachear resultado vazio até fim do dia para não bater no BCB a cada request
|
log.Printf("[cdi] BCB decode error para [%s, %s]: %v — retry em 15min", startDate, endDate, err)
|
||||||
log.Printf("[cdi] BCB decode error para [%s, %s]: %v — cacheando vazio até 23:59", startDate, endDate, err)
|
s.cache.markError(startDate, endDate)
|
||||||
s.cache.put(nil, startDate, endDate)
|
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user