set 10 min quotations timeout

This commit is contained in:
Stephan D
2026-02-20 17:19:31 +01:00
parent 199811ba09
commit 51514159f5
14 changed files with 279 additions and 26 deletions

View File

@@ -28,6 +28,14 @@ func (e serviceError) Error() string {
return string(e)
}
const (
defaultMaxQuoteTTL = 10 * time.Minute
defaultMaxQuoteTTLMillis = int64(defaultMaxQuoteTTL / time.Millisecond)
)
// Option configures oracle service behavior.
type Option func(*Service)
var (
errSideRequired = serviceError("oracle: side is required")
errAmountsMutuallyExclusive = serviceError("oracle: exactly one amount must be provided")
@@ -38,21 +46,40 @@ var (
)
type Service struct {
logger mlogger.Logger
storage storage.Repository
producer pmessaging.Producer
announcer *discovery.Announcer
invokeURI string
logger mlogger.Logger
storage storage.Repository
producer pmessaging.Producer
announcer *discovery.Announcer
invokeURI string
maxQuoteTTLMillis int64
oraclev1.UnimplementedOracleServer
}
func NewService(logger mlogger.Logger, repo storage.Repository, prod pmessaging.Producer, invokeURI string) *Service {
// WithMaxQuoteTTLMillis caps firm quote TTL requests to the supplied number of milliseconds.
func WithMaxQuoteTTLMillis(value int64) Option {
return func(s *Service) {
if value > 0 {
s.maxQuoteTTLMillis = value
}
}
}
func NewService(logger mlogger.Logger, repo storage.Repository, prod pmessaging.Producer, invokeURI string, opts ...Option) *Service {
initMetrics()
svc := &Service{
logger: logger.Named("oracle"),
storage: repo,
producer: prod,
invokeURI: strings.TrimSpace(invokeURI),
logger: logger.Named("oracle"),
storage: repo,
producer: prod,
invokeURI: strings.TrimSpace(invokeURI),
maxQuoteTTLMillis: defaultMaxQuoteTTLMillis,
}
for _, opt := range opts {
if opt != nil {
opt(svc)
}
}
if svc.maxQuoteTTLMillis <= 0 {
svc.maxQuoteTTLMillis = defaultMaxQuoteTTLMillis
}
svc.startDiscoveryAnnouncer()
return svc
@@ -222,7 +249,16 @@ func (s *Service) getQuoteResponder(ctx context.Context, req *oraclev1.GetQuoteR
expiresAt := int64(0)
if req.GetFirm() {
expiry, err := computeExpiry(now, req.GetTtlMs())
ttlMs := req.GetTtlMs()
if ttlMs > s.maxQuoteTTLMillis {
logger.Info(
"Clamping requested firm quote ttl to configured maximum",
zap.Int64("requested_ttl_ms", ttlMs),
zap.Int64("max_ttl_ms", s.maxQuoteTTLMillis),
)
ttlMs = s.maxQuoteTTLMillis
}
expiry, err := computeExpiry(now, ttlMs)
if err != nil {
return gsresponse.InvalidArgument[oraclev1.GetQuoteResponse](s.logger, mservice.FXOracle, err)
}

View File

@@ -181,6 +181,62 @@ func TestServiceGetQuoteFirm(t *testing.T) {
}
}
func TestServiceGetQuoteFirm_ClampsTTLToConfiguredMax(t *testing.T) {
const (
configuredMaxTTL = 1 * time.Second
requestedTTL = 1 * time.Minute
)
repo := &repositoryStub{}
repo.pairs = &pairStoreStub{
getFn: func(ctx context.Context, pair model.CurrencyPair) (*model.Pair, error) {
return &model.Pair{
Pair: pair,
BaseMeta: model.CurrencySettings{Code: pair.Base, Decimals: 2, Rounding: model.RoundingModeHalfEven},
QuoteMeta: model.CurrencySettings{Code: pair.Quote, Decimals: 2, Rounding: model.RoundingModeHalfEven},
}, nil
},
}
repo.rates = &ratesStoreStub{
latestFn: func(ctx context.Context, pair model.CurrencyPair, provider string) (*model.RateSnapshot, error) {
return &model.RateSnapshot{
Pair: pair,
Provider: provider,
Ask: "1.10",
Bid: "1.08",
RateRef: "rate#1",
AsOfUnixMs: time.Now().UnixMilli(),
}, nil
},
}
repo.quotes = &quotesStoreStub{}
repo.currencies = currencyStoreStub{}
svc := NewService(zap.NewNop(), repo, nil, "", WithMaxQuoteTTLMillis(int64(configuredMaxTTL/time.Millisecond)))
start := time.Now()
resp, err := svc.GetQuote(context.Background(), &oraclev1.GetQuoteRequest{
Pair: &fxv1.CurrencyPair{Base: "USD", Quote: "EUR"},
Side: fxv1.Side_BUY_BASE_SELL_QUOTE,
AmountInput: &oraclev1.GetQuoteRequest_BaseAmount{BaseAmount: &moneyv1.Money{
Currency: "USD",
Amount: "100",
}},
Firm: true,
TtlMs: int64(requestedTTL / time.Millisecond),
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
expiry := time.UnixMilli(resp.GetQuote().GetExpiresAtUnixMs())
if expiry.Before(start) {
t.Fatalf("expected expiry after request start, got %s", expiry)
}
if expiry.After(start.Add(5 * time.Second)) {
t.Fatalf("expected clamped expiry close to 1s max ttl, got %s", expiry)
}
}
func TestServiceGetQuoteRateNotFound(t *testing.T) {
repo := &repositoryStub{
pairs: &pairStoreStub{