1use crate::error::RPCError;
2use async_trait::async_trait;
3use ckb_dao_utils::extract_dao_data;
4use ckb_db_schema::COLUMN_CELL;
5use ckb_jsonrpc_types::{
6 CellsInfo, Disk, DiskUsage, Global, MiningInfo, Network, NetworkInfo, Overview, PeerInfo,
7 SysInfo, TerminalPoolInfo,
8};
9use ckb_logger::error;
10use ckb_network::NetworkController;
11use ckb_shared::shared::Shared;
12use ckb_store::ChainStore;
13use ckb_types::utilities::compact_to_difficulty;
14use ckb_util::Mutex;
15use jsonrpc_core::Result;
16use jsonrpc_utils::rpc;
17use lru::LruCache;
18use std::sync::Arc;
19use std::time::{Duration, Instant};
20use sysinfo::{Disks as SysDisks, Networks as SysNetworks, System};
21
22pub mod ttl {
24 use std::time::Duration;
25
26 pub const SYSTEM_INFO: Duration = Duration::from_secs(5);
28
29 pub const MINING_INFO: Duration = Duration::from_secs(10);
31
32 pub const TX_POOL_INFO: Duration = Duration::from_secs(2);
34
35 pub const CELLS_INFO: Duration = Duration::from_secs(30);
37
38 pub const NETWORK_INFO: Duration = Duration::from_secs(10);
40}
41
42bitflags::bitflags! {
43 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
45 pub struct RefreshKind: u32 {
46 const NOTHING = 0b00000000;
48 const SYSTEM_INFO = 0b00000001;
50 const MINING_INFO = 0b00000010;
52 const TX_POOL_INFO = 0b00000100;
54 const CELLS_INFO = 0b00001000;
56 const NETWORK_INFO = 0b00010000;
58 const EVERYTHING = 0b00011111;
60 }
61}
62
63#[derive(Debug, Clone)]
65pub struct CacheStats {
66 pub sys_info_cached: usize,
67 pub mining_info_cached: usize,
68 pub tx_pool_info_cached: usize,
69 pub cells_info_cached: usize,
70 pub network_info_cached: usize,
71}
72
73#[derive(Clone, Debug)]
75struct CacheEntry<T> {
76 data: T,
77 timestamp: Instant,
78}
79
80impl<T> CacheEntry<T> {
81 fn new(data: T) -> Self {
82 Self {
83 data,
84 timestamp: Instant::now(),
85 }
86 }
87
88 fn is_expired(&self, ttl: Duration) -> bool {
89 self.timestamp.elapsed() > ttl
90 }
91}
92
93#[derive(Clone)]
95pub struct TerminalCache {
96 sys_info: Arc<Mutex<LruCache<(), CacheEntry<SysInfo>>>>,
98 mining_info: Arc<Mutex<LruCache<(), CacheEntry<MiningInfo>>>>,
100 tx_pool_info: Arc<Mutex<LruCache<(), CacheEntry<TerminalPoolInfo>>>>,
102 cells_info: Arc<Mutex<LruCache<(), CacheEntry<CellsInfo>>>>,
104 network_info: Arc<Mutex<LruCache<(), CacheEntry<NetworkInfo>>>>,
106}
107
108impl TerminalCache {
109 pub fn new() -> Self {
111 Self {
112 sys_info: Arc::new(Mutex::new(LruCache::new(1))),
113 mining_info: Arc::new(Mutex::new(LruCache::new(1))),
114 tx_pool_info: Arc::new(Mutex::new(LruCache::new(1))),
115 cells_info: Arc::new(Mutex::new(LruCache::new(1))),
116 network_info: Arc::new(Mutex::new(LruCache::new(1))),
117 }
118 }
119
120 pub fn get_sys_info(&self) -> Option<SysInfo> {
122 let mut cache = self.sys_info.lock();
123 if let Some(entry) = cache.get(&())
124 && !entry.is_expired(ttl::SYSTEM_INFO)
125 {
126 return Some(entry.data.clone());
127 }
128 None
129 }
130
131 pub fn set_sys_info(&self, info: SysInfo) {
133 let mut cache = self.sys_info.lock();
134 cache.put((), CacheEntry::new(info));
135 }
136
137 pub fn get_mining_info(&self) -> Option<MiningInfo> {
139 let mut cache = self.mining_info.lock();
140 if let Some(entry) = cache.get(&())
141 && !entry.is_expired(ttl::MINING_INFO)
142 {
143 return Some(entry.data.clone());
144 }
145
146 None
147 }
148
149 pub fn set_mining_info(&self, info: MiningInfo) {
151 let mut cache = self.mining_info.lock();
152 cache.put((), CacheEntry::new(info));
153 }
154
155 pub fn get_tx_pool_info(&self) -> Option<TerminalPoolInfo> {
157 let mut cache = self.tx_pool_info.lock();
158 if let Some(entry) = cache.get(&())
159 && !entry.is_expired(ttl::TX_POOL_INFO)
160 {
161 return Some(entry.data.clone());
162 }
163 None
164 }
165
166 pub fn set_tx_pool_info(&self, info: TerminalPoolInfo) {
168 let mut cache = self.tx_pool_info.lock();
169 cache.put((), CacheEntry::new(info));
170 }
171
172 pub fn get_cells_info(&self) -> Option<CellsInfo> {
174 let mut cache = self.cells_info.lock();
175 if let Some(entry) = cache.get(&())
176 && !entry.is_expired(ttl::CELLS_INFO)
177 {
178 return Some(entry.data.clone());
179 }
180
181 None
182 }
183
184 pub fn set_cells_info(&self, info: CellsInfo) {
186 let mut cache = self.cells_info.lock();
187 cache.put((), CacheEntry::new(info));
188 }
189
190 pub fn get_network_info(&self) -> Option<NetworkInfo> {
192 let mut cache = self.network_info.lock();
193 if let Some(entry) = cache.get(&())
194 && !entry.is_expired(ttl::NETWORK_INFO)
195 {
196 return Some(entry.data.clone());
197 }
198
199 None
200 }
201
202 pub fn set_network_info(&self, info: NetworkInfo) {
204 let mut cache = self.network_info.lock();
205 cache.put((), CacheEntry::new(info));
206 }
207
208 pub fn clear_specific(&self, refresh: RefreshKind) {
210 if refresh.contains(RefreshKind::SYSTEM_INFO) {
211 self.sys_info.lock().clear();
212 }
213 if refresh.contains(RefreshKind::MINING_INFO) {
214 self.mining_info.lock().clear();
215 }
216 if refresh.contains(RefreshKind::TX_POOL_INFO) {
217 self.tx_pool_info.lock().clear();
218 }
219 if refresh.contains(RefreshKind::CELLS_INFO) {
220 self.cells_info.lock().clear();
221 }
222 if refresh.contains(RefreshKind::NETWORK_INFO) {
223 self.network_info.lock().clear();
224 }
225 }
226
227 pub fn get_stats(&self) -> CacheStats {
229 CacheStats {
230 sys_info_cached: self.sys_info.lock().len(),
231 mining_info_cached: self.mining_info.lock().len(),
232 tx_pool_info_cached: self.tx_pool_info.lock().len(),
233 cells_info_cached: self.cells_info.lock().len(),
234 network_info_cached: self.network_info.lock().len(),
235 }
236 }
237
238 pub fn clear_all(&self) {
240 self.sys_info.lock().clear();
241 self.mining_info.lock().clear();
242 self.tx_pool_info.lock().clear();
243 self.cells_info.lock().clear();
244 self.network_info.lock().clear();
245 }
246}
247
248impl Default for TerminalCache {
249 fn default() -> Self {
250 Self::new()
251 }
252}
253
254#[rpc(openrpc)]
279#[async_trait]
280pub trait TerminalRpc {
281 #[rpc(name = "get_overview")]
428 fn get_overview(&self, refresh: Option<u32>) -> Result<Overview>;
429}
430
431#[derive(Clone)]
432pub(crate) struct TerminalRpcImpl {
433 pub shared: Shared,
434 pub network_controller: NetworkController,
435 pub cache: TerminalCache,
436}
437
438#[async_trait]
439impl TerminalRpc for TerminalRpcImpl {
440 fn get_overview(&self, refresh: Option<u32>) -> Result<Overview> {
441 let refresh = refresh
442 .and_then(RefreshKind::from_bits)
443 .unwrap_or(RefreshKind::NOTHING);
444
445 if refresh.contains(RefreshKind::EVERYTHING) {
447 self.cache.clear_all();
448 }
449
450 let sys = self.get_sys_info(refresh)?;
451 let mining = self.get_mining_info(refresh)?;
452 let pool = self.get_tx_pool_info(refresh)?;
453 let cells = self.get_cells_info(refresh)?;
454 let network = self.get_network_info(refresh)?;
455
456 Ok(Overview {
457 sys,
458 cells,
459 mining,
460 pool,
461 network,
462 version: self.network_controller.version().to_owned(),
463 })
464 }
465}
466
467impl TerminalRpcImpl {
468 fn get_mining_info(&self, refresh: RefreshKind) -> Result<MiningInfo> {
469 if !refresh.contains(RefreshKind::MINING_INFO)
471 && let Some(cached) = self.cache.get_mining_info()
472 {
473 return Ok(cached);
474 }
475
476 let current_epoch_ext =
478 self.shared
479 .snapshot()
480 .get_current_epoch_ext()
481 .ok_or_else(|| {
482 RPCError::custom(
483 RPCError::CKBInternalError,
484 "failed to get current epoch_ext",
485 )
486 })?;
487 let difficulty = compact_to_difficulty(current_epoch_ext.compact_target());
488 let mining_info = MiningInfo {
489 difficulty,
490 hash_rate: current_epoch_ext.previous_epoch_hash_rate().to_owned(),
493 };
494
495 self.cache.set_mining_info(mining_info.clone());
497 Ok(mining_info)
498 }
499
500 fn get_sys_info(&self, refresh: RefreshKind) -> Result<SysInfo> {
501 if !refresh.contains(RefreshKind::SYSTEM_INFO)
503 && let Some(cached) = self.cache.get_sys_info()
504 {
505 return Ok(cached);
506 }
507
508 let mut sys = System::new_all();
510 sys.refresh_all();
511
512 let total_memory = sys.total_memory();
513 let used_memory = sys.used_memory();
514 let global_cpu_usage = sys.global_cpu_usage();
515 let sys_disks = SysDisks::new_with_refreshed_list();
516 let disks = sys_disks
517 .iter()
518 .map(|disk| Disk {
519 total_space: disk.total_space(),
520 available_space: disk.available_space(),
521 is_removable: disk.is_removable(),
522 })
523 .collect();
524 let sys_networks = SysNetworks::new_with_refreshed_list();
525 let networks = sys_networks
526 .iter()
527 .map(|(name, data)| Network {
528 interface_name: name.clone(),
529 received: data.received(),
530 total_received: data.total_received(),
531 transmitted: data.transmitted(),
532 total_transmitted: data.total_transmitted(),
533 })
534 .collect();
535
536 let global = Global {
537 total_memory,
538 used_memory,
539 global_cpu_usage,
540 disks,
541 networks,
542 };
543
544 let process = sys
545 .process(
546 sysinfo::get_current_pid()
547 .map_err(|e| RPCError::custom(RPCError::CKBInternalError, e))?,
548 )
549 .ok_or_else(|| {
550 RPCError::custom(RPCError::CKBInternalError, "failed to get current process")
551 })?;
552
553 let sys_disk_usage = process.disk_usage();
554 let sys_info = SysInfo {
555 global,
556 cpu_usage: process.cpu_usage(),
557 memory: process.memory(),
558 disk_usage: DiskUsage {
559 total_written_bytes: sys_disk_usage.total_written_bytes,
560 written_bytes: sys_disk_usage.written_bytes,
561 total_read_bytes: sys_disk_usage.total_read_bytes,
562 read_bytes: sys_disk_usage.read_bytes,
563 },
564 virtual_memory: process.virtual_memory(),
565 };
566
567 self.cache.set_sys_info(sys_info.clone());
569 Ok(sys_info)
570 }
571
572 fn get_tx_pool_info(&self, refresh: RefreshKind) -> Result<TerminalPoolInfo> {
573 if !refresh.contains(RefreshKind::TX_POOL_INFO)
575 && let Some(cached) = self.cache.get_tx_pool_info()
576 {
577 return Ok(cached);
578 }
579
580 let tx_pool = self.shared.tx_pool_controller();
582 let get_tx_pool_info = tx_pool.get_tx_pool_info();
583 if let Err(e) = get_tx_pool_info {
584 error!("Send get_tx_pool_info request error {}", e);
585 return Err(RPCError::ckb_internal_error(e));
586 };
587
588 let info = get_tx_pool_info.unwrap();
589
590 let block_template = self
591 .shared
592 .get_block_template(None, None, None)
593 .map_err(|err| {
594 error!("Send get_block_template request error {}", err);
595 RPCError::ckb_internal_error(err)
596 })?
597 .map_err(|err| {
598 error!("Get_block_template result error {}", err);
599 RPCError::from_any_error(err)
600 })?;
601
602 let total_recent_reject_num = tx_pool.get_total_recent_reject_num().map_err(|err| {
603 error!("Get_total_recent_reject_num result error {}", err);
604 RPCError::from_any_error(err)
605 })?;
606
607 let tx_pool_info = TerminalPoolInfo {
608 pending: (info.pending_size as u64).into(),
609 proposed: (info.proposed_size as u64).into(),
610 orphan: (info.orphan_size as u64).into(),
611 committing: (block_template.transactions.len() as u64).into(),
612 total_recent_reject_num: total_recent_reject_num.unwrap_or(0).into(),
613 total_tx_size: (info.total_tx_size as u64).into(),
614 total_tx_cycles: info.total_tx_cycles.into(),
615 last_txs_updated_at: info.last_txs_updated_at.into(),
616 max_tx_pool_size: info.max_tx_pool_size.into(),
617 };
618
619 self.cache.set_tx_pool_info(tx_pool_info.clone());
621 Ok(tx_pool_info)
622 }
623
624 fn get_cells_info(&self, refresh: RefreshKind) -> Result<CellsInfo> {
625 if !refresh.contains(RefreshKind::CELLS_INFO)
627 && let Some(cached) = self.cache.get_cells_info()
628 {
629 return Ok(cached);
630 }
631
632 let snapshot = self.shared.cloned_snapshot();
634 let tip_header = snapshot.tip_header();
635 let (_ar, _c, _s, u) = extract_dao_data(tip_header.dao());
636 let estimate_live_cells_num = self
637 .shared
638 .store()
639 .estimate_num_keys_cf(COLUMN_CELL)
640 .map_err(|err| {
641 error!("estimate_num_keys_cf error {}", err);
642 RPCError::ckb_internal_error(err)
643 })?;
644
645 let cells_info = CellsInfo {
646 total_occupied_capacities: u.into(),
647 estimate_live_cells_num: estimate_live_cells_num.unwrap_or(0).into(),
648 };
649
650 self.cache.set_cells_info(cells_info.clone());
652 Ok(cells_info)
653 }
654
655 fn get_network_info(&self, refresh: RefreshKind) -> Result<NetworkInfo> {
656 if !refresh.contains(RefreshKind::NETWORK_INFO)
658 && let Some(cached) = self.cache.get_network_info()
659 {
660 return Ok(cached);
661 }
662
663 let peers = self.network_controller.connected_peers();
665 let total_peers = peers.len();
666 let mut outbound_peers = 0;
667 let mut inbound_peers = 0;
668 let mut peer_infos = Vec::new();
669
670 for (peer_index, peer) in peers {
671 if peer.is_outbound() {
673 outbound_peers += 1;
674 } else {
675 inbound_peers += 1;
676 }
677
678 let peer_id = peer_index.value();
680 let is_outbound = peer.is_outbound();
681 let latency_ms = if let Some(rtt) = peer.ping_rtt {
682 rtt.as_millis() as u64
683 } else {
684 0
685 };
686
687 peer_infos.push(PeerInfo {
688 peer_id,
689 is_outbound,
690 latency_ms: latency_ms.into(),
691 address: peer.connected_addr.to_string(),
692 });
693 }
694
695 let network_info = NetworkInfo {
696 connected_peers: (total_peers as u64).into(),
697 outbound_peers: (outbound_peers as u64).into(),
698 inbound_peers: (inbound_peers as u64).into(),
699 peers: peer_infos,
700 };
701
702 self.cache.set_network_info(network_info.clone());
704 Ok(network_info)
705 }
706}
707
708#[cfg(test)]
709mod tests {
710 use super::*;
711 use std::time::Duration;
712
713 #[test]
714 fn test_cache_entry_expiration() {
715 let entry = CacheEntry::new("test_data");
716 assert!(!entry.is_expired(Duration::from_secs(1)));
717
718 let _ = entry.is_expired(Duration::from_millis(0));
721 }
722
723 #[test]
724 fn test_refresh_kind_flags() {
725 let nothing = RefreshKind::NOTHING;
726 assert!(!nothing.contains(RefreshKind::SYSTEM_INFO));
727 assert!(!nothing.contains(RefreshKind::MINING_INFO));
728
729 let system = RefreshKind::SYSTEM_INFO;
730 assert!(system.contains(RefreshKind::SYSTEM_INFO));
731 assert!(!system.contains(RefreshKind::MINING_INFO));
732
733 let all = RefreshKind::EVERYTHING;
734 assert!(all.contains(RefreshKind::SYSTEM_INFO));
735 assert!(all.contains(RefreshKind::MINING_INFO));
736 assert!(all.contains(RefreshKind::TX_POOL_INFO));
737 assert!(all.contains(RefreshKind::CELLS_INFO));
738 }
739
740 #[test]
741 fn test_terminal_cache_basic_operations() {
742 let cache = TerminalCache::new();
743
744 assert!(cache.get_sys_info().is_none());
746 assert!(cache.get_mining_info().is_none());
747
748 let sys_info = SysInfo {
750 global: Global {
751 total_memory: 1000,
752 used_memory: 500,
753 global_cpu_usage: 50.0,
754 disks: vec![],
755 networks: vec![],
756 },
757 cpu_usage: 0.0,
758 memory: 0,
759 disk_usage: DiskUsage {
760 total_written_bytes: 0,
761 written_bytes: 0,
762 total_read_bytes: 0,
763 read_bytes: 0,
764 },
765 virtual_memory: 0,
766 };
767
768 cache.set_sys_info(sys_info);
769 assert!(cache.get_sys_info().is_some());
770
771 cache.clear_all();
773 assert!(cache.get_sys_info().is_none());
774 }
775
776 #[test]
777 fn test_network_info_basic_operations() {
778 let cache = TerminalCache::new();
779
780 assert!(cache.get_network_info().is_none());
782
783 let network_info = NetworkInfo {
785 connected_peers: 5u64.into(),
786 outbound_peers: 3u64.into(),
787 inbound_peers: 2u64.into(),
788 peers: vec![
789 PeerInfo {
790 peer_id: 0,
791 is_outbound: true,
792 latency_ms: 150u64.into(),
793 address: "/ip4/192.168.1.100/tcp/8114".to_string(),
794 },
795 PeerInfo {
796 peer_id: 1,
797 is_outbound: true,
798 latency_ms: 50u64.into(),
799 address: "/ip4/192.168.1.101/tcp/8114".to_string(),
800 },
801 PeerInfo {
802 peer_id: 2,
803 is_outbound: false,
804 latency_ms: 300u64.into(),
805 address: "/ip4/192.168.1.102/tcp/8114".to_string(),
806 },
807 PeerInfo {
808 peer_id: 3,
809 is_outbound: false,
810 latency_ms: 100u64.into(),
811 address: "/ip4/192.168.1.103/tcp/8114".to_string(),
812 },
813 PeerInfo {
814 peer_id: 4,
815 is_outbound: true,
816 latency_ms: 0u64.into(),
817 address: "/ip4/192.168.1.104/tcp/8114".to_string(),
818 },
819 ],
820 };
821
822 cache.set_network_info(network_info);
823 let cached = cache
824 .get_network_info()
825 .expect("Should have cached network info");
826
827 assert_eq!(cached.connected_peers, 5u64.into());
828 assert_eq!(cached.outbound_peers, 3u64.into());
829 assert_eq!(cached.inbound_peers, 2u64.into());
830 assert_eq!(cached.peers.len(), 5);
831 assert_eq!(cached.peers[0].peer_id, 0);
832 assert!(cached.peers[0].is_outbound);
833 assert_eq!(cached.peers[0].latency_ms, 150u64.into());
834 }
835
836 #[test]
837 fn test_refresh_kind_network_flag() {
838 let nothing = RefreshKind::NOTHING;
839 assert!(!nothing.contains(RefreshKind::NETWORK_INFO));
840
841 let network = RefreshKind::NETWORK_INFO;
842 assert!(network.contains(RefreshKind::NETWORK_INFO));
843 assert!(!network.contains(RefreshKind::SYSTEM_INFO));
844
845 let all = RefreshKind::EVERYTHING;
846 assert!(all.contains(RefreshKind::NETWORK_INFO));
847 assert!(all.contains(RefreshKind::SYSTEM_INFO));
848 assert!(all.contains(RefreshKind::MINING_INFO));
849 assert!(all.contains(RefreshKind::TX_POOL_INFO));
850 assert!(all.contains(RefreshKind::CELLS_INFO));
851 }
852
853 #[test]
854 fn test_cache_stats_includes_network() {
855 let cache = TerminalCache::new();
856 let stats = cache.get_stats();
857
858 assert_eq!(stats.sys_info_cached, 0);
860 assert_eq!(stats.mining_info_cached, 0);
861 assert_eq!(stats.tx_pool_info_cached, 0);
862 assert_eq!(stats.cells_info_cached, 0);
863 assert_eq!(stats.network_info_cached, 0);
864
865 let network_info = NetworkInfo::default();
867 cache.set_network_info(network_info);
868
869 let stats = cache.get_stats();
870 assert_eq!(stats.network_info_cached, 1);
871 }
872}