fix: recover transient stock history failures within bounded deadlines

This commit is contained in:
2026-10-09 23:20:23 +08:00
parent c8c61d29bf
commit 2acfc64d58
6 changed files with 85 additions and 6 deletions
+8 -1
View File
@@ -19,7 +19,7 @@ func NewRouter(health *service.HealthService, logger *zap.Logger, identity Ident
gin.SetMode(gin.ReleaseMode)
r := gin.New()
r.Use(func(c *gin.Context) {
ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second)
ctx, cancel := context.WithTimeout(c.Request.Context(), requestTimeout(c.Request))
defer cancel()
if identity.PublicOrigin != "" {
ctx = context.WithValue(ctx, publicOriginContextKey{}, identity.PublicOrigin)
@@ -87,3 +87,10 @@ func NewRouter(health *service.HealthService, logger *zap.Logger, identity Ident
})
return r
}
func requestTimeout(r *http.Request) time.Duration {
if r.Method == http.MethodGet && (r.URL.Path == "/api/admin/v1/market/history" || r.URL.Path == "/api/v1/market/history") || r.Method == http.MethodPost && r.URL.Path == "/api/admin/v1/backtests/from-market" {
return 35 * time.Second
}
return 5 * time.Second
}
+3
View File
@@ -90,6 +90,9 @@ func (s *MarketService) History(ctx context.Context, u model.User, symbol, excha
return model.MarketHistory{}, e
}
result, e := s.adapter.History(ctx, c, symbol, exchange, start, end)
if e != nil && ctx.Err() == nil {
result, e = s.adapter.History(ctx, c, symbol, exchange, start, end)
}
if e != nil {
return result, apperror.ErrUnavailable
}
+62
View File
@@ -0,0 +1,62 @@
package service
import (
"context"
"errors"
"git.sundynix.cn/Blizzard/Veyra/internal/apperror"
"git.sundynix.cn/Blizzard/Veyra/internal/model"
"testing"
)
type historyProvider struct{ ProviderRepository }
func (historyProvider) Get(context.Context, string) (model.ProviderConfig, error) {
return model.ProviderConfig{Enabled: true, MarketAdapter: "sina_public"}, nil
}
type historyRepo struct{ MarketRepository }
type retryHistory struct {
MarketAdapter
calls int
cancel context.CancelFunc
alwaysFail bool
}
func (a *retryHistory) History(ctx context.Context, _ model.ProviderConfig, symbol, exchange, start, end string) (model.MarketHistory, error) {
a.calls++
if a.cancel != nil {
a.cancel()
}
if a.calls == 1 || a.alwaysFail {
return model.MarketHistory{}, errors.New("upstream failure")
}
return model.MarketHistory{Symbol: symbol, Exchange: exchange, Bars: []model.HistoricalBar{{Date: start, Close: "9.41"}}}, nil
}
func TestHistoryRetriesOnceAndHonorsCancellation(t *testing.T) {
for _, tc := range []struct {
name string
cancel, fail bool
calls int
}{{"recovers", false, false, 2}, {"bounded failure", false, true, 2}, {"cancelled", true, false, 1}} {
t.Run(tc.name, func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
adapter := &retryHistory{alwaysFail: tc.fail}
if tc.cancel {
adapter.cancel = cancel
}
s := NewMarketService(historyRepo{}, historyProvider{}, adapter, true)
result, err := s.History(ctx, model.User{Role: "ADMIN", Status: "ACTIVE"}, "002215", "SZSE", "2026-07-01", "2026-07-02")
if adapter.calls != tc.calls {
t.Fatalf("calls: %d", adapter.calls)
}
if tc.fail || tc.cancel {
if !errors.Is(err, apperror.ErrUnavailable) {
t.Fatal(err)
}
} else if err != nil || result.Symbol != "002215" || len(result.Bars) != 1 {
t.Fatal("retry did not preserve real result", err)
}
})
}
}