Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ package server

import (
"context"
"math"
"math/big"
"strconv"
"testing"
"time"

Expand Down Expand Up @@ -36,10 +36,24 @@ func TestRIExchangeAutoWorkers_ConcreteStoreAndFixtureClient(t *testing.T) {
require.NoError(t, conn.QueryRow(ctx, "SELECT gen_random_uuid()::text").Scan(&testRunID))
standaloneRIID, ladderRIID := "ri-standalone-"+testRunID, "ri-ladder-"+testRunID
store := config.NewPostgresStore(conn)
baselineText, err := store.GetRIExchangeDailySpend(ctx, time.Now().UTC())
require.NoError(t, err)
baseline, err := strconv.ParseFloat(baselineText, 64)
require.NoError(t, err)
// Each run's daily cap is distinct from the 1000 USD per-exchange cap so
// only one of them can bind (#666). The standalone run leaves 1500+ USD of
// daily headroom, so the per-exchange cap must set its 1000 USD ceiling;
// the ladder run leaves under 1000 USD, so the daily cap must set it.
// Caps are today's spend rounded up to whole dollars plus a whole-dollar
// margin: a float64 holding an integer converts to the six-decimal cap
// string exactly, so a fractional-micro spend can't floor a cap a micro
// below its intended value.
todaySpendUSD := func() (exact *big.Rat, wholeDollars float64) {
spendText, spendErr := store.GetRIExchangeDailySpend(ctx, time.Now().UTC())
require.NoError(t, spendErr)
spend, ok := new(big.Rat).SetString(spendText)
require.True(t, ok, "daily spend %q is not a decimal", spendText)
approx, _ := spend.Float64()
return spend, math.Ceil(approx)
}
baselineSpend, baselineWholeDollars := todaySpendUSD()
wantCeiling := map[string]string{standaloneRIID: "1000.000000"}

quoteClient := func(riID string, executions *int) *mockExchangeClient {
return &mockExchangeClient{
Expand All @@ -48,14 +62,14 @@ func TestRIExchangeAutoWorkers_ConcreteStoreAndFixtureClient(t *testing.T) {
},
executeFunc: func(executeCtx context.Context, req exchange.ExchangeExecuteRequest) (string, *exchange.ExchangeQuoteSummary, error) {
require.Equal(t, riID, req.ReservedIDs[0])
require.Equal(t, "1000.000000", req.MaxPaymentDueUSD.FloatString(6))
require.Equal(t, wantCeiling[riID], req.MaxPaymentDueUSD.FloatString(6))
var reservationID, status, payment string
require.NoError(t, conn.QueryRow(executeCtx,
"SELECT id::text, status, payment_due::text FROM ri_exchange_history WHERE $1 = ANY(source_ri_ids)",
riID).Scan(&reservationID, &status, &payment))
require.NotEmpty(t, reservationID)
require.Equal(t, "processing", status)
require.Equal(t, "1000.000000", payment)
require.Equal(t, wantCeiling[riID], payment)
*executions++
return "accepted-" + riID, &exchange.ExchangeQuoteSummary{IsValidExchange: true, PaymentDueUSD: big.NewRat(950, 1), PaymentDueUSDStr: "950.000000", CurrencyCode: "USD"}, nil
},
Expand All @@ -72,7 +86,7 @@ func TestRIExchangeAutoWorkers_ConcreteStoreAndFixtureClient(t *testing.T) {
standaloneExecutions := 0
app := &Application{Config: store, Email: &mockEmailSender{sendCompletedFunc: func(context.Context, email.RIExchangeNotificationData) error { return nil }}}
standaloneCfg := &config.GlobalConfig{RIExchangeEnabled: true, RIExchangeMode: "auto", RIExchangeUtilizationThreshold: 95,
RIExchangeMaxPerExchangeUSD: 1000, RIExchangeMaxDailyUSD: baseline + 1000, RIExchangeLookbackDays: 30}
RIExchangeMaxPerExchangeUSD: 1000, RIExchangeMaxDailyUSD: baselineWholeDollars + 1500, RIExchangeLookbackDays: 30}
standalone, err := app.executeRIExchangeReshape(ctx, standaloneCfg, riExchangeClients{
listConvertibleRIs: func(context.Context) ([]ec2svc.ConvertibleRI, error) { return ri(standaloneRIID), nil },
getRIUtilization: func(context.Context, int) ([]recommendations.RIUtilization, error) { return util(standaloneRIID), nil },
Expand All @@ -91,6 +105,13 @@ func TestRIExchangeAutoWorkers_ConcreteStoreAndFixtureClient(t *testing.T) {
require.Equal(t, "completed", row.Status)
require.Equal(t, "950.000000", row.PaymentDue)

// Today's spend now includes the standalone exchange's 950 USD, so the
// ladder's ceiling is its daily cap minus that spend, between 980 and 981
// USD: below the per-exchange cap and above the 900 USD initial quote.
ladderSpend, ladderSpendWholeDollars := todaySpendUSD()
require.Equal(t, "950.000000", new(big.Rat).Sub(ladderSpend, baselineSpend).FloatString(6))
ladderDailyCapUSD := ladderSpendWholeDollars + 980
wantCeiling[ladderRIID] = new(big.Rat).Sub(new(big.Rat).SetFloat64(ladderDailyCapUSD), ladderSpend).FloatString(6)
var ladderRunID string
require.NoError(t, conn.QueryRow(ctx, "INSERT INTO ladder_runs (status) VALUES ('executing') RETURNING id::text").Scan(&ladderRunID))
ladderExecutions := 0
Expand All @@ -101,7 +122,7 @@ func TestRIExchangeAutoWorkers_ConcreteStoreAndFixtureClient(t *testing.T) {
return "offering-test", nil
},
RIs: riInfos, Utilization: utilInfos, RIMetadata: metadata,
Config: exchange.RIExchangeConfig{Mode: "auto", UtilizationThreshold: 95, MaxPaymentPerExchangeUSD: 1000, MaxPaymentDailyUSD: baseline + 2000},
Config: exchange.RIExchangeConfig{Mode: "auto", UtilizationThreshold: 95, MaxPaymentPerExchangeUSD: 1000, MaxPaymentDailyUSD: ladderDailyCapUSD},
AccountID: "666666666666", Region: "eu-west-1", LadderRunID: &ladderRunID,
})
require.NoError(t, err)
Expand Down
7 changes: 7 additions & 0 deletions internal/server/handler_ri_exchange_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,13 +195,18 @@ func TestConvertForAutoExchange(t *testing.T) {
func TestConfigExchangeStoreAdapter(t *testing.T) {
savedRecord := (*config.RIExchangeRecord)(nil)
completedID := ""
// The adapter must pass both caps through unchanged and in order (#666):
// the store treats the first as the daily cap and the second as the
// per-exchange cap, so a swap would pass a mock that ignores them.
var gotDailyCap, gotPerExchangeCap string
mockStore := &mockConfigStoreForExchange{
saveRIExchangeRecordFunc: func(ctx context.Context, record *config.RIExchangeRecord) error {
savedRecord = record
record.ID = "saved-id"
return nil
},
reserveRIExchangeFunc: func(ctx context.Context, record *config.RIExchangeRecord, dailyCapUSD, perExchangeCapUSD string) (string, error) {
gotDailyCap, gotPerExchangeCap = dailyCapUSD, perExchangeCapUSD
savedRecord = record
record.ID = "reserved-id"
record.PaymentDue = "10.000000"
Expand Down Expand Up @@ -255,6 +260,8 @@ func TestConfigExchangeStoreAdapter(t *testing.T) {
record := &exchange.ExchangeRecord{PaymentDue: "5", Status: "processing", Mode: "auto", LadderRunID: &ladderRunID}
ceiling, err := adapter.ReserveRIExchange(context.Background(), record, "20", "10")
testutil.AssertNoError(t, err)
testutil.AssertEqual(t, "20", gotDailyCap)
testutil.AssertEqual(t, "10", gotPerExchangeCap)
testutil.AssertEqual(t, "10.000000", ceiling)
testutil.AssertEqual(t, "reserved-id", record.ID)
testutil.AssertEqual(t, ceiling, record.PaymentDue)
Expand Down
Loading