Skip to content

Commit 104a7e7

Browse files
apollo_l1_gas_price: while price oracle waits for query, return previous timestamp price (#10216)
1 parent 3e095e4 commit 104a7e7

File tree

2 files changed

+140
-31
lines changed

2 files changed

+140
-31
lines changed

crates/apollo_l1_gas_price/src/eth_to_strk_oracle.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ impl EthToStrkOracleClientTrait for EthToStrkOracleClient {
187187
/// - `decimals`: a `u64` value, must be equal to `ETH_TO_STRK_QUANTIZATION`.
188188
#[instrument(skip(self))]
189189
async fn eth_to_fri_rate(&self, timestamp: u64) -> Result<u128, EthToStrkOracleClientError> {
190+
const NUMBER_OF_TIMESTAMPS_BACK: u64 = 1;
190191
let quantized_timestamp = (timestamp - self.config.lag_interval_seconds)
191192
.checked_div(self.config.lag_interval_seconds)
192193
.expect("lag_interval_seconds should be non-zero");
@@ -204,7 +205,18 @@ impl EthToStrkOracleClientTrait for EthToStrkOracleClient {
204205
.get_or_insert_mut(quantized_timestamp, || self.spawn_query(quantized_timestamp));
205206
// If the query is not finished, return an error.
206207
if !handle.is_finished() {
207-
info!("Query not yet resolved: timestamp={timestamp}");
208+
debug!("Query not yet resolved: timestamp={timestamp}");
209+
// If the previous quantized timestamp is in the cache, use it.
210+
if let Some(rate) = cache.get(&(quantized_timestamp - NUMBER_OF_TIMESTAMPS_BACK)) {
211+
debug!(
212+
"Query not yet resolved: timestamp={timestamp}, using previous rate {rate} \
213+
from quantized timestamp={}",
214+
(quantized_timestamp - NUMBER_OF_TIMESTAMPS_BACK)
215+
* self.config.lag_interval_seconds
216+
);
217+
return Ok(*rate);
218+
}
219+
// If not, return a query not ready error.
208220
return Err(EthToStrkOracleClientError::QueryNotReadyError(timestamp));
209221
}
210222
let result = handle.now_or_never().expect("Handle must be finished if we got here");

crates/apollo_l1_gas_price/src/eth_to_strk_oracle_test.rs

Lines changed: 127 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use std::collections::BTreeMap;
2+
use std::time::Duration;
23

34
use apollo_config::converters::UrlAndHeaders;
45
use apollo_l1_gas_price_types::errors::EthToStrkOracleClientError;
@@ -20,19 +21,21 @@ async fn make_server(server: &mut ServerGuard, body: serde_json::Value) -> Mock
2021

2122
#[tokio::test]
2223
async fn eth_to_fri_rate_uses_cache_on_quantized_hit() {
23-
let expected_rate = 123456;
24-
let expected_rate_hex = format!("0x{expected_rate:x}");
25-
let timestamp1 = 1234567890;
26-
let timestamp2 = timestamp1 + 10; // Still in the same quantized bucket
27-
let lag_interval_seconds = 60;
24+
const EXPECTED_RATE: u128 = 123456;
25+
let expected_rate_hex = format!("0x{EXPECTED_RATE:x}");
26+
const TIMESTAMP1: u64 = 1234567890;
27+
const TIMESTAMP_OFFSET: u64 = 10;
28+
// Still in the same quantized bucket.
29+
const TIMESTAMP2: u64 = TIMESTAMP1 + TIMESTAMP_OFFSET;
30+
const LAG_INTERVAL_SECONDS: u64 = 60;
2831

29-
let quantized_timestamp = (timestamp1 - lag_interval_seconds) / lag_interval_seconds;
30-
let adjusted_timestamp = quantized_timestamp * lag_interval_seconds;
32+
let quantized_timestamp = (TIMESTAMP1 - LAG_INTERVAL_SECONDS) / LAG_INTERVAL_SECONDS;
33+
let adjusted_timestamp = quantized_timestamp * LAG_INTERVAL_SECONDS;
3134

3235
let mut server = mockito::Server::new_async().await;
3336

3437
// Define a mock response for a GET request with a specific adjusted_timestamp in the path
35-
let _m = server
38+
let _mock_response = server
3639
.mock("GET", "/") // Match the base path only.
3740
.match_query(mockito::Matcher::UrlEncoded("timestamp".into(), adjusted_timestamp.to_string()))
3841
.with_header("Content-Type", "application/json")
@@ -49,38 +52,128 @@ async fn eth_to_fri_rate_uses_cache_on_quantized_hit() {
4952
headers: BTreeMap::new(), // No additional headers needed for this test.
5053
};
5154
let url_header_list = Some(vec![url_and_headers]);
52-
let config =
53-
EthToStrkOracleConfig { url_header_list, lag_interval_seconds, ..Default::default() };
55+
let config = EthToStrkOracleConfig {
56+
url_header_list,
57+
lag_interval_seconds: LAG_INTERVAL_SECONDS,
58+
..Default::default()
59+
};
5460
let client = EthToStrkOracleClient::new(config.clone());
5561

5662
// First request should fail because the cache is empty.
57-
assert!(client.eth_to_fri_rate(timestamp1).await.is_err());
63+
assert!(client.eth_to_fri_rate(TIMESTAMP1).await.is_err());
5864
// Wait for the query to resolve.
59-
while client.eth_to_fri_rate(timestamp1).await.is_err() {
65+
while client.eth_to_fri_rate(TIMESTAMP1).await.is_err() {
6066
tokio::task::yield_now().await; // Don't block the executor.
6167
}
62-
let rate1 = client.eth_to_fri_rate(timestamp1).await.unwrap();
68+
let rate1 = client.eth_to_fri_rate(TIMESTAMP1).await.unwrap();
6369
let rate2 = client
64-
.eth_to_fri_rate(timestamp2)
70+
.eth_to_fri_rate(TIMESTAMP2)
6571
.await
6672
.expect("Should resolve immediately due to the cache");
6773
assert_eq!(rate1, rate2);
6874
}
6975

76+
#[tokio::test]
77+
async fn eth_to_fri_rate_uses_prev_cache_when_query_not_ready() {
78+
const EXPECTED_RATE: u128 = 123456;
79+
let expected_rate_hex = format!("0x{EXPECTED_RATE:x}");
80+
let different_rate = EXPECTED_RATE * 2;
81+
let different_rate_hex = format!("0x{:x}", different_rate);
82+
const LAG_INTERVAL_SECONDS: u64 = 60;
83+
84+
const TIMESTAMP1: u64 = 1234567890;
85+
const TIMESTAMP2: u64 = TIMESTAMP1 + LAG_INTERVAL_SECONDS;
86+
87+
let quantized_timestamp1 = (TIMESTAMP1 - LAG_INTERVAL_SECONDS) / LAG_INTERVAL_SECONDS;
88+
let adjusted_timestamp1 = quantized_timestamp1 * LAG_INTERVAL_SECONDS;
89+
let quantized_timestamp2 = (TIMESTAMP2 - LAG_INTERVAL_SECONDS) / LAG_INTERVAL_SECONDS;
90+
let adjusted_timestamp2 = quantized_timestamp2 * LAG_INTERVAL_SECONDS;
91+
92+
let mut server = mockito::Server::new_async().await;
93+
94+
// Define a mock response for a GET request with a specific adjusted_timestamp in the path
95+
let _mock_response1 = server
96+
.mock("GET", "/") // Match the base path only.
97+
.match_query(mockito::Matcher::UrlEncoded("timestamp".into(), adjusted_timestamp1.to_string()))
98+
.with_header("Content-Type", "application/json")
99+
.with_body(
100+
json!({
101+
"price": expected_rate_hex,
102+
"decimals": 18
103+
})
104+
.to_string(),
105+
)
106+
.create();
107+
// Second response (same matcher) returns a different value on the next call.
108+
let _mock_response2 = server
109+
.mock("GET", "/")
110+
.match_query(mockito::Matcher::UrlEncoded(
111+
"timestamp".into(),
112+
adjusted_timestamp2.to_string(),
113+
))
114+
.with_header("Content-Type", "application/json")
115+
.with_body(
116+
json!({
117+
"price": different_rate_hex,
118+
"decimals": 18
119+
})
120+
.to_string(),
121+
)
122+
.create();
123+
124+
let url_and_headers = UrlAndHeaders {
125+
url: Url::parse(&server.url()).unwrap(),
126+
headers: BTreeMap::new(), // No additional headers needed for this test.
127+
};
128+
let url_header_list = Some(vec![url_and_headers]);
129+
let config = EthToStrkOracleConfig {
130+
url_header_list,
131+
lag_interval_seconds: LAG_INTERVAL_SECONDS,
132+
..Default::default()
133+
};
134+
let client = EthToStrkOracleClient::new(config.clone());
135+
136+
// First request should fail because the cache is empty.
137+
assert!(client.eth_to_fri_rate(TIMESTAMP1).await.is_err());
138+
// Wait for the query to resolve.
139+
while client.eth_to_fri_rate(TIMESTAMP1).await.is_err() {
140+
tokio::task::yield_now().await; // Don't block the executor.
141+
}
142+
let rate1 = client.eth_to_fri_rate(TIMESTAMP1).await.unwrap();
143+
assert_eq!(rate1, EXPECTED_RATE);
144+
// Second request should resolve immediately due to the cache.
145+
let rate2 = client.eth_to_fri_rate(TIMESTAMP2).await.unwrap();
146+
assert_eq!(rate2, EXPECTED_RATE);
147+
148+
// Wait for the query to resolve, and the price to be updated.
149+
for _ in 0..100 {
150+
let current_rate = client.eth_to_fri_rate(TIMESTAMP2).await.unwrap();
151+
if current_rate > EXPECTED_RATE {
152+
break;
153+
}
154+
tokio::time::sleep(Duration::from_millis(1)).await;
155+
}
156+
157+
// Third request should already successfully get the query from the server.
158+
let rate3 = client.eth_to_fri_rate(TIMESTAMP2).await.unwrap();
159+
assert_eq!(rate3, different_rate);
160+
}
161+
70162
#[tokio::test]
71163
async fn eth_to_fri_rate_two_urls() {
72-
let expected_rate = 123456;
73-
let expected_rate_hex = format!("0x{expected_rate:x}");
74-
let lag_interval_seconds = 60;
75-
let timestamp1 = 1234567890;
76-
let timestamp2 = timestamp1 + lag_interval_seconds * 2; // New quantized bucket
164+
const EXPECTED_RATE: u128 = 123456;
165+
let expected_rate_hex = format!("0x{EXPECTED_RATE:x}");
166+
const LAG_INTERVAL_SECONDS: u64 = 60;
167+
const TIMESTAMP1: u64 = 1234567890;
168+
const TIMESTAMP2: u64 = TIMESTAMP1 + LAG_INTERVAL_SECONDS * 2; // New quantized bucket
77169
let mut server1 = mockito::Server::new_async().await;
78170
let mut server2 = mockito::Server::new_async().await;
79171

80172
// Define a mock response with badly formatted JSON for server1
81-
let _m1 = make_server(&mut server1, json!({"foo": "0x0", "bar": 18})).await;
173+
let _mock_response1 = make_server(&mut server1, json!({"foo": "0x0", "bar": 18})).await;
82174
// For server2 we get the expected response.
83-
let _m2 = make_server(&mut server2, json!({"price": &expected_rate_hex, "decimals": 18})).await;
175+
let _mock_response2 =
176+
make_server(&mut server2, json!({"price": &expected_rate_hex, "decimals": 18})).await;
84177

85178
let url_header_list = Some(vec![
86179
UrlAndHeaders {
@@ -92,25 +185,29 @@ async fn eth_to_fri_rate_two_urls() {
92185
headers: BTreeMap::new(), // No additional headers needed for this test.
93186
},
94187
]);
95-
let config =
96-
EthToStrkOracleConfig { url_header_list, lag_interval_seconds, ..Default::default() };
188+
let config = EthToStrkOracleConfig {
189+
url_header_list,
190+
lag_interval_seconds: LAG_INTERVAL_SECONDS,
191+
..Default::default()
192+
};
97193
let client = EthToStrkOracleClient::new(config.clone());
98194
// First request should fail because the cache is empty.
99-
assert!(client.eth_to_fri_rate(timestamp1).await.is_err());
195+
assert!(client.eth_to_fri_rate(TIMESTAMP1).await.is_err());
100196
// Wait for the query to resolve.
101-
while client.eth_to_fri_rate(timestamp1).await.is_err() {
197+
while client.eth_to_fri_rate(TIMESTAMP1).await.is_err() {
102198
tokio::task::yield_now().await; // Don't block the executor.
103199
}
104-
let rate1 = client.eth_to_fri_rate(timestamp1).await.unwrap();
105-
assert_eq!(rate1, expected_rate);
200+
let rate1 = client.eth_to_fri_rate(TIMESTAMP1).await.unwrap();
201+
assert_eq!(rate1, EXPECTED_RATE);
106202

107203
// Note this server fails on missing "decimals", not "price".
108-
let _m3 = make_server(&mut server2, json!({"price": &expected_rate_hex, "bar": 18})).await;
204+
let _mock_response3 =
205+
make_server(&mut server2, json!({"price": &expected_rate_hex, "bar": 18})).await;
109206
// First request should fail because the cache is empty.
110-
assert!(client.eth_to_fri_rate(timestamp2).await.is_err());
207+
assert!(client.eth_to_fri_rate(TIMESTAMP2).await.is_err());
111208
// Wait for the query to resolve.
112209
loop {
113-
match client.eth_to_fri_rate(timestamp2).await {
210+
match client.eth_to_fri_rate(TIMESTAMP2).await {
114211
Ok(_) => panic!("Both servers should be returning bad JSON!"),
115212
Err(EthToStrkOracleClientError::QueryNotReadyError(_)) => {}
116213
Err(EthToStrkOracleClientError::AllUrlsFailedError(_, index)) => {

0 commit comments

Comments
 (0)