Skip to main content

ab_farmer/node_client/
caching_proxy_node_client.rs

1//! Node client wrapper around another node client that caches some data for better performance and
2//! proxies other requests through
3
4use crate::node_client::{NodeClient, NodeClientExt};
5use crate::utils::AsyncJoinOnDrop;
6use ab_core_primitives::pieces::{Piece, PieceIndex};
7use ab_core_primitives::segments::{
8    SegmentIndex, SuperSegmentHeader, SuperSegmentIndex, SuperSegmentRoot,
9};
10use ab_farmer_rpc_primitives::{
11    BlockSealInfo, BlockSealResponse, FarmerAppInfo, FarmerShardMembershipInfo,
12    MAX_SUPER_SEGMENT_HEADERS_PER_REQUEST, SlotInfo, SolutionResponse,
13};
14use async_lock::{
15    Mutex as AsyncMutex, RwLock as AsyncRwLock,
16    RwLockUpgradableReadGuardArc as AsyncRwLockUpgradableReadGuard,
17    RwLockWriteGuardArc as AsyncRwLockWriteGuard,
18};
19use async_trait::async_trait;
20use futures::{FutureExt, Stream, StreamExt, select};
21use std::pin::Pin;
22use std::sync::Arc as StdArc;
23use std::time::{Duration, Instant};
24use tokio::sync::watch;
25use tokio_stream::wrappers::WatchStream;
26use tracing::{debug, info, trace, warn};
27use triomphe::Arc;
28
29const SEGMENT_HEADERS_SYNC_INTERVAL: Duration = Duration::from_secs(1);
30const FARMER_APP_INFO_DEDUPLICATION_WINDOW: Duration = Duration::from_secs(1);
31
32#[derive(Debug, Default)]
33struct SuperSegmentHeaders {
34    super_segment_headers: Vec<SuperSegmentHeader>,
35    last_synced: Option<Instant>,
36}
37
38impl SuperSegmentHeaders {
39    /// Push a new super segment header to the cache if it is the next super segment header.
40    /// Otherwise, skip the push.
41    fn push(&mut self, new_segment_header: SuperSegmentHeader) {
42        if self.super_segment_headers.len() as u64 == u64::from(new_segment_header.index.as_inner())
43        {
44            self.super_segment_headers.push(new_segment_header);
45        }
46    }
47
48    /// Get cached super segment headers for the given super segment indices.
49    ///
50    /// Returns `None` for super segment indices that are not in the cache.
51    fn get_super_segment_headers(
52        &self,
53        super_segment_indices: &[SuperSegmentIndex],
54    ) -> Vec<Option<SuperSegmentHeader>> {
55        super_segment_indices
56            .iter()
57            .map(|super_segment_index| {
58                self.super_segment_headers
59                    .get(u64::from(*super_segment_index) as usize)
60                    .copied()
61            })
62            .collect::<Vec<_>>()
63    }
64
65    /// Get the last `limit` super segment headers from the cache
66    fn last_super_segment_headers(&self, limit: u32) -> Vec<Option<SuperSegmentHeader>> {
67        self.super_segment_headers
68            .iter()
69            .rev()
70            .take(limit as usize)
71            .rev()
72            .copied()
73            .map(Some)
74            .collect()
75    }
76
77    // TODO: Maybe caching or more compact storage that points segment indices to super segments
78    //  when this is called thousands of times during replotting?
79    fn super_segment_root_for_segment_index(
80        &self,
81        segment_index: SegmentIndex,
82    ) -> Option<SuperSegmentRoot> {
83        let index = self
84            .super_segment_headers
85            .binary_search_by_key(&segment_index, |super_segment_header| {
86                super_segment_header.max_segment_index.as_inner()
87            })
88            .unwrap_or_else(|insert_index| insert_index);
89
90        let super_segment_header = self.super_segment_headers.get(index).copied()?;
91
92        let max_segment_index = super_segment_header.max_segment_index.as_inner();
93        let first_segment_index = max_segment_index
94            - SegmentIndex::from(u64::from(super_segment_header.num_segments))
95            + SegmentIndex::ONE;
96
97        (first_segment_index..=max_segment_index)
98            .contains(&segment_index)
99            .then_some(super_segment_header.root)
100    }
101
102    /// Whether the last sync was too recent to sync again
103    fn is_sync_rate_limited(&self) -> bool {
104        self.last_synced
105            .is_some_and(|last_synced| last_synced.elapsed() < SEGMENT_HEADERS_SYNC_INTERVAL)
106    }
107
108    /// Get uncached headers from the node.
109    /// This only requires a read lock.
110    ///
111    /// Returns any extra super segment headers if the download succeeds, or an error if it fails.
112    /// The caller must write the returned super segment headers to the cache and reset the sync
113    /// rate-limit timer.
114    async fn request_uncached_headers<NC>(
115        &self,
116        client: &NC,
117    ) -> anyhow::Result<Vec<SuperSegmentHeader>>
118    where
119        NC: NodeClient,
120    {
121        let mut extra_super_segment_headers = Vec::new();
122        let mut super_segment_index_offset =
123            SuperSegmentIndex::from(self.super_segment_headers.len() as u64);
124        let segment_index_step =
125            SuperSegmentIndex::from(MAX_SUPER_SEGMENT_HEADERS_PER_REQUEST as u64);
126
127        'outer: loop {
128            let from = super_segment_index_offset;
129            let to = super_segment_index_offset + segment_index_step;
130            trace!(%from, %to, "Requesting super segment headers");
131
132            for maybe_super_segment_header in client
133                .super_segment_headers((from..to).collect::<Vec<_>>())
134                .await
135                .map_err(|error| {
136                    anyhow::anyhow!(
137                        "Failed to download super segment headers {from}..{to} from node: {error}"
138                    )
139                })?
140            {
141                let Some(super_segment_header) = maybe_super_segment_header else {
142                    // Reached non-existent super segment header
143                    break 'outer;
144                };
145
146                extra_super_segment_headers.push(super_segment_header);
147            }
148
149            super_segment_index_offset += segment_index_step;
150        }
151
152        Ok(extra_super_segment_headers)
153    }
154
155    /// Write the sync results to the cache, and reset the sync rate-limit timer.
156    fn write_cache(&mut self, extra_super_segment_headers: Vec<SuperSegmentHeader>) {
157        for super_segment_header in extra_super_segment_headers {
158            self.push(super_segment_header);
159        }
160        self.last_synced.replace(Instant::now());
161    }
162}
163
164/// Node client wrapper around another node client that caches some data for better performance and
165/// proxies other requests through.
166///
167/// NOTE: Archived segment acknowledgement is ignored in this client, all subscriptions are
168/// acknowledged implicitly and immediately.
169/// NOTE: Subscription messages that are not processed in time will be skipped for performance
170/// reasons!
171#[derive(Debug, Clone)]
172pub struct CachingProxyNodeClient<NC> {
173    inner: NC,
174    slot_info_receiver: watch::Receiver<Option<SlotInfo>>,
175    new_super_segment_headers_receiver: watch::Receiver<Option<SuperSegmentHeader>>,
176    block_sealing_receiver: watch::Receiver<Option<BlockSealInfo>>,
177    super_segment_headers: StdArc<AsyncRwLock<SuperSegmentHeaders>>,
178    last_farmer_app_info: Arc<AsyncMutex<(FarmerAppInfo, Instant)>>,
179    _background_task: Arc<AsyncJoinOnDrop<()>>,
180}
181
182impl<NC> CachingProxyNodeClient<NC>
183where
184    NC: NodeClient + Clone,
185{
186    /// Create a new instance
187    pub async fn new(client: NC) -> anyhow::Result<Self> {
188        let mut super_segment_headers = SuperSegmentHeaders::default();
189        let mut new_super_segments_notifications =
190            client.subscribe_new_super_segment_headers().await?;
191
192        info!("Downloading all super segment headers from node...");
193        // No locking is needed, we are the first and only instance right now.
194        let headers = super_segment_headers
195            .request_uncached_headers(&client)
196            .await?;
197        super_segment_headers.write_cache(headers);
198        info!("Downloaded all super segment headers from node successfully");
199
200        let super_segment_headers = StdArc::new(AsyncRwLock::new(super_segment_headers));
201
202        let (slot_info_sender, slot_info_receiver) = watch::channel(None::<SlotInfo>);
203        let slot_info_proxy_fut = {
204            let mut slot_info_subscription = client.subscribe_slot_info().await?;
205
206            async move {
207                let mut last_slot_number = None;
208                while let Some(slot_info) = slot_info_subscription.next().await {
209                    if let Some(last_slot_number) = last_slot_number
210                        && last_slot_number >= slot_info.slot
211                    {
212                        continue;
213                    }
214                    last_slot_number.replace(slot_info.slot);
215
216                    if let Err(error) = slot_info_sender.send(Some(slot_info)) {
217                        warn!(%error, "Failed to proxy slot info notification");
218                        return;
219                    }
220                }
221            }
222        };
223
224        let (new_super_segment_headers_sender, new_super_segment_headers_receiver) =
225            watch::channel(None::<SuperSegmentHeader>);
226        let super_segment_headers_maintenance_fut = {
227            let client = client.clone();
228            let super_segment_headers = StdArc::clone(&super_segment_headers);
229
230            async move {
231                let mut last_super_segment_index = None;
232                while let Some(new_segment_header) = new_super_segments_notifications.next().await {
233                    let super_segment_index = new_segment_header.index;
234                    trace!(?new_segment_header, "New super segment header notification");
235
236                    if let Some(last_super_segment_index) = last_super_segment_index
237                        && last_super_segment_index >= super_segment_index
238                    {
239                        continue;
240                    }
241                    last_super_segment_index.replace(super_segment_index);
242
243                    // Notifications are not guaranteed to be delivered (for example, they are
244                    // skipped when not processed in time or lost while the source is restarting),
245                    // download missing headers before handling this one to not end up with a gap
246                    loop {
247                        let cached_count = super_segment_headers
248                            .read()
249                            .await
250                            .super_segment_headers
251                            .len() as u64;
252                        if cached_count >= u64::from(super_segment_index.as_inner()) {
253                            break;
254                        }
255
256                        let result = super_segment_headers
257                            .read()
258                            .await
259                            .request_uncached_headers(&client)
260                            .await;
261                        match result {
262                            Ok(extra_super_segment_headers) => {
263                                if !extra_super_segment_headers.is_empty() {
264                                    super_segment_headers
265                                        .write()
266                                        .await
267                                        .write_cache(extra_super_segment_headers);
268                                    continue;
269                                }
270
271                                debug!(
272                                    %super_segment_index,
273                                    "Missing super segment headers are not available yet, \
274                                    retrying"
275                                );
276                            }
277                            Err(error) => {
278                                warn!(
279                                    %error,
280                                    %super_segment_index,
281                                    "Failed to download missing super segment headers, retrying"
282                                );
283                            }
284                        }
285
286                        tokio::time::sleep(SEGMENT_HEADERS_SYNC_INTERVAL).await;
287                    }
288
289                    // Usually already downloaded above, but the source might not have it yet
290                    super_segment_headers.write().await.push(new_segment_header);
291
292                    if let Err(error) =
293                        new_super_segment_headers_sender.send(Some(new_segment_header))
294                    {
295                        warn!(%error, "Failed to proxy new super segment header notification");
296                        return;
297                    }
298                }
299            }
300        };
301
302        let (block_sealing_sender, block_sealing_receiver) = watch::channel(None::<BlockSealInfo>);
303        let block_sealing_proxy_fut = {
304            let mut block_sealing_subscription = client.subscribe_block_sealing().await?;
305
306            async move {
307                while let Some(block_sealing_info) = block_sealing_subscription.next().await {
308                    if let Err(error) = block_sealing_sender.send(Some(block_sealing_info)) {
309                        warn!(%error, "Failed to proxy block sealing notification");
310                        return;
311                    }
312                }
313            }
314        };
315
316        let farmer_app_info = client
317            .farmer_app_info()
318            .await
319            .map_err(|error| anyhow::anyhow!("Failed to get farmer app info: {error}"))?;
320        let last_farmer_app_info = Arc::new(AsyncMutex::new((farmer_app_info, Instant::now())));
321
322        let background_task = tokio::spawn(async move {
323            select! {
324                () = slot_info_proxy_fut.fuse() => {},
325                () = super_segment_headers_maintenance_fut.fuse() => {},
326                () = block_sealing_proxy_fut.fuse() => {},
327            }
328        });
329
330        let node_client = Self {
331            inner: client,
332            slot_info_receiver,
333            new_super_segment_headers_receiver,
334            block_sealing_receiver,
335            super_segment_headers,
336            last_farmer_app_info,
337            _background_task: Arc::new(AsyncJoinOnDrop::new(background_task, true)),
338        };
339
340        Ok(node_client)
341    }
342}
343
344#[async_trait]
345impl<NC> NodeClient for CachingProxyNodeClient<NC>
346where
347    NC: NodeClient,
348{
349    async fn farmer_app_info(&self) -> anyhow::Result<FarmerAppInfo> {
350        let (last_farmer_app_info, last_farmer_app_info_request) =
351            &mut *self.last_farmer_app_info.lock().await;
352
353        if last_farmer_app_info_request.elapsed() > FARMER_APP_INFO_DEDUPLICATION_WINDOW {
354            let new_last_farmer_app_info = self.inner.farmer_app_info().await?;
355
356            *last_farmer_app_info = new_last_farmer_app_info;
357            *last_farmer_app_info_request = Instant::now();
358        }
359
360        Ok(last_farmer_app_info.clone())
361    }
362
363    async fn subscribe_slot_info(
364        &self,
365    ) -> anyhow::Result<Pin<Box<dyn Stream<Item = SlotInfo> + Send + 'static>>> {
366        Ok(Box::pin(
367            WatchStream::new(self.slot_info_receiver.clone())
368                .filter_map(|maybe_slot_info| async move { maybe_slot_info }),
369        ))
370    }
371
372    async fn submit_solution_response(
373        &self,
374        solution_response: SolutionResponse,
375    ) -> anyhow::Result<()> {
376        self.inner.submit_solution_response(solution_response).await
377    }
378
379    async fn subscribe_block_sealing(
380        &self,
381    ) -> anyhow::Result<Pin<Box<dyn Stream<Item = BlockSealInfo> + Send + 'static>>> {
382        Ok(Box::pin(
383            WatchStream::new(self.block_sealing_receiver.clone())
384                .filter_map(|maybe_block_sealing_info| async move { maybe_block_sealing_info }),
385        ))
386    }
387
388    async fn submit_block_seal(&self, block_seal: BlockSealResponse) -> anyhow::Result<()> {
389        self.inner.submit_block_seal(block_seal).await
390    }
391
392    async fn subscribe_new_super_segment_headers(
393        &self,
394    ) -> anyhow::Result<Pin<Box<dyn Stream<Item = SuperSegmentHeader> + Send + 'static>>> {
395        Ok(Box::pin(
396            WatchStream::new(self.new_super_segment_headers_receiver.clone())
397                .filter_map(|maybe_super_segment_header| async move { maybe_super_segment_header }),
398        ))
399    }
400
401    async fn super_segment_headers(
402        &self,
403        super_segment_indices: Vec<SuperSegmentIndex>,
404    ) -> anyhow::Result<Vec<Option<SuperSegmentHeader>>> {
405        let retrieved_super_segment_headers = self
406            .super_segment_headers
407            .read()
408            .await
409            .get_super_segment_headers(&super_segment_indices);
410
411        if retrieved_super_segment_headers.iter().all(Option::is_some) {
412            return Ok(retrieved_super_segment_headers);
413        }
414
415        // We might be missing a requested super segment header.
416        // Sync the cache with the node, apply a rate limit, and return cached super segment
417        // headers.
418
419        // If we took a write lock here, a queue of writers could starve all the readers, even if
420        // those writers are rate-limited. So we take an upgradable read lock for the rate limit
421        // check.
422        let super_segment_headers = self.super_segment_headers.upgradable_read_arc().await;
423
424        // Try again after acquiring the upgradeable read lock, in case another caller already
425        // synced the headers
426        let retrieved_super_segment_headers =
427            super_segment_headers.get_super_segment_headers(&super_segment_indices);
428        if retrieved_super_segment_headers.iter().all(Option::is_some) {
429            return Ok(retrieved_super_segment_headers);
430        }
431
432        // Skip the sync if we're still within the sync rate limit
433        if super_segment_headers.is_sync_rate_limited() {
434            return Ok(retrieved_super_segment_headers);
435        }
436
437        // Try to sync the cache with the node
438        let extra_super_segment_headers = super_segment_headers
439            .request_uncached_headers(&self.inner)
440            .await?;
441
442        if extra_super_segment_headers.is_empty() {
443            // No extra super segment headers on the node, so just return what is in the cache
444            return Ok(retrieved_super_segment_headers);
445        }
446
447        // Need to update the cached super segment headers, so take the write lock
448        let mut super_segment_headers =
449            AsyncRwLockUpgradableReadGuard::upgrade(super_segment_headers).await;
450        super_segment_headers.write_cache(extra_super_segment_headers);
451
452        // Downgrade the write lock to a read lock to get the updated super segment headers for the
453        // query
454        Ok(AsyncRwLockWriteGuard::downgrade(super_segment_headers)
455            .get_super_segment_headers(&super_segment_indices))
456    }
457
458    async fn super_segment_root_for_segment_index(
459        &self,
460        segment_index: SegmentIndex,
461    ) -> anyhow::Result<Option<SuperSegmentRoot>> {
462        Ok(self
463            .super_segment_headers
464            .read()
465            .await
466            .super_segment_root_for_segment_index(segment_index))
467    }
468
469    async fn piece(&self, piece_index: PieceIndex) -> anyhow::Result<Option<Piece>> {
470        self.inner.piece(piece_index).await
471    }
472
473    async fn update_shard_membership_info(
474        &self,
475        info: FarmerShardMembershipInfo,
476    ) -> anyhow::Result<()> {
477        self.inner.update_shard_membership_info(info).await
478    }
479}
480
481#[async_trait]
482impl<NC> NodeClientExt for CachingProxyNodeClient<NC>
483where
484    NC: NodeClientExt,
485{
486    async fn cached_super_segment_headers(
487        &self,
488        super_segment_indices: Vec<SuperSegmentIndex>,
489    ) -> anyhow::Result<Vec<Option<SuperSegmentHeader>>> {
490        // To avoid remote denial of service, we don't update the cache here because it is called
491        // from network code
492        Ok(self
493            .super_segment_headers
494            .read()
495            .await
496            .get_super_segment_headers(&super_segment_indices))
497    }
498
499    async fn last_super_segment_headers(
500        &self,
501        limit: u32,
502    ) -> anyhow::Result<Vec<Option<SuperSegmentHeader>>> {
503        Ok(self
504            .super_segment_headers
505            .read()
506            .await
507            .last_super_segment_headers(limit))
508    }
509}