1use std::{sync::Arc, time::Duration};
2
3use async_trait::async_trait;
4use chrono::{DateTime, Utc};
5use fuel_core_client::client::types::NodeInfo;
6use fuel_tx::ConsensusParameters;
7use fuels_core::types::errors::Result;
8use tokio::sync::RwLock;
9
10#[cfg_attr(test, mockall::automock)]
11#[async_trait]
12pub trait CacheableRpcs {
13 async fn consensus_parameters(&self) -> Result<ConsensusParameters>;
14 async fn node_info(&self) -> Result<NodeInfo>;
15}
16
17trait Clock {
18 fn now(&self) -> DateTime<Utc>;
19}
20
21#[derive(Debug, Clone)]
22pub struct TtlConfig {
23 pub consensus_parameters: Duration,
24}
25
26impl Default for TtlConfig {
27 fn default() -> Self {
28 TtlConfig {
29 consensus_parameters: Duration::from_secs(60),
30 }
31 }
32}
33
34#[derive(Debug, Clone)]
35struct Dated<T> {
36 value: T,
37 date: DateTime<Utc>,
38}
39
40impl<T> Dated<T> {
41 fn is_stale(&self, now: DateTime<Utc>, ttl: Duration) -> bool {
42 self.date + ttl < now
43 }
44}
45
46#[derive(Debug, Clone, Copy)]
47pub struct SystemClock;
48impl Clock for SystemClock {
49 fn now(&self) -> DateTime<Utc> {
50 Utc::now()
51 }
52}
53
54#[derive(Debug, Clone)]
55pub struct CachedClient<Client, Clock = SystemClock> {
56 client: Client,
57 ttl_config: TtlConfig,
58 cached_consensus_params: Arc<RwLock<Option<Dated<ConsensusParameters>>>>,
59 cached_node_info: Arc<RwLock<Option<Dated<NodeInfo>>>>,
60 clock: Clock,
61}
62
63impl<Client, Clock> CachedClient<Client, Clock> {
64 pub fn new(client: Client, ttl: TtlConfig, clock: Clock) -> Self {
65 Self {
66 client,
67 ttl_config: ttl,
68 cached_consensus_params: Default::default(),
69 cached_node_info: Default::default(),
70 clock,
71 }
72 }
73
74 pub fn set_ttl(&mut self, ttl: TtlConfig) {
75 self.ttl_config = ttl
76 }
77
78 pub fn inner(&self) -> &Client {
79 &self.client
80 }
81
82 pub fn inner_mut(&mut self) -> &mut Client {
83 &mut self.client
84 }
85}
86
87impl<Client, Clk> CachedClient<Client, Clk>
88where
89 Client: CacheableRpcs,
90{
91 pub async fn clear(&self) {
92 *self.cached_consensus_params.write().await = None;
93 }
94}
95
96#[async_trait]
97impl<Client, Clk> CacheableRpcs for CachedClient<Client, Clk>
98where
99 Clk: Clock + Send + Sync,
100 Client: CacheableRpcs + Send + Sync,
101{
102 async fn consensus_parameters(&self) -> Result<ConsensusParameters> {
103 {
104 let read_lock = self.cached_consensus_params.read().await;
105 if let Some(entry) = read_lock.as_ref() {
106 if !entry.is_stale(self.clock.now(), self.ttl_config.consensus_parameters) {
107 return Ok(entry.value.clone());
108 }
109 }
110 }
111
112 let mut write_lock = self.cached_consensus_params.write().await;
113
114 if let Some(entry) = write_lock.as_ref() {
116 if !entry.is_stale(self.clock.now(), self.ttl_config.consensus_parameters) {
117 return Ok(entry.value.clone());
118 }
119 }
120
121 let fresh_parameters = self.client.consensus_parameters().await?;
122 *write_lock = Some(Dated {
123 value: fresh_parameters.clone(),
124 date: self.clock.now(),
125 });
126
127 Ok(fresh_parameters)
128 }
129
130 async fn node_info(&self) -> Result<NodeInfo> {
131 let ttl = self.ttl_config.consensus_parameters;
133 {
134 let read_lock = self.cached_node_info.read().await;
135 if let Some(entry) = read_lock.as_ref() {
136 if !entry.is_stale(self.clock.now(), ttl) {
137 return Ok(entry.value.clone());
138 }
139 }
140 }
141
142 let mut write_lock = self.cached_node_info.write().await;
143
144 if let Some(entry) = write_lock.as_ref() {
146 if !entry.is_stale(self.clock.now(), ttl) {
147 return Ok(entry.value.clone());
148 }
149 }
150
151 let fresh_node_info = self.client.node_info().await?;
152 *write_lock = Some(Dated {
153 value: fresh_node_info.clone(),
154 date: self.clock.now(),
155 });
156
157 Ok(fresh_node_info)
158 }
159}
160
161#[cfg(test)]
162mod tests {
163 use std::sync::Mutex;
164
165 use fuel_core_client::client::schema::{
166 node_info::{IndexationFlags, TxPoolStats},
167 U64,
168 };
169 use fuel_types::ChainId;
170
171 use super::*;
172
173 #[derive(Clone, Default)]
174 struct TestClock {
175 time: Arc<Mutex<DateTime<Utc>>>,
176 }
177
178 impl TestClock {
179 fn update_time(&self, time: DateTime<Utc>) {
180 *self.time.lock().unwrap() = time;
181 }
182 }
183
184 impl Clock for TestClock {
185 fn now(&self) -> DateTime<Utc> {
186 *self.time.lock().unwrap()
187 }
188 }
189
190 #[tokio::test]
191 async fn initial_call_to_consensus_params_fwd_to_api() {
192 let mut api = MockCacheableRpcs::new();
194 api.expect_consensus_parameters()
195 .once()
196 .return_once(|| Ok(ConsensusParameters::default()));
197 let sut = CachedClient::new(api, TtlConfig::default(), TestClock::default());
198
199 let _consensus_params = sut.consensus_parameters().await.unwrap();
201
202 }
205
206 #[tokio::test]
207 async fn new_call_to_consensus_params_cached() {
208 let mut api = MockCacheableRpcs::new();
210 api.expect_consensus_parameters()
211 .once()
212 .return_once(|| Ok(ConsensusParameters::default()));
213 let sut = CachedClient::new(
214 api,
215 TtlConfig {
216 consensus_parameters: Duration::from_secs(10),
217 },
218 TestClock::default(),
219 );
220 let consensus_parameters = sut.consensus_parameters().await.unwrap();
221
222 let second_call_consensus_params = sut.consensus_parameters().await.unwrap();
224
225 assert_eq!(consensus_parameters, second_call_consensus_params);
228 }
229
230 #[tokio::test]
231 async fn if_ttl_expired_cache_is_updated() {
232 let original_consensus_params = ConsensusParameters::default();
234
235 let changed_consensus_params = {
236 let mut params = original_consensus_params.clone();
237 params.set_chain_id(ChainId::new(99));
238 params
239 };
240
241 let api = {
242 let mut api = MockCacheableRpcs::new();
243 let original_consensus_params = original_consensus_params.clone();
244 let changed_consensus_params = changed_consensus_params.clone();
245 api.expect_consensus_parameters()
246 .once()
247 .return_once(move || Ok(original_consensus_params));
248
249 api.expect_consensus_parameters()
250 .once()
251 .return_once(move || Ok(changed_consensus_params));
252 api
253 };
254
255 let clock = TestClock::default();
256 let start_time = clock.now();
257
258 let sut = CachedClient::new(
259 api,
260 TtlConfig {
261 consensus_parameters: Duration::from_secs(10),
262 },
263 clock.clone(),
264 );
265 let consensus_parameters = sut.consensus_parameters().await.unwrap();
266
267 clock.update_time(start_time + Duration::from_secs(11));
268 let second_call_consensus_params = sut.consensus_parameters().await.unwrap();
270
271 assert_eq!(consensus_parameters, original_consensus_params);
274 assert_eq!(second_call_consensus_params, changed_consensus_params);
275 }
276
277 #[tokio::test]
278 async fn clear_cache_clears_consensus_params_cache() {
279 let first_params = ConsensusParameters::default();
281 let second_params = {
282 let mut params = ConsensusParameters::default();
283 params.set_chain_id(ChainId::new(1234));
284 params
285 };
286
287 let api = {
288 let mut api = MockCacheableRpcs::new();
289 let first_clone = first_params.clone();
290 api.expect_consensus_parameters()
291 .times(1)
292 .return_once(move || Ok(first_clone));
293
294 let second_clone = second_params.clone();
295 api.expect_consensus_parameters()
296 .times(1)
297 .return_once(move || Ok(second_clone));
298 api
299 };
300
301 let clock = TestClock::default();
302 let sut = CachedClient::new(api, TtlConfig::default(), clock.clone());
303
304 let result1 = sut.consensus_parameters().await.unwrap();
305
306 sut.clear().await;
308
309 let result2 = sut.consensus_parameters().await.unwrap();
311
312 assert_eq!(result1, first_params);
313 assert_eq!(result2, second_params);
314 }
315
316 fn dummy_node_info() -> NodeInfo {
317 NodeInfo {
318 utxo_validation: true,
319 vm_backtrace: false,
320 max_tx: u64::MAX,
321 max_gas: u64::MAX,
322 max_size: u64::MAX,
323 max_depth: u64::MAX,
324 node_version: "0.0.1".to_string(),
325 indexation: IndexationFlags {
326 balances: true,
327 coins_to_spend: true,
328 asset_metadata: true,
329 },
330 tx_pool_stats: TxPoolStats {
331 tx_count: U64(1),
332 total_gas: U64(1),
333 total_size: U64(1),
334 },
335 }
336 }
337
338 #[tokio::test]
339 async fn initial_call_to_node_info_fwd_to_api() {
340 let mut api = MockCacheableRpcs::new();
342 api.expect_node_info()
343 .once()
344 .return_once(|| Ok(dummy_node_info()));
345 let sut = CachedClient::new(api, TtlConfig::default(), TestClock::default());
346
347 let _node_info = sut.node_info().await.unwrap();
349
350 }
353
354 #[tokio::test]
355 async fn new_call_to_node_info_cached() {
356 let mut api = MockCacheableRpcs::new();
358 api.expect_node_info()
359 .once()
360 .return_once(|| Ok(dummy_node_info()));
361 let sut = CachedClient::new(
362 api,
363 TtlConfig {
364 consensus_parameters: Duration::from_secs(10),
365 },
366 TestClock::default(),
367 );
368 let first_node_info = sut.node_info().await.unwrap();
369
370 let second_node_info = sut.node_info().await.unwrap();
372
373 assert_eq!(first_node_info, second_node_info);
375 }
376
377 #[tokio::test]
378 async fn if_ttl_expired_node_info_cache_is_updated() {
379 let original_node_info = dummy_node_info();
381
382 let changed_node_info = NodeInfo {
383 node_version: "changed".to_string(),
384 ..dummy_node_info()
385 };
386
387 let api = {
388 let mut api = MockCacheableRpcs::new();
389 let original_clone = original_node_info.clone();
390 api.expect_node_info()
391 .times(1)
392 .return_once(move || Ok(original_clone));
393
394 let changed_clone = changed_node_info.clone();
395 api.expect_node_info()
396 .times(1)
397 .return_once(move || Ok(changed_clone));
398 api
399 };
400
401 let clock = TestClock::default();
402 let start_time = clock.now();
403
404 let sut = CachedClient::new(
405 api,
406 TtlConfig {
407 consensus_parameters: Duration::from_secs(10),
408 },
409 clock.clone(),
410 );
411 let first_call = sut.node_info().await.unwrap();
412
413 clock.update_time(start_time + Duration::from_secs(11));
415
416 let second_call = sut.node_info().await.unwrap();
418
419 assert_eq!(first_call, original_node_info);
421 assert_eq!(second_call, changed_node_info);
422 }
423}