fuels_accounts/provider/
cache.rs

1use std::{sync::Arc, time::Duration};
2
3use async_trait::async_trait;
4use chrono::{DateTime, Utc};
5use fuel_tx::ConsensusParameters;
6use fuels_core::types::errors::Result;
7use tokio::sync::RwLock;
8
9#[cfg_attr(test, mockall::automock)]
10#[async_trait]
11pub trait CacheableRpcs {
12    async fn consensus_parameters(&self) -> Result<ConsensusParameters>;
13}
14
15trait Clock {
16    fn now(&self) -> DateTime<Utc>;
17}
18
19#[derive(Debug, Clone)]
20pub struct TtlConfig {
21    pub consensus_parameters: Duration,
22}
23
24impl Default for TtlConfig {
25    fn default() -> Self {
26        TtlConfig {
27            consensus_parameters: Duration::from_secs(60),
28        }
29    }
30}
31
32#[derive(Debug, Clone)]
33struct Dated<T> {
34    value: T,
35    date: DateTime<Utc>,
36}
37
38impl<T> Dated<T> {
39    fn is_stale(&self, now: DateTime<Utc>, ttl: Duration) -> bool {
40        self.date + ttl < now
41    }
42}
43
44#[derive(Debug, Clone, Copy)]
45pub struct SystemClock;
46impl Clock for SystemClock {
47    fn now(&self) -> DateTime<Utc> {
48        Utc::now()
49    }
50}
51
52#[derive(Debug, Clone)]
53pub struct CachedClient<Client, Clock = SystemClock> {
54    client: Client,
55    ttl_config: TtlConfig,
56    cached_consensus_params: Arc<RwLock<Option<Dated<ConsensusParameters>>>>,
57    clock: Clock,
58}
59
60impl<Client, Clock> CachedClient<Client, Clock> {
61    pub fn new(client: Client, ttl: TtlConfig, clock: Clock) -> Self {
62        Self {
63            client,
64            ttl_config: ttl,
65            cached_consensus_params: Default::default(),
66            clock,
67        }
68    }
69
70    pub fn set_ttl(&mut self, ttl: TtlConfig) {
71        self.ttl_config = ttl
72    }
73
74    pub fn inner(&self) -> &Client {
75        &self.client
76    }
77
78    pub fn inner_mut(&mut self) -> &mut Client {
79        &mut self.client
80    }
81}
82
83impl<Client, Clk> CachedClient<Client, Clk>
84where
85    Client: CacheableRpcs,
86{
87    pub async fn clear(&self) {
88        *self.cached_consensus_params.write().await = None;
89    }
90}
91
92#[async_trait]
93impl<Client, Clk> CacheableRpcs for CachedClient<Client, Clk>
94where
95    Clk: Clock + Send + Sync,
96    Client: CacheableRpcs + Send + Sync,
97{
98    async fn consensus_parameters(&self) -> Result<ConsensusParameters> {
99        {
100            let read_lock = self.cached_consensus_params.read().await;
101            if let Some(entry) = read_lock.as_ref() {
102                if !entry.is_stale(self.clock.now(), self.ttl_config.consensus_parameters) {
103                    return Ok(entry.value.clone());
104                }
105            }
106        }
107
108        let mut write_lock = self.cached_consensus_params.write().await;
109
110        // because it could have been updated since we last checked
111        if let Some(entry) = write_lock.as_ref() {
112            if !entry.is_stale(self.clock.now(), self.ttl_config.consensus_parameters) {
113                return Ok(entry.value.clone());
114            }
115        }
116
117        let fresh_parameters = self.client.consensus_parameters().await?;
118        *write_lock = Some(Dated {
119            value: fresh_parameters.clone(),
120            date: self.clock.now(),
121        });
122
123        Ok(fresh_parameters)
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use std::sync::Mutex;
130
131    use fuel_types::ChainId;
132
133    use super::*;
134
135    #[derive(Clone, Default)]
136    struct TestClock {
137        time: Arc<Mutex<DateTime<Utc>>>,
138    }
139
140    impl TestClock {
141        fn update_time(&self, time: DateTime<Utc>) {
142            *self.time.lock().unwrap() = time;
143        }
144    }
145
146    impl Clock for TestClock {
147        fn now(&self) -> DateTime<Utc> {
148            *self.time.lock().unwrap()
149        }
150    }
151
152    #[tokio::test]
153    async fn initial_call_to_consensus_params_fwd_to_api() {
154        // given
155        let mut api = MockCacheableRpcs::new();
156        api.expect_consensus_parameters()
157            .once()
158            .return_once(|| Ok(ConsensusParameters::default()));
159        let sut = CachedClient::new(api, TtlConfig::default(), TestClock::default());
160
161        // when
162        let _consensus_params = sut.consensus_parameters().await.unwrap();
163
164        // then
165        // mock validates the call went through
166    }
167
168    #[tokio::test]
169    async fn new_call_to_consensus_params_cached() {
170        // given
171        let mut api = MockCacheableRpcs::new();
172        api.expect_consensus_parameters()
173            .once()
174            .return_once(|| Ok(ConsensusParameters::default()));
175        let sut = CachedClient::new(
176            api,
177            TtlConfig {
178                consensus_parameters: Duration::from_secs(10),
179            },
180            TestClock::default(),
181        );
182        let consensus_parameters = sut.consensus_parameters().await.unwrap();
183
184        // when
185        let second_call_consensus_params = sut.consensus_parameters().await.unwrap();
186
187        // then
188        // mock validates only one call
189        assert_eq!(consensus_parameters, second_call_consensus_params);
190    }
191
192    #[tokio::test]
193    async fn if_ttl_expired_cache_is_updated() {
194        // given
195        let original_consensus_params = ConsensusParameters::default();
196
197        let changed_consensus_params = {
198            let mut params = original_consensus_params.clone();
199            params.set_chain_id(ChainId::new(99));
200            params
201        };
202
203        let api = {
204            let mut api = MockCacheableRpcs::new();
205            let original_consensus_params = original_consensus_params.clone();
206            let changed_consensus_params = changed_consensus_params.clone();
207            api.expect_consensus_parameters()
208                .once()
209                .return_once(move || Ok(original_consensus_params));
210
211            api.expect_consensus_parameters()
212                .once()
213                .return_once(move || Ok(changed_consensus_params));
214            api
215        };
216
217        let clock = TestClock::default();
218        let start_time = clock.now();
219
220        let sut = CachedClient::new(
221            api,
222            TtlConfig {
223                consensus_parameters: Duration::from_secs(10),
224            },
225            clock.clone(),
226        );
227        let consensus_parameters = sut.consensus_parameters().await.unwrap();
228
229        clock.update_time(start_time + Duration::from_secs(11));
230        // when
231        let second_call_consensus_params = sut.consensus_parameters().await.unwrap();
232
233        // then
234        // mock validates two calls made
235        assert_eq!(consensus_parameters, original_consensus_params);
236        assert_eq!(second_call_consensus_params, changed_consensus_params);
237    }
238
239    #[tokio::test]
240    async fn clear_cache_clears_consensus_params_cache() {
241        // given
242        let first_params = ConsensusParameters::default();
243        let second_params = {
244            let mut params = ConsensusParameters::default();
245            params.set_chain_id(ChainId::new(1234));
246            params
247        };
248
249        let api = {
250            let mut api = MockCacheableRpcs::new();
251            let first_clone = first_params.clone();
252            api.expect_consensus_parameters()
253                .times(1)
254                .return_once(move || Ok(first_clone));
255
256            let second_clone = second_params.clone();
257            api.expect_consensus_parameters()
258                .times(1)
259                .return_once(move || Ok(second_clone));
260            api
261        };
262
263        let clock = TestClock::default();
264        let sut = CachedClient::new(api, TtlConfig::default(), clock.clone());
265
266        let result1 = sut.consensus_parameters().await.unwrap();
267
268        // when
269        sut.clear().await;
270
271        // then
272        let result2 = sut.consensus_parameters().await.unwrap();
273
274        assert_eq!(result1, first_params);
275        assert_eq!(result2, second_params);
276    }
277}