diff --git a/oracle/price-feeder/oracle/oracle.go b/oracle/price-feeder/oracle/oracle.go index e38eeec5cb..def4228007 100644 --- a/oracle/price-feeder/oracle/oracle.go +++ b/oracle/price-feeder/oracle/oracle.go @@ -330,7 +330,11 @@ func (o *Oracle) SetPrices(ctx context.Context) error { } } + o.mtx.Lock() o.prices = computedPrices + o.lastPriceSyncTS = time.Now() + o.mtx.Unlock() + return nil } @@ -650,7 +654,6 @@ func (o *Oracle) tick( if err = o.SetPrices(ctx); err != nil { return err } - o.lastPriceSyncTS = time.Now() // Get oracle vote period, next block height, current vote period, and index // in the vote period. diff --git a/oracle/price-feeder/oracle/oracle_test.go b/oracle/price-feeder/oracle/oracle_test.go index f54fe1dab7..4512bff2f3 100644 --- a/oracle/price-feeder/oracle/oracle_test.go +++ b/oracle/price-feeder/oracle/oracle_test.go @@ -1903,6 +1903,82 @@ func TestSafeMapContains(t *testing.T) { } } +// TestSetPricesAndGetPricesConcurrency tests that concurrent calls to SetPrices +// and GetPrices do not cause a data race. Run with `go test -race` to verify. +func TestSetPricesAndGetPricesConcurrency(t *testing.T) { + oracle := &Oracle{ + logger: zerolog.Nop(), + providerPairs: map[string][]types.CurrencyPair{ + config.ProviderBinance: {{Base: "ATOM", Quote: "USD"}}, + }, + chainDenomMapping: map[string]string{ + "ATOM": "uatom", + }, + priceProviders: map[string]provider.Provider{ + config.ProviderBinance: mockProvider{ + prices: map[string]provider.TickerPrice{ + "ATOMUSD": { + Price: sdk.MustNewDecFromStr("10.00"), + Volume: sdk.MustNewDecFromStr("1000000.00"), + }, + }, + }, + }, + failedProviders: make(map[string]error), + paramCache: ParamCache{ + params: &oracletypes.Params{ + Whitelist: denomList("uatom"), + }, + }, + providerTimeout: 100 * time.Millisecond, + deviations: make(map[string]sdk.Dec), + } + + ctx := context.Background() + var wg sync.WaitGroup + const numGoroutines = 10 + const numIterations = 50 + + // Start goroutines that call SetPrices + for i := 0; i < numGoroutines; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for j := 0; j < numIterations; j++ { + _ = oracle.SetPrices(ctx) + } + }() + } + + // Start goroutines that call GetPrices + for i := 0; i < numGoroutines; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for j := 0; j < numIterations; j++ { + _ = oracle.GetPrices() + } + }() + } + + // Start goroutines that call GetLastPriceSyncTimestamp + for i := 0; i < numGoroutines; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for j := 0; j < numIterations; j++ { + _ = oracle.GetLastPriceSyncTimestamp() + } + }() + } + + wg.Wait() + + // Verify the oracle is still in a valid state + prices := oracle.GetPrices() + require.NotNil(t, prices) +} + func TestReportPriceErrMetrics(t *testing.T) { tests := []struct { name string