fuels_accounts/provider/
cache.rs

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        // because it could have been updated since we last checked
115        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        // must borrow from consensus_parameters to keep the change non-breaking
132        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        // because it could have been updated since we last checked
145        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        // given
193        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        // when
200        let _consensus_params = sut.consensus_parameters().await.unwrap();
201
202        // then
203        // mock validates the call went through
204    }
205
206    #[tokio::test]
207    async fn new_call_to_consensus_params_cached() {
208        // given
209        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        // when
223        let second_call_consensus_params = sut.consensus_parameters().await.unwrap();
224
225        // then
226        // mock validates only one call
227        assert_eq!(consensus_parameters, second_call_consensus_params);
228    }
229
230    #[tokio::test]
231    async fn if_ttl_expired_cache_is_updated() {
232        // given
233        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        // when
269        let second_call_consensus_params = sut.consensus_parameters().await.unwrap();
270
271        // then
272        // mock validates two calls made
273        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        // given
280        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        // when
307        sut.clear().await;
308
309        // then
310        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        // given
341        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        // when
348        let _node_info = sut.node_info().await.unwrap();
349
350        // then
351        // The mock verifies that the API call was made.
352    }
353
354    #[tokio::test]
355    async fn new_call_to_node_info_cached() {
356        // given
357        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        // when: second call should return the cached value
371        let second_node_info = sut.node_info().await.unwrap();
372
373        // then: only one API call should have been made and the values are equal
374        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        // given
380        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        // Advance time past the TTL.
414        clock.update_time(start_time + Duration::from_secs(11));
415
416        // when: a new API call should be triggered because the TTL expired
417        let second_call = sut.node_info().await.unwrap();
418
419        // then
420        assert_eq!(first_call, original_node_info);
421        assert_eq!(second_call, changed_node_info);
422    }
423}