1use crate::cache::fetcher::{FetcherResult, YangLibraryFetcher};
216use crate::cache::storage::{
217 SubscriptionInfo, YangLibraryCache, YangLibraryCacheError, YangLibraryReference,
218};
219use crate::{
220 ContentId, OTL_YANG_PUSH_CACHED_CONTENT_ID_KEY, OTL_YANG_PUSH_SUBSCRIPTION_ID_KEY,
221 OTL_YANG_PUSH_SUBSCRIPTION_ROUTER_CONTENT_ID_KEY, OTL_YANG_PUSH_SUBSCRIPTION_TARGET_KEY,
222};
223use futures_util::StreamExt;
224use futures_util::stream::FuturesUnordered;
225use netgauze_netconf_proto::yang_push::types::SubscriptionId;
226use rustc_hash::FxHashMap;
227use std::net::SocketAddr;
228use std::path::PathBuf;
229use std::sync::Arc;
230use std::time::Duration;
231use tokio::sync::{mpsc, oneshot};
232use tokio::task::{JoinError, JoinHandle};
233use tracing::{debug, info, warn};
234
235const OTL_CACHE_REQUEST_TYPE: &str = "netgauze.udp.notif.yang.push.cache.request.type";
236
237#[derive(Debug, Clone)]
238pub struct CachingStats {
239 pub requests_received: opentelemetry::metrics::Counter<u64>,
240 pub pending_cache_requests: opentelemetry::metrics::Gauge<u64>,
241 pub cache_hits: opentelemetry::metrics::Counter<u64>,
242 pub cache_misses: opentelemetry::metrics::Counter<u64>,
243 pub device_fetch_request: opentelemetry::metrics::Counter<u64>,
244 pub device_fetch_queue: opentelemetry::metrics::Gauge<u64>,
245 pub device_fetch_succeeded: opentelemetry::metrics::Counter<u64>,
246 pub device_fetch_failed: opentelemetry::metrics::Counter<u64>,
247}
248
249impl CachingStats {
250 pub fn new(meter: opentelemetry::metrics::Meter) -> Self {
251 let requests_received = meter
252 .u64_counter("netgauze.collector.yang_push.caching.requests.received")
253 .with_description("Number of requests received by the YANG library cache actor")
254 .build();
255 let pending_cache_requests = meter
256 .u64_gauge("netgauze.collector.yang_push.caching.requests.pending")
257 .with_description("Number of pending cache requests in the YANG library cache actor that are waiting for fetch to complete")
258 .build();
259 let cache_hits = meter
260 .u64_counter("netgauze.collector.yang_push.caching.requests.cache.hits")
261 .with_description("Number of cache hits in the YANG library cache actor")
262 .build();
263 let cache_misses = meter
264 .u64_counter("netgauze.collector.yang_push.caching.requests.cache.misses")
265 .with_description("Number of cache misses in the YANG library cache actor")
266 .build();
267 let device_fetch_request = meter
268 .u64_counter("netgauze.collector.yang_push.caching.device.fetch.requests")
269 .with_description(
270 "Number of device fetch requests initiated by the YANG library cache actor",
271 )
272 .build();
273 let device_fetch_queue = meter
274 .u64_gauge("netgauze.collector.yang_push.caching.device.fetch.pending")
275 .with_description("Number of device fetch requests that are currently queued in the YANG library cache actor")
276 .build();
277 let device_fetch_succeeded = meter
278 .u64_counter("netgauze.collector.yang_push.caching.device.fetch.response.succeeded")
279 .with_description("Number of device fetch requests initiated by the YANG library cache actor and succeeded")
280 .build();
281 let device_fetch_failed = meter
282 .u64_counter("netgauze.collector.yang_push.caching.device.fetch.response.failed")
283 .with_description("Number of device fetch requests initiated by the YANG library cache actor and failed")
284 .build();
285 Self {
286 requests_received,
287 pending_cache_requests,
288 cache_hits,
289 cache_misses,
290 device_fetch_request,
291 device_fetch_queue,
292 device_fetch_succeeded,
293 device_fetch_failed,
294 }
295 }
296}
297
298#[derive(Debug, Clone)]
299pub enum CacheActorCommand {
300 Shutdown,
301 Cleanup,
303}
304
305#[derive(Debug)]
306pub enum CacheLookupCommand {
307 LookupBySubscriptionInfo(SubscriptionInfo, async_channel::Sender<CacheResponse>),
308
309 LookupBySubscriptionInfoOneShot(SubscriptionInfo, oneshot::Sender<CacheResponse>),
310
311 LookupBySubscriptionId {
312 collector: SocketAddr,
313 interface: Option<String>,
314 peer: SocketAddr,
315 subscription_id: SubscriptionId,
316 tx: async_channel::Sender<CacheResponse>,
317 },
318
319 LookupBySubscriptionIdOneShot {
320 collector: SocketAddr,
321 interface: Option<String>,
322 peer: SocketAddr,
323 subscription_id: SubscriptionId,
324 tx: oneshot::Sender<CacheResponse>,
325 },
326
327 LookupByContentId(
328 ContentId,
329 async_channel::Sender<(ContentId, Option<Arc<YangLibraryReference>>)>,
330 ),
331
332 LookupByContentIdOneShot(
333 ContentId,
334 oneshot::Sender<(ContentId, Option<Arc<YangLibraryReference>>)>,
335 ),
336}
337
338impl std::fmt::Display for CacheLookupCommand {
339 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
340 match self {
341 Self::LookupBySubscriptionInfo(subscription_info, _) => {
342 write!(f, "lookup by subscription info {subscription_info}")
343 }
344 Self::LookupBySubscriptionInfoOneShot(subscription_info, _) => {
345 write!(
346 f,
347 "lookup by subscription info {subscription_info} (one shot)"
348 )
349 }
350 Self::LookupBySubscriptionId {
351 collector,
352 interface,
353 peer,
354 subscription_id,
355 tx: _tx,
356 } => {
357 write!(
358 f,
359 "lookup by subscription id {subscription_id} from peer {peer}, collector {collector}, interface {interface:?}",
360 )
361 }
362 Self::LookupBySubscriptionIdOneShot {
363 collector,
364 interface,
365 peer,
366 subscription_id,
367 tx: _tx,
368 } => {
369 write!(
370 f,
371 "lookup by subscription id {subscription_id} from peer {peer}, collector {collector}, interface {interface:?} (one shot)",
372 )
373 }
374 Self::LookupByContentId(content_id, _) => {
375 write!(f, "lookup by content id {content_id}")
376 }
377 Self::LookupByContentIdOneShot(content_id, _) => {
378 write!(f, "lookup by content id {content_id} (one shot)")
379 }
380 }
381 }
382}
383
384#[derive(Debug, strum_macros::Display)]
385pub enum CacheActorCacheError {}
386
387impl std::error::Error for CacheActorCacheError {}
388
389#[derive(Debug, Clone)]
390pub struct CacheResponse {
391 cached_content_id: Option<ContentId>,
392 subscription_info: SubscriptionInfo,
393 yang_lib_ref: Option<Arc<YangLibraryReference>>,
394}
395
396impl CacheResponse {
397 pub const fn cached_content_id(&self) -> Option<&ContentId> {
398 self.cached_content_id.as_ref()
399 }
400
401 pub const fn subscription_info(&self) -> &SubscriptionInfo {
402 &self.subscription_info
403 }
404
405 pub fn yang_lib_ref(&self) -> Option<Arc<YangLibraryReference>> {
406 self.yang_lib_ref.as_ref().map(Arc::clone)
407 }
408}
409
410impl From<CacheResponse>
411 for (
412 Option<ContentId>,
413 SubscriptionInfo,
414 Option<Arc<YangLibraryReference>>,
415 )
416{
417 fn from(value: CacheResponse) -> Self {
418 (
419 value.cached_content_id,
420 value.subscription_info,
421 value.yang_lib_ref,
422 )
423 }
424}
425
426struct CacheActor<F: YangLibraryFetcher> {
427 cmd_rx: mpsc::Receiver<CacheActorCommand>,
428 schema_cache: YangLibraryCache,
429 requests: async_channel::Receiver<CacheLookupCommand>,
430 fetcher: F,
431 fetcher_timeout: Duration,
432 pending_requests: FxHashMap<SubscriptionInfo, Vec<async_channel::Sender<CacheResponse>>>,
433 workers_queue: FuturesUnordered<JoinHandle<FetcherResult>>,
434 stats: CachingStats,
435}
436
437impl<F: YangLibraryFetcher> CacheActor<F> {
438 async fn send_yang_lib_ref(
439 subscription_info: &SubscriptionInfo,
440 yang_lib_ref: Option<Arc<YangLibraryReference>>,
441 sender: async_channel::Sender<CacheResponse>,
442 ) {
443 let hit = yang_lib_ref.is_some();
444 let cached_content_id = yang_lib_ref
445 .as_ref()
446 .map(|x| x.content_id())
447 .cloned()
448 .unwrap_or_default();
449 let response = CacheResponse {
450 cached_content_id: yang_lib_ref.as_ref().map(|x| x.content_id().clone()),
451 subscription_info: subscription_info.clone(),
452 yang_lib_ref,
453 };
454 match sender.send(response).await {
455 Ok(_) => {
456 debug!(
457 peer=%subscription_info.peer(),
458 subscription_id=subscription_info.id(),
459 router_content_id=subscription_info.content_id(),
460 target=%subscription_info.target(),
461 cached_content_id,
462 hit,
463 "yang library reference sent to requester"
464 );
465 }
466 Err(err) => {
467 warn!(
468 peer=%subscription_info.peer(),
469 subscription_id=subscription_info.id(),
470 router_content_id=subscription_info.content_id(),
471 target=%subscription_info.target(),
472 cached_content_id,
473 hit,
474 error = %err,
475 "failed to send yang library reference to requester"
476 );
477 }
478 }
479 }
480
481 async fn send_yang_lib_ref_content_id(
482 content_id: &ContentId,
483 yang_lib_ref: Option<Arc<YangLibraryReference>>,
484 sender: async_channel::Sender<(ContentId, Option<Arc<YangLibraryReference>>)>,
485 ) {
486 let found = yang_lib_ref.is_some();
487 let cached_content_id = yang_lib_ref.as_ref().map(|x| x.content_id().clone());
488 match sender.send((content_id.clone(), yang_lib_ref)).await {
489 Ok(_) => {
490 debug!(
491 content_id,
492 found,
493 cached_content_id = cached_content_id.unwrap_or_default(),
494 "yang library reference sent to requester"
495 );
496 }
497 Err(err) => {
498 warn!(
499 content_id,
500 found,
501 cached_content_id=cached_content_id.unwrap_or_default(),
502 error = %err,
503 "failed to send yang library reference to requester"
504 );
505 }
506 }
507 }
508
509 fn send_yang_lib_ref_oneshot(
510 subscription_info: &SubscriptionInfo,
511 yang_lib_ref: Option<Arc<YangLibraryReference>>,
512 sender: oneshot::Sender<CacheResponse>,
513 ) {
514 let hit = yang_lib_ref.is_some();
515 let cached_content_id = yang_lib_ref
516 .as_ref()
517 .map(|x| x.content_id().clone())
518 .unwrap_or_default();
519 let response = CacheResponse {
520 cached_content_id: yang_lib_ref.as_ref().map(|x| x.content_id().clone()),
521 subscription_info: subscription_info.clone(),
522 yang_lib_ref,
523 };
524 match sender.send(response) {
525 Ok(_) => {
526 debug!(
527 peer=%subscription_info.peer(),
528 subscription_id=subscription_info.id(),
529 router_content_id=subscription_info.content_id(),
530 target=%subscription_info.target(),
531 cached_content_id,
532 hit,
533 "yang library reference sent (oneshot) to requester"
534 );
535 }
536 Err(_err) => {
537 warn!(
538 peer=%subscription_info.peer(),
539 subscription_id=subscription_info.id(),
540 router_content_id=subscription_info.content_id(),
541 target=%subscription_info.target(),
542 cached_content_id,
543 "failed to send (oneshot) yang library reference to requester"
544 );
545 }
546 }
547 }
548
549 fn send_yang_lib_ref_oneshot_content_id(
550 content_id: &ContentId,
551 yang_lib_ref: Option<Arc<YangLibraryReference>>,
552 sender: oneshot::Sender<(ContentId, Option<Arc<YangLibraryReference>>)>,
553 ) {
554 let hit = yang_lib_ref.is_some();
555 match sender.send((content_id.clone(), yang_lib_ref)) {
556 Ok(_) => {
557 debug!(
558 content_id,
559 hit, "yang library reference sent (oneshot) to requester"
560 );
561 }
562 Err(_err) => {
563 warn!(
564 content_id,
565 hit, "failed to send (oneshot) yang library reference to requester"
566 );
567 }
568 }
569 }
570
571 async fn process_worker_result(&mut self, worker_result: Result<FetcherResult, JoinError>) {
572 match worker_result {
573 Err(err) => {
574 self.stats.device_fetch_failed.add(
575 1,
576 &[opentelemetry::KeyValue::new(
577 "error.message",
578 format!("{err}"),
579 )],
580 );
581 warn!(error=%err, "cache actor worker failed to execute a task");
582 }
583 Ok(Err((subscription_info, err))) => {
584 self.stats.device_fetch_failed.add(
585 1,
586 &[
587 opentelemetry::KeyValue::new(
588 "network.peer.address",
589 format!("{}", subscription_info.peer().ip()),
590 ),
591 opentelemetry::KeyValue::new(
592 "network.peer.port",
593 opentelemetry::Value::I64(subscription_info.peer().port().into()),
594 ),
595 opentelemetry::KeyValue::new(
596 OTL_YANG_PUSH_SUBSCRIPTION_ID_KEY,
597 opentelemetry::Value::I64(subscription_info.id().into()),
598 ),
599 opentelemetry::KeyValue::new(
600 OTL_YANG_PUSH_SUBSCRIPTION_TARGET_KEY,
601 format!("{}", subscription_info.target()),
602 ),
603 opentelemetry::KeyValue::new(
604 OTL_YANG_PUSH_SUBSCRIPTION_ROUTER_CONTENT_ID_KEY,
605 subscription_info.content_id().to_string(),
606 ),
607 opentelemetry::KeyValue::new("error.message", format!("{err}")),
608 ],
609 );
610 warn!(
611 peer=%subscription_info.peer(),
612 subscription_id=subscription_info.id(),
613 router_content_id=subscription_info.content_id(),
614 target=%subscription_info.target(),
615 error=%err,
616 "network fetcher failed to fetch yang library from device for subscription info"
617 );
618 let pending_senders = self.pending_requests.remove(&subscription_info);
619 self.stats
620 .pending_cache_requests
621 .record(self.pending_requests.len() as u64, &[]);
622 if let Some(pending_senders) = pending_senders {
623 for sender in pending_senders {
624 let _ = sender
625 .send(CacheResponse {
626 cached_content_id: None,
627 subscription_info: subscription_info.clone(),
628 yang_lib_ref: None,
629 })
630 .await
631 .map_err(|error| {
632 warn!(
633 peer=%subscription_info.peer(),
634 subscription_id=subscription_info.id(),
635 router_content_id=subscription_info.content_id(),
636 target=%subscription_info.target(),
637 error=%error,
638 "failed to send error response to requester"
639 );
640 });
641 debug!(
642 peer=%subscription_info.peer(),
643 subscription_id=subscription_info.id(),
644 router_content_id=subscription_info.content_id(),
645 target=%subscription_info.target(),
646 error=%err,
647 "sent error response to requester due to fetch failure"
648 );
649 }
650 }
651 }
652 Ok(Ok((subscription_info, yang_lib, schemas))) => {
653 let otl_tags = [
654 opentelemetry::KeyValue::new(
655 "network.peer.address",
656 format!("{}", subscription_info.peer().ip()),
657 ),
658 opentelemetry::KeyValue::new(
659 "network.peer.port",
660 opentelemetry::Value::I64(subscription_info.peer().port().into()),
661 ),
662 opentelemetry::KeyValue::new(
663 OTL_YANG_PUSH_SUBSCRIPTION_ID_KEY,
664 opentelemetry::Value::I64(subscription_info.id().into()),
665 ),
666 opentelemetry::KeyValue::new(
667 OTL_YANG_PUSH_SUBSCRIPTION_TARGET_KEY,
668 format!("{}", subscription_info.target()),
669 ),
670 opentelemetry::KeyValue::new(
671 OTL_YANG_PUSH_SUBSCRIPTION_ROUTER_CONTENT_ID_KEY,
672 subscription_info.content_id().to_string(),
673 ),
674 opentelemetry::KeyValue::new(
675 OTL_YANG_PUSH_CACHED_CONTENT_ID_KEY,
676 yang_lib.content_id().to_string(),
677 ),
678 ];
679 self.stats.device_fetch_succeeded.add(1, &otl_tags);
680 info!(
681 subscription_id=subscription_info.id(),
682 router_content_id=subscription_info.content_id(),
683 target=%subscription_info.target(),
684 modules_count = schemas.len(),
685 cached_content_id = yang_lib.content_id(),
686 "fetched yang library from device successfully"
687 );
688 let result = self.schema_cache.put_yang_library(
689 subscription_info.clone(),
690 yang_lib,
691 schemas,
692 );
693 let yang_lib_ref = match result {
694 Ok(yang_lib_ref) => {
695 info!(
696 peer=%subscription_info.peer(),
697 subscription_id=subscription_info.id(),
698 router_content_id=subscription_info.content_id(),
699 target=%subscription_info.target(),
700 "yang library for subscription info stored in cache successfully"
701 );
702 Some(yang_lib_ref)
703 }
704 Err(err) => {
705 warn!(
706 peer=%subscription_info.peer(),
707 subscription_id=subscription_info.id(),
708 router_content_id=subscription_info.content_id(),
709 target=%subscription_info.target(),
710 error=%err,
711 "failed to store yang library in cache"
712 );
713 None
714 }
715 };
716 let empty = SubscriptionInfo::new_empty(
719 subscription_info.collector(),
720 subscription_info.interface(),
721 subscription_info.peer(),
722 subscription_info.id(),
723 );
724 let mut pending_senders = self
725 .pending_requests
726 .remove(&subscription_info)
727 .unwrap_or(vec![]);
728 let pending_sender_by_id = self.pending_requests.remove(&empty).unwrap_or(vec![]);
729 pending_senders.extend(pending_sender_by_id);
730 self.stats
731 .pending_cache_requests
732 .record(self.pending_requests.len() as u64, &[]);
733 for sender in pending_senders {
734 Self::send_yang_lib_ref(&subscription_info, yang_lib_ref.clone(), sender).await;
735 }
736 }
737 }
738 }
739
740 fn cleanup_closed_senders(&mut self) {
741 self.pending_requests.retain(|_, senders| {
742 senders.retain(|s| !s.is_closed());
743 !senders.is_empty()
744 });
745 self.stats
746 .pending_cache_requests
747 .record(self.pending_requests.len() as u64, &[]);
748 }
749
750 fn otl_tags_from_subscription_inf(
751 subscription_info: &SubscriptionInfo,
752 ) -> Vec<opentelemetry::KeyValue> {
753 Vec::from([
754 opentelemetry::KeyValue::new(
755 "network.peer.address",
756 format!("{}", subscription_info.peer().ip()),
757 ),
758 opentelemetry::KeyValue::new(
759 "network.peer.port",
760 opentelemetry::Value::I64(subscription_info.peer().port().into()),
761 ),
762 opentelemetry::KeyValue::new(
763 OTL_YANG_PUSH_SUBSCRIPTION_ID_KEY,
764 opentelemetry::Value::I64(subscription_info.id().into()),
765 ),
766 opentelemetry::KeyValue::new(
767 OTL_YANG_PUSH_SUBSCRIPTION_TARGET_KEY,
768 format!("{}", subscription_info.target()),
769 ),
770 opentelemetry::KeyValue::new(
771 OTL_YANG_PUSH_SUBSCRIPTION_ROUTER_CONTENT_ID_KEY,
772 subscription_info.content_id().to_string(),
773 ),
774 opentelemetry::KeyValue::new(OTL_CACHE_REQUEST_TYPE, "LOOKUP_BY_SUBSCRIPTION_INFO"),
775 ])
776 }
777
778 async fn process_request(&mut self, request: CacheLookupCommand) {
779 match request {
780 CacheLookupCommand::LookupBySubscriptionInfo(subscription_info, sender) => {
781 let otl_tags = Self::otl_tags_from_subscription_inf(&subscription_info);
782 self.stats.requests_received.add(1, otl_tags.as_ref());
783 debug!(
784 peer=%subscription_info.peer(),
785 subscription_id=subscription_info.id(),
786 router_content_id=subscription_info.content_id(),
787 target=%subscription_info.target(),
788 "processing cache lookup by subscription info request"
789 );
790 let yang_lib_ref = self
791 .schema_cache
792 .get_by_subscription_info(&subscription_info);
793 match yang_lib_ref {
794 Some(yang_lib_ref) => {
795 info!(
796 peer=%subscription_info.peer(),
797 subscription_id=subscription_info.id(),
798 router_content_id=subscription_info.content_id(),
799 target=%subscription_info.target(),
800 cached_content_id=yang_lib_ref.content_id(),
801 "cache hit: yang library reference found in cache"
802 );
803 self.stats.cache_hits.add(1, &otl_tags);
804 Self::send_yang_lib_ref(&subscription_info, Some(yang_lib_ref), sender)
805 .await;
806 }
807 None => {
808 self.stats.cache_misses.add(1, &otl_tags);
811 let entry = self
812 .pending_requests
813 .entry(subscription_info.clone())
814 .or_default();
815 let should_fetch = entry.is_empty();
816 entry.push(sender);
817
818 if should_fetch {
819 info!(
820 peer=%subscription_info.peer(),
821 subscription_id=subscription_info.id(),
822 router_content_id=subscription_info.content_id(),
823 target=%subscription_info.target(),
824 "cache miss: starting fetch from device"
825 );
826 self.stats.device_fetch_request.add(1, &otl_tags);
827 let job_result = tokio::time::timeout(
828 self.fetcher_timeout,
829 self.fetcher.fetch(subscription_info.clone()),
830 )
831 .await;
832 let job = match job_result {
833 Ok(worker_result) => worker_result,
834 Err(err) => {
835 warn!(
836 peer=%subscription_info.peer(),
837 subscription_id=subscription_info.id(),
838 router_content_id=subscription_info.content_id(),
839 target=%subscription_info.target(),
840 error=%err,
841 "failed to fetch yang library from device"
842 );
843 entry.remove(entry.len() - 1);
846 self.stats.device_fetch_failed.add(1, &otl_tags);
847 return;
848 }
849 };
850 self.workers_queue.push(job);
851 self.stats
852 .device_fetch_queue
853 .record(self.workers_queue.len() as u64, &[]);
854 } else {
855 debug!(
856 peer=%subscription_info.peer(),
857 subscription_id=subscription_info.id(),
858 router_content_id=subscription_info.content_id(),
859 target=%subscription_info.target(),
860 pending_requests_count=entry.len(),
861 "cache miss: fetch request in progress from the router, queuing request to avoid duplicate requests to the router"
862 );
863 }
864 self.stats
865 .pending_cache_requests
866 .record(self.pending_requests.len() as u64, &otl_tags);
867 }
868 }
869 }
870 CacheLookupCommand::LookupBySubscriptionInfoOneShot(subscription_info, sender) => {
871 let mut otl_tags = Self::otl_tags_from_subscription_inf(&subscription_info);
872 self.stats.requests_received.add(1, otl_tags.as_ref());
873 debug!(
874 peer=%subscription_info.peer(),
875 subscription_id=subscription_info.id(),
876 router_content_id=subscription_info.content_id(),
877 target=%subscription_info.target(),
878 "processing cache lookup by subscription info request (one shot)"
879 );
880 if let Some(yang_lib_ref) = self
882 .schema_cache
883 .get_by_subscription_info(&subscription_info)
884 {
885 info!(
886 peer=%subscription_info.peer(),
887 subscription_id=subscription_info.id(),
888 router_content_id=subscription_info.content_id(),
889 target=%subscription_info.target(),
890 cached_content_id=yang_lib_ref.content_id(),
891 "cache hit: yang library reference found in cache"
892 );
893 self.stats.cache_hits.add(1, &otl_tags);
894 } else {
895 self.stats.cache_misses.add(1, &otl_tags);
896 self.stats.device_fetch_request.add(1, &otl_tags);
897 let worker_result = tokio::time::timeout(
898 self.fetcher_timeout,
899 self.fetcher.fetch_blocking(subscription_info.clone()),
900 )
901 .await;
902
903 let worker_result = match worker_result {
904 Ok(worker_result) => {
905 self.stats.device_fetch_succeeded.add(1, &otl_tags);
906 worker_result
907 }
908 Err(err) => {
909 otl_tags.push(opentelemetry::KeyValue::new(
910 "error.message",
911 format!("{err}"),
912 ));
913 self.stats.device_fetch_failed.add(1, &otl_tags);
914 Err((subscription_info.clone(), err.into()))
915 }
916 };
917 self.process_worker_result(Ok(worker_result)).await;
918 }
919 let yang_lib_ref = self
920 .schema_cache
921 .get_by_subscription_info(&subscription_info.clone());
922 Self::send_yang_lib_ref_oneshot(&subscription_info, yang_lib_ref, sender);
923 }
924 CacheLookupCommand::LookupBySubscriptionId {
925 collector,
926 interface,
927 peer,
928 subscription_id,
929 tx,
930 } => {
931 let otel_tags = [
932 opentelemetry::KeyValue::new("network.peer.address", format!("{}", peer.ip())),
933 opentelemetry::KeyValue::new(
934 "network.peer.port",
935 opentelemetry::Value::I64(peer.port().into()),
936 ),
937 opentelemetry::KeyValue::new(
938 OTL_YANG_PUSH_SUBSCRIPTION_ID_KEY,
939 opentelemetry::Value::I64(subscription_id.into()),
940 ),
941 ];
942 self.stats.requests_received.add(1, &otel_tags);
943 debug!(
944 peer=%peer,
945 subscription_id,
946 "processing cache lookup by subscription id request"
947 );
948 let response = self
949 .schema_cache
950 .get_by_subscription_id(peer.ip(), subscription_id);
951 if let Some((subscription_info, yang_lib_ref)) = response {
952 self.stats.cache_hits.add(1, &otel_tags);
953 Self::send_yang_lib_ref(&subscription_info, yang_lib_ref, tx).await;
954 } else {
955 warn!(
956 peer=%peer,
957 subscription_id,
958 "cache miss: subscription id not found in cache"
959 );
960 self.stats.cache_misses.add(1, &otel_tags);
961 let subscription_info = SubscriptionInfo::new_empty(
962 collector,
963 interface.clone(),
964 peer,
965 subscription_id,
966 );
967 let entry = self
968 .pending_requests
969 .entry(subscription_info.clone())
970 .or_default();
971 let should_fetch = entry.is_empty();
972 entry.push(tx);
973
974 if should_fetch {
975 info!(
976 peer=%peer,
977 subscription_id,
978 "cache miss: starting fetch from device by subscription id"
979 );
980 self.stats.device_fetch_request.add(1, &otel_tags);
981 let job_result = tokio::time::timeout(
982 self.fetcher_timeout,
983 self.fetcher.fetch_by_subscription_id(
984 collector,
985 interface.clone().map(String::into_boxed_str),
986 peer,
987 subscription_id,
988 ),
989 )
990 .await;
991 let job = match job_result {
992 Ok(worker_result) => worker_result,
993 Err(err) => {
994 warn!(
995 peer=%peer,
996 subscription_id,
997 router_content_id=subscription_info.clone().content_id().clone(),
998 target=%subscription_info.target(),
999 error=%err,
1000 "failed to fetch yang library from device"
1001 );
1002 entry.remove(entry.len() - 1);
1005 self.stats.device_fetch_failed.add(1, &otel_tags);
1006 return;
1007 }
1008 };
1009 self.workers_queue.push(job);
1010 self.stats
1011 .device_fetch_queue
1012 .record(self.workers_queue.len() as u64, &[]);
1013 } else {
1014 debug!(
1015 peer=%subscription_info.peer(),
1016 subscription_id=subscription_info.id(),
1017 router_content_id=subscription_info.content_id(),
1018 target=%subscription_info.target(),
1019 pending_requests_count=entry.len(),
1020 "cache miss: fetch request in progress from the router, queuing request to avoid duplicate requests to the router"
1021 );
1022 }
1023 self.stats
1024 .pending_cache_requests
1025 .record(self.pending_requests.len() as u64, &otel_tags);
1026 }
1027 }
1028 CacheLookupCommand::LookupBySubscriptionIdOneShot {
1029 collector,
1030 interface,
1031 peer,
1032 subscription_id,
1033 tx,
1034 } => {
1035 let mut otel_tags = vec![
1036 opentelemetry::KeyValue::new("network.peer.address", format!("{}", peer.ip())),
1037 opentelemetry::KeyValue::new(
1038 "network.peer.port",
1039 opentelemetry::Value::I64(peer.port().into()),
1040 ),
1041 opentelemetry::KeyValue::new(
1042 OTL_YANG_PUSH_SUBSCRIPTION_ID_KEY,
1043 opentelemetry::Value::I64(subscription_id.into()),
1044 ),
1045 ];
1046 self.stats.requests_received.add(1, &otel_tags);
1047 debug!(
1048 peer=%peer,
1049 subscription_id,
1050 "processing cache lookup by subscription id request (one shot)");
1051 let response = self
1052 .schema_cache
1053 .get_by_subscription_id(peer.ip(), subscription_id);
1054 if let Some((subscription_info, yang_lib_ref)) = response {
1055 self.stats.cache_hits.add(1, &otel_tags);
1056 Self::send_yang_lib_ref_oneshot(&subscription_info, yang_lib_ref, tx);
1057 } else {
1058 warn!(
1059 peer=%peer,
1060 subscription_id,
1061 "cache miss: subscription id not found in cache"
1062 );
1063 self.stats.cache_misses.add(1, &otel_tags);
1064 self.stats.device_fetch_request.add(1, &otel_tags);
1065
1066 let worker_result = tokio::time::timeout(
1067 self.fetcher_timeout,
1068 self.fetcher.fetch_by_subscription_id_blocking(
1069 collector,
1070 interface.clone().map(String::into_boxed_str),
1071 peer,
1072 subscription_id,
1073 ),
1074 )
1075 .await;
1076 let worker_result = match worker_result {
1077 Ok(worker_result) => {
1078 self.stats.device_fetch_succeeded.add(1, &otel_tags);
1079 worker_result
1080 }
1081 Err(err) => {
1082 otel_tags.push(opentelemetry::KeyValue::new(
1083 "error.message",
1084 format!("{err}"),
1085 ));
1086 self.stats.device_fetch_failed.add(1, &otel_tags);
1087 let empty = SubscriptionInfo::new_empty(
1088 collector,
1089 interface,
1090 peer,
1091 subscription_id,
1092 );
1093 Err((empty.clone(), err.into()))
1094 }
1095 };
1096 self.process_worker_result(Ok(worker_result)).await;
1097 }
1098 }
1099 CacheLookupCommand::LookupByContentId(content_id, sender) => {
1100 let otl_tags = [opentelemetry::KeyValue::new(
1101 OTL_YANG_PUSH_CACHED_CONTENT_ID_KEY,
1102 content_id.to_string(),
1103 )];
1104 self.stats.requests_received.add(1, &otl_tags);
1105 debug!(content_id, "processing cache lookup request by content id");
1106 let yang_lib_ref = self.schema_cache.get_by_content_id(&content_id);
1107 if yang_lib_ref.is_some() {
1108 self.stats.cache_hits.add(1, &otl_tags);
1109 } else {
1110 self.stats.cache_misses.add(1, &otl_tags);
1111 }
1112 Self::send_yang_lib_ref_content_id(&content_id, yang_lib_ref, sender).await;
1113 }
1114 CacheLookupCommand::LookupByContentIdOneShot(content_id, sender) => {
1115 let otl_tags = [opentelemetry::KeyValue::new(
1116 OTL_YANG_PUSH_CACHED_CONTENT_ID_KEY,
1117 content_id.to_string(),
1118 )];
1119 self.stats.requests_received.add(1, &otl_tags);
1120 debug!(
1121 content_id,
1122 "processing cache lookup request by content id (one shot)"
1123 );
1124 let yang_lib_ref = self.schema_cache.get_by_content_id(&content_id);
1125 if yang_lib_ref.is_some() {
1126 self.stats.cache_hits.add(1, &otl_tags);
1127 } else {
1128 self.stats.cache_misses.add(1, &otl_tags);
1129 }
1130 Self::send_yang_lib_ref_oneshot_content_id(&content_id, yang_lib_ref, sender);
1131 }
1132 }
1133 }
1134
1135 async fn run(mut self) -> Result<String, CacheActorCacheError> {
1136 loop {
1137 tokio::select! {
1138 biased;
1139 cmd = self.cmd_rx.recv() => {
1140 match cmd {
1141 Some(CacheActorCommand::Shutdown) => {
1142 info!(
1143 pending_requests = self.pending_requests.len(),
1144 active_workers = self.workers_queue.len(),
1145 "cache actor shutting down and aborting all active workers"
1146 );
1147 for task in self.workers_queue {
1148 task.abort();
1149 }
1150 return Ok("Schema cache shutdown successful".to_string());
1151 }
1152 Some(CacheActorCommand::Cleanup) => {
1153 self.cleanup_closed_senders();
1154 }
1155 None => {
1156 warn!(
1157 pending_requests = self.pending_requests.len(),
1158 active_workers = self.workers_queue.len(),
1159 "cache actor channel closed unexpectedly due to closed command channel"
1160 );
1161 for task in self.workers_queue {
1162 task.abort();
1163 }
1164 self.stats.device_fetch_queue.record(0, &[]);
1165 return Ok("Schema cache shutdown successful".to_string());
1166 }
1167 }
1168 }
1169 work_result = self.workers_queue.next(), if !self.workers_queue.is_empty()=> {
1170 if let Some(work_result) = work_result {
1171 self.stats.device_fetch_queue.record(self.workers_queue.len() as u64, &[]);
1172 self.process_worker_result(work_result).await;
1173 }
1174 }
1175 request = self.requests.recv() => {
1176 if let Ok(request) = request {
1177 self.process_request(request).await;
1178 } else {
1179 self.stats.requests_received.add(1, &[]);
1180 warn!(
1181 pending_requests = self.pending_requests.len(),
1182 active_workers = self.workers_queue.len(),
1183 "cache actor channel closed unexpectedly due to closed request channel"
1184 );
1185 for task in self.workers_queue {
1186 task.abort();
1187 }
1188 self.stats.device_fetch_queue.record(0, &[]);
1189 return Ok("Schema cache shutdown successful".to_string())
1190 }
1191 }
1192 }
1193 }
1194 }
1195}
1196
1197#[derive(Debug, strum_macros::Display)]
1198pub enum CacheActorHandleError {
1199 #[strum(to_string = "failed to send command to the schema caching actor")]
1200 SendError,
1201
1202 #[strum(to_string = "failed to fetch yang library from device {0}")]
1203 CacheError(YangLibraryCacheError),
1204}
1205
1206impl std::error::Error for CacheActorHandleError {}
1207
1208impl From<YangLibraryCacheError> for CacheActorHandleError {
1209 fn from(err: YangLibraryCacheError) -> Self {
1210 CacheActorHandleError::CacheError(err)
1211 }
1212}
1213
1214#[derive(Debug, Clone)]
1215pub struct CacheActorHandle {
1216 cmd_tx: mpsc::Sender<CacheActorCommand>,
1217 requests_tx: async_channel::Sender<CacheLookupCommand>,
1218}
1219
1220impl CacheActorHandle {
1221 pub fn new<F: YangLibraryFetcher + Send + Sync + 'static>(
1222 buffer_size: usize,
1223 schema_cache: either::Either<YangLibraryCache, PathBuf>,
1224 fetcher: F,
1225 fetcher_timeout: Duration,
1226 stats: either::Either<opentelemetry::metrics::Meter, CachingStats>,
1227 ) -> Result<(JoinHandle<Result<String, CacheActorCacheError>>, Self), CacheActorHandleError>
1228 {
1229 let (cmd_tx, cmd_rx) = mpsc::channel(10);
1230 let (requests_tx, requests_rx) = async_channel::bounded(buffer_size);
1231 let schema_cache = match schema_cache {
1232 either::Either::Left(schema_cache) => schema_cache,
1233 either::Either::Right(root_path) => YangLibraryCache::from_disk(root_path)?,
1234 };
1235 let stats = match stats {
1236 either::Either::Left(meter) => CachingStats::new(meter),
1237 either::Either::Right(stats) => stats,
1238 };
1239
1240 let actor = CacheActor {
1241 cmd_rx,
1242 schema_cache,
1243 requests: requests_rx,
1244 fetcher,
1245 fetcher_timeout,
1246 pending_requests: FxHashMap::default(),
1247 workers_queue: FuturesUnordered::new(),
1248 stats,
1249 };
1250
1251 let cmd_tx_clone = cmd_tx.clone();
1252 let handle = Self {
1253 cmd_tx,
1254 requests_tx,
1255 };
1256 let join_handle = tokio::spawn(actor.run());
1257
1258 tokio::spawn(async move {
1259 let mut cleanup_interval = tokio::time::interval(Duration::from_secs(300));
1260 loop {
1261 cleanup_interval.tick().await;
1262 if let Err(err) = cmd_tx_clone.send(CacheActorCommand::Cleanup).await {
1263 warn!(error=%err, "failed to send cleanup command to cache actor, stopping periodic cleanup task");
1264 break;
1265 }
1266 }
1267 });
1268 Ok((join_handle, handle))
1269 }
1270
1271 pub async fn shutdown(self) -> Result<(), CacheActorHandleError> {
1272 self.cmd_tx
1273 .send(CacheActorCommand::Shutdown)
1274 .await
1275 .map_err(|_| CacheActorHandleError::SendError)
1276 }
1277
1278 pub fn request_tx(&self) -> async_channel::Sender<CacheLookupCommand> {
1279 self.requests_tx.clone()
1280 }
1281}
1282
1283#[cfg(test)]
1284pub(crate) mod tests {
1285 use super::*;
1286 use crate::cache::fetcher::tests::TestYangLibFetcher;
1287 use futures_util::stream::FuturesOrdered;
1288 use netgauze_netconf_proto::yang_push::identities::{Encoding, Transport};
1289 use netgauze_netconf_proto::yang_push::subscription::{UpdateTrigger, YangPushModuleVersion};
1290 use netgauze_udp_notif_pkt::notification::Target;
1291 use std::collections::HashMap;
1292 use std::net::{IpAddr, Ipv4Addr, SocketAddr};
1293 use std::path::Path;
1294 use std::time::Duration;
1295
1296 pub(crate) fn test_subscription_info() -> SubscriptionInfo {
1297 SubscriptionInfo::new(
1298 SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)), 10000),
1299 None,
1300 SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 200)), 830),
1301 1,
1302 Target::new_datastore(
1303 "ds:operational".into(),
1304 either::Right("/ietf-interfaces:interfaces/ietf-interfaces:interface[ietf-interfaces:name='eth0']/statistics".into()),
1305 ),
1306 None,
1307 Some(Transport::UDPNotif),
1308 Some(Encoding::Json),
1309 Some("test subscription".into()),
1310 Some(UpdateTrigger::OnChange {
1311 dampening_period: None,
1312 sync_on_start: Some(true),
1313 excluded_change: None,
1314 }),
1315 Box::new([
1316 YangPushModuleVersion::new("ietf-interfaces".into(), Some("2018-02-20".into()), None),
1317 YangPushModuleVersion::new("ietf-ip".into(), None, None),
1318 ]),
1319 ContentId::from("ietf-interfaces-lib".to_string()),
1320 )
1321 }
1322
1323 fn copy_dir_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
1324 std::fs::create_dir_all(&dst)?;
1325 for entry in std::fs::read_dir(src)? {
1326 let entry = entry?;
1327 let ty = entry.file_type()?;
1328 if ty.is_dir() {
1329 copy_dir_all(entry.path(), dst.as_ref().join(entry.file_name()))?;
1330 } else {
1331 std::fs::copy(entry.path(), dst.as_ref().join(entry.file_name()))?;
1332 }
1333 }
1334 Ok(())
1335 }
1336
1337 #[allow(clippy::type_complexity)]
1338 pub(crate) fn setup_actor_with_loaded_cache() -> (
1339 JoinHandle<Result<String, CacheActorCacheError>>,
1340 CacheActorHandle,
1341 YangLibraryReference,
1342 SubscriptionInfo,
1343 Arc<std::sync::Mutex<HashMap<SubscriptionInfo, usize>>>,
1344 ) {
1345 let cache_dir = tempfile::tempdir().unwrap();
1346 let yang_lib_ref_path = cache_dir.path().to_path_buf().join("ietf-interfaces-lib");
1347 copy_dir_all(
1348 "../../assets/yang/ietf-interfaces",
1349 yang_lib_ref_path.as_path(),
1350 )
1351 .unwrap();
1352 let yang_lib_path = yang_lib_ref_path.join("yang-lib.xml");
1353 let yang_lib_ref = YangLibraryReference::load_from_disk(yang_lib_path, yang_lib_ref_path)
1354 .expect("Failed to load yang library reference from disk");
1355 let subscription_info = yang_lib_ref
1356 .subscriptions_info()
1357 .expect("Failed to get subscriptions info")[0]
1358 .clone();
1359 let yang_lib = yang_lib_ref.yang_library().unwrap();
1360 let schemas = yang_lib_ref.load_schemas().unwrap();
1361 let fetcher = TestYangLibFetcher::new(HashMap::from([(
1362 subscription_info.clone(),
1363 (yang_lib, schemas),
1364 )]));
1365 let fetcher_count = Arc::clone(&fetcher.fetch_counts);
1366 let (join, handle) = CacheActorHandle::new(
1367 100,
1368 either::Right(cache_dir.path().to_path_buf()),
1369 fetcher,
1370 Duration::from_secs(1),
1371 either::Either::Left(opentelemetry::global::meter("test-meter")),
1372 )
1373 .expect("Failed to create cache actor");
1374 (join, handle, yang_lib_ref, subscription_info, fetcher_count)
1375 }
1376
1377 #[allow(clippy::type_complexity)]
1378 pub(crate) fn setup_actor_with_empty_cache() -> (
1379 JoinHandle<Result<String, CacheActorCacheError>>,
1380 CacheActorHandle,
1381 SubscriptionInfo,
1382 Arc<std::sync::Mutex<HashMap<SubscriptionInfo, usize>>>,
1383 ) {
1384 let fetcher_dir = tempfile::tempdir().unwrap();
1386 let yang_lib_ref_path = fetcher_dir.path().to_path_buf().join("ietf-interfaces-lib");
1387 copy_dir_all(
1388 "../../assets/yang/ietf-interfaces",
1389 yang_lib_ref_path.as_path(),
1390 )
1391 .unwrap();
1392 let yang_lib_path = yang_lib_ref_path.join("yang-lib.xml");
1393 let yang_lib_ref = YangLibraryReference::load_from_disk(yang_lib_path, yang_lib_ref_path)
1394 .expect("Failed to load yang library reference from disk");
1395 let subscription_info = yang_lib_ref
1396 .subscriptions_info()
1397 .expect("Failed to get subscriptions info")[0]
1398 .clone();
1399 let yang_lib = yang_lib_ref.yang_library().unwrap();
1400 let schemas = yang_lib_ref.load_schemas().unwrap();
1401 let fetcher = TestYangLibFetcher::new(HashMap::from([(
1402 subscription_info.clone(),
1403 (yang_lib, schemas),
1404 )]));
1405 let fetcher_count = Arc::clone(&fetcher.fetch_counts);
1406
1407 let cache_dir = tempfile::tempdir().unwrap();
1408 let (join, handle) = CacheActorHandle::new(
1409 100,
1410 either::Right(cache_dir.path().to_path_buf()),
1411 fetcher,
1412 Duration::from_secs(1),
1413 either::Either::Left(opentelemetry::global::meter("test-meter")),
1414 )
1415 .expect("Failed to create cache actor");
1416 (join, handle, subscription_info, fetcher_count)
1417 }
1418
1419 #[tokio::test]
1420 #[tracing_test::traced_test]
1421 async fn test_cache_miss_device_fail() {
1422 let (join_handle, handle, _cached_lib_ref, _cached_subscription_info, fetcher_count) =
1423 setup_actor_with_loaded_cache();
1424 {
1425 let hits_counts = fetcher_count
1426 .lock()
1427 .expect("Failed to lock fetcher counts")
1428 .clone();
1429 assert_eq!(hits_counts.len(), 0);
1430 }
1431 let subscription_info = test_subscription_info();
1432
1433 let (tx, rx) = async_channel::unbounded();
1434 handle
1435 .request_tx()
1436 .send(CacheLookupCommand::LookupBySubscriptionInfo(
1437 subscription_info.clone(),
1438 tx,
1439 ))
1440 .await
1441 .unwrap();
1442
1443 tokio::task::yield_now().await;
1444 let response = tokio::time::timeout(Duration::from_millis(1000), rx.recv())
1445 .await
1446 .expect("timeout waiting for response")
1447 .expect("failed to receive response");
1448 assert_eq!(response.subscription_info(), &subscription_info);
1449 assert_eq!(response.yang_lib_ref(), None);
1450
1451 {
1453 let hits_counts = fetcher_count
1454 .lock()
1455 .expect("Failed to lock fetcher counts")
1456 .clone();
1457 assert_eq!(hits_counts.len(), 1);
1458 assert_eq!(hits_counts.get(&subscription_info), Some(&1));
1459 }
1460
1461 tokio::time::timeout(Duration::from_millis(100), handle.shutdown())
1462 .await
1463 .expect("timeout during shutdown")
1464 .expect("failed to shutdown actor");
1465 tokio::time::timeout(Duration::from_millis(100), join_handle)
1466 .await
1467 .expect("timeout during join")
1468 .expect("failed to join actor")
1469 .expect("cache actor failed");
1470 }
1471
1472 #[tokio::test]
1473 #[tracing_test::traced_test]
1474 async fn test_cache_miss_triggers_fetch() {
1475 let (join_handle, handle, subscription_info, fetcher_count) =
1476 setup_actor_with_empty_cache();
1477 {
1478 let hits_counts = fetcher_count
1479 .lock()
1480 .expect("Failed to lock fetcher counts")
1481 .clone();
1482 assert_eq!(hits_counts.len(), 0);
1483 }
1484
1485 let (tx, rx) = async_channel::unbounded();
1486 handle
1487 .request_tx()
1488 .send(CacheLookupCommand::LookupBySubscriptionInfo(
1489 subscription_info.clone(),
1490 tx,
1491 ))
1492 .await
1493 .unwrap();
1494
1495 tokio::task::yield_now().await;
1496 let response = tokio::time::timeout(Duration::from_millis(1000), rx.recv())
1497 .await
1498 .expect("timeout waiting for response")
1499 .expect("failed to receive response");
1500 assert_eq!(response.subscription_info(), &subscription_info);
1501 assert!(response.yang_lib_ref().is_some());
1502
1503 {
1505 let hits_counts = fetcher_count
1506 .lock()
1507 .expect("Failed to lock fetcher counts")
1508 .clone();
1509 assert_eq!(hits_counts.len(), 1);
1510 assert_eq!(hits_counts.get(&subscription_info), Some(&1));
1511 }
1512
1513 let (tx, rx) = async_channel::unbounded();
1514 handle
1515 .request_tx()
1516 .send(CacheLookupCommand::LookupBySubscriptionInfo(
1517 subscription_info.clone(),
1518 tx,
1519 ))
1520 .await
1521 .unwrap();
1522
1523 tokio::task::yield_now().await;
1524 let response = tokio::time::timeout(Duration::from_millis(1000), rx.recv())
1525 .await
1526 .expect("timeout waiting for response")
1527 .expect("failed to receive response");
1528 assert_eq!(response.subscription_info(), &subscription_info);
1529 assert!(response.yang_lib_ref().is_some());
1530
1531 {
1533 let hits_counts = fetcher_count
1534 .lock()
1535 .expect("Failed to lock fetcher counts")
1536 .clone();
1537 assert_eq!(hits_counts.len(), 1);
1538 assert_eq!(hits_counts.get(&subscription_info), Some(&1));
1539 }
1540
1541 tokio::time::timeout(Duration::from_millis(100), handle.shutdown())
1542 .await
1543 .expect("timeout during shutdown")
1544 .expect("failed to shutdown actor");
1545 tokio::time::timeout(Duration::from_millis(100), join_handle)
1546 .await
1547 .expect("timeout during join")
1548 .expect("failed to join actor")
1549 .expect("cache actor failed");
1550 }
1551
1552 #[tokio::test]
1553 async fn test_concurrent_fetch_deduplication() {
1554 let (join_handle, handle, subscription_info, fetcher_count) =
1555 setup_actor_with_empty_cache();
1556
1557 let mut tasks = FuturesOrdered::new();
1558 for _ in 0..10 {
1559 let h = handle.clone();
1560 let sub = subscription_info.clone();
1561 tasks.push_back(tokio::spawn(async move {
1562 let (tx, rx) = async_channel::unbounded();
1563 h.request_tx()
1564 .send(CacheLookupCommand::LookupBySubscriptionInfo(sub, tx))
1565 .await
1566 .unwrap();
1567 tokio::time::timeout(Duration::from_secs(1), rx.recv())
1568 .await
1569 .expect("timeout waiting for response")
1570 .unwrap()
1571 }));
1572 }
1573
1574 let results = tasks.collect::<Vec<_>>().await;
1575 assert_eq!(results.len(), 10);
1576
1577 {
1579 let count = fetcher_count.lock().unwrap();
1580 assert_eq!(count.get(&subscription_info), Some(&1));
1581 }
1582
1583 tokio::time::timeout(Duration::from_millis(100), handle.shutdown())
1585 .await
1586 .expect("timeout during shutdown")
1587 .expect("failed to shutdown actor");
1588 tokio::time::timeout(Duration::from_millis(100), join_handle)
1589 .await
1590 .expect("timeout during join")
1591 .expect("failed to join actor")
1592 .expect("cache actor failed");
1593 }
1594
1595 #[tokio::test]
1596 #[tracing_test::traced_test]
1597 async fn test_cache_miss_device_fail_by_id() {
1598 let (join_handle, handle, _cached_lib_ref, _cached_subscription_info, fetcher_count) =
1599 setup_actor_with_loaded_cache();
1600 {
1601 let hits_counts = fetcher_count
1602 .lock()
1603 .expect("Failed to lock fetcher counts")
1604 .clone();
1605 assert_eq!(hits_counts.len(), 0);
1606 }
1607 let subscription_info = test_subscription_info();
1608
1609 let (tx, rx) = async_channel::unbounded();
1610 handle
1611 .request_tx()
1612 .send(CacheLookupCommand::LookupBySubscriptionId {
1613 collector: subscription_info.collector(),
1614 interface: subscription_info.interface(),
1615 peer: subscription_info.peer(),
1616 subscription_id: subscription_info.id(),
1617 tx,
1618 })
1619 .await
1620 .unwrap();
1621
1622 tokio::task::yield_now().await;
1623 tokio::time::sleep(Duration::from_millis(100)).await;
1624 let response = tokio::time::timeout(Duration::from_millis(1000), rx.recv())
1625 .await
1626 .expect("timeout waiting for response")
1627 .expect("failed to receive response");
1628 assert_eq!(
1629 response.subscription_info(),
1630 &SubscriptionInfo::new_empty(
1631 subscription_info.collector(),
1632 subscription_info.interface(),
1633 subscription_info.peer(),
1634 subscription_info.id(),
1635 )
1636 );
1637 assert_eq!(response.yang_lib_ref(), None);
1638
1639 {
1641 let hits_counts = fetcher_count
1642 .lock()
1643 .expect("Failed to lock fetcher counts")
1644 .clone();
1645 assert_eq!(hits_counts.len(), 1);
1646 assert_eq!(
1647 hits_counts.get(&SubscriptionInfo::new_empty(
1648 subscription_info.collector(),
1649 subscription_info.interface(),
1650 subscription_info.peer(),
1651 subscription_info.id()
1652 )),
1653 Some(&1)
1654 );
1655 }
1656
1657 tokio::time::timeout(Duration::from_millis(100), handle.shutdown())
1658 .await
1659 .expect("timeout during shutdown")
1660 .expect("failed to shutdown actor");
1661 tokio::time::timeout(Duration::from_millis(100), join_handle)
1662 .await
1663 .expect("timeout during join")
1664 .expect("failed to join actor")
1665 .expect("cache actor failed");
1666 }
1667
1668 #[tokio::test]
1669 #[tracing_test::traced_test]
1670 async fn test_cache_miss_triggers_fetch_by_id() {
1671 let (join_handle, handle, subscription_info, fetcher_count) =
1672 setup_actor_with_empty_cache();
1673 {
1674 let hits_counts = fetcher_count
1675 .lock()
1676 .expect("Failed to lock fetcher counts")
1677 .clone();
1678 assert_eq!(hits_counts.len(), 0);
1679 }
1680
1681 let (tx, rx) = async_channel::unbounded();
1682 handle
1683 .request_tx()
1684 .send(CacheLookupCommand::LookupBySubscriptionId {
1685 collector: subscription_info.collector(),
1686 interface: subscription_info.interface(),
1687 peer: subscription_info.peer(),
1688 subscription_id: subscription_info.id(),
1689 tx,
1690 })
1691 .await
1692 .unwrap();
1693
1694 tokio::task::yield_now().await;
1695 let response = tokio::time::timeout(Duration::from_millis(1000), rx.recv())
1696 .await
1697 .expect("timeout waiting for response")
1698 .expect("failed to receive response");
1699 assert_eq!(response.subscription_info(), &subscription_info);
1700 assert!(response.yang_lib_ref().is_some());
1701
1702 {
1704 let hits_counts = fetcher_count
1705 .lock()
1706 .expect("Failed to lock fetcher counts")
1707 .clone();
1708 assert_eq!(hits_counts.len(), 1);
1709 assert_eq!(hits_counts.get(&subscription_info), Some(&1));
1710 }
1711
1712 let (tx, rx) = async_channel::unbounded();
1713 handle
1714 .request_tx()
1715 .send(CacheLookupCommand::LookupBySubscriptionInfo(
1716 subscription_info.clone(),
1717 tx,
1718 ))
1719 .await
1720 .unwrap();
1721
1722 tokio::task::yield_now().await;
1723 let response = tokio::time::timeout(Duration::from_millis(1000), rx.recv())
1724 .await
1725 .expect("timeout waiting for response")
1726 .expect("failed to receive response");
1727 assert_eq!(response.subscription_info(), &subscription_info);
1728 assert!(response.yang_lib_ref().is_some());
1729
1730 {
1732 let hits_counts = fetcher_count
1733 .lock()
1734 .expect("Failed to lock fetcher counts")
1735 .clone();
1736 assert_eq!(hits_counts.len(), 1);
1737 assert_eq!(hits_counts.get(&subscription_info), Some(&1));
1738 }
1739
1740 tokio::time::timeout(Duration::from_millis(100), handle.shutdown())
1741 .await
1742 .expect("timeout during shutdown")
1743 .expect("failed to shutdown actor");
1744 tokio::time::timeout(Duration::from_millis(100), join_handle)
1745 .await
1746 .expect("timeout during join")
1747 .expect("failed to join actor")
1748 .expect("cache actor failed");
1749 }
1750
1751 #[tokio::test]
1752 async fn test_concurrent_fetch_deduplication_by_id() {
1753 let (join_handle, handle, subscription_info, fetcher_count) =
1754 setup_actor_with_empty_cache();
1755
1756 let mut tasks = FuturesOrdered::new();
1757 for _ in 0..10 {
1758 let h = handle.clone();
1759 let sub = subscription_info.clone();
1760 tasks.push_back(tokio::spawn(async move {
1761 let (tx, rx) = async_channel::unbounded();
1762 h.request_tx()
1763 .send(CacheLookupCommand::LookupBySubscriptionId {
1764 collector: sub.collector(),
1765 interface: sub.interface(),
1766 peer: sub.peer(),
1767 subscription_id: sub.id(),
1768 tx,
1769 })
1770 .await
1771 .unwrap();
1772 rx.recv().await.unwrap()
1773 }));
1774 }
1775
1776 let results = tasks
1777 .collect::<Vec<_>>()
1778 .await
1779 .into_iter()
1780 .map(|res| res.unwrap())
1781 .collect::<Vec<_>>();
1782 assert_eq!(results.len(), 10);
1783
1784 {
1786 let count = fetcher_count.lock().unwrap();
1787 assert_eq!(count.get(&subscription_info), Some(&1));
1788 }
1789
1790 tokio::time::timeout(Duration::from_millis(100), handle.shutdown())
1792 .await
1793 .expect("timeout during shutdown")
1794 .expect("failed to shutdown actor");
1795 tokio::time::timeout(Duration::from_millis(100), join_handle)
1796 .await
1797 .expect("timeout during join")
1798 .expect("failed to join actor")
1799 .expect("cache actor failed");
1800 }
1801}