fuels_accounts/provider/
cache.rs1use 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 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 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 let _consensus_params = sut.consensus_parameters().await.unwrap();
163
164 }
167
168 #[tokio::test]
169 async fn new_call_to_consensus_params_cached() {
170 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 let second_call_consensus_params = sut.consensus_parameters().await.unwrap();
186
187 assert_eq!(consensus_parameters, second_call_consensus_params);
190 }
191
192 #[tokio::test]
193 async fn if_ttl_expired_cache_is_updated() {
194 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 let second_call_consensus_params = sut.consensus_parameters().await.unwrap();
232
233 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 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 sut.clear().await;
270
271 let result2 = sut.consensus_parameters().await.unwrap();
273
274 assert_eq!(result1, first_params);
275 assert_eq!(result2, second_params);
276 }
277}