Skip to main content

quic_p2p/
bootstrap_cache.rs

1// Copyright 2019 MaidSafe.net limited.
2//
3// This SAFE Network Software is licensed to you under the MIT license <LICENSE-MIT
4// http://opensource.org/licenses/MIT> or the Modified BSD license <LICENSE-BSD
5// https://opensource.org/licenses/BSD-3-Clause>, at your option. This file may not be copied,
6// modified, or distributed except according to those terms. Please review the Licences for the
7// specific language governing permissions and limitations relating to use of the SAFE Network
8// Software.
9
10use crate::dirs::Dirs;
11use crate::utils;
12use crate::{QuicP2pError, R};
13use log::info;
14use std::{
15    collections::{HashSet, VecDeque},
16    fs, io,
17    net::SocketAddr,
18    path::PathBuf,
19};
20
21/// Maximum peers in the cache.
22const MAX_CACHE_SIZE: usize = 200;
23
24/// A very simple LRU like struct that writes itself to disk every 10 entries added.
25pub struct BootstrapCache {
26    peers: VecDeque<SocketAddr>,
27    cache_path: PathBuf,
28    add_count: u8,
29    hard_coded_contacts: HashSet<SocketAddr>,
30}
31
32impl BootstrapCache {
33    /// Tries to construct bootstrap cache backed by a file.
34    /// Fails if not able to obtain write permissions for a cache file.
35    ///
36    /// ## Args
37    ///
38    /// - hard_coded_contacts: these peers are hard coded into the binary and should
39    ///   not be cached upon successful connection.
40    pub fn new(
41        hard_coded_contacts: HashSet<SocketAddr>,
42        user_override: Option<&Dirs>,
43    ) -> R<BootstrapCache> {
44        let path = |dir: &Dirs| {
45            let path = dir.cache_dir();
46            path.join("bootstrap_cache")
47        };
48
49        let cache_path = user_override.map_or_else(
50            || Ok::<_, QuicP2pError>(path(&utils::project_dir()?)),
51            |d| Ok(path(d)),
52        )?;
53
54        let peers = if cache_path.exists() {
55            utils::read_from_disk(&cache_path)?
56        } else {
57            let cache_dir = cache_path
58                .parent()
59                .ok_or_else(|| io::ErrorKind::NotFound.into())
60                .map_err(QuicP2pError::Io)?;
61            fs::create_dir_all(&cache_dir)?;
62            Default::default()
63        };
64
65        Ok(BootstrapCache {
66            peers,
67            cache_path,
68            add_count: 0u8,
69            hard_coded_contacts,
70        })
71    }
72
73    pub fn peers_mut(&mut self) -> &mut VecDeque<SocketAddr> {
74        &mut self.peers
75    }
76
77    pub fn peers(&self) -> &VecDeque<SocketAddr> {
78        &self.peers
79    }
80
81    pub fn hard_coded_contacts(&self) -> &HashSet<SocketAddr> {
82        &self.hard_coded_contacts
83    }
84
85    /// Caches given peer if it's not in hard coded contacts.
86    pub fn add_peer(&mut self, peer: SocketAddr) {
87        if self.hard_coded_contacts.contains(&peer) {
88            return;
89        }
90
91        if self.peers.contains(&peer) {
92            self.move_to_cache_top(peer);
93        } else {
94            self.insert_new(peer);
95        }
96    }
97
98    fn insert_new(&mut self, peer: SocketAddr) {
99        self.peers.push_back(peer);
100        self.add_count += 1;
101        if self.peers.len() > MAX_CACHE_SIZE {
102            let _ = self.peers.pop_front();
103        }
104        self.try_sync_to_disk();
105    }
106
107    fn move_to_cache_top(&mut self, peer: SocketAddr) {
108        if let Some(pos) = self.peers.iter().position(|p| *p == peer) {
109            let _ = self.peers.remove(pos);
110            self.peers.push_back(peer);
111        }
112    }
113
114    /// Write cached peers to disk every 10 inserted peers.
115    fn try_sync_to_disk(&mut self) {
116        if self.add_count > 9 {
117            if let Err(e) = utils::write_to_disk(&self.cache_path, &self.peers) {
118                info!("Failed to write bootstrap cache to disk: {}", e);
119            }
120            self.add_count = 0;
121        }
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128    use crate::test_utils::{make_node_addr, rand_node_addr, test_dirs};
129    use unwrap::unwrap;
130
131    mod add_peer {
132        use super::*;
133
134        #[test]
135        fn when_10_peers_are_added_they_are_synced_to_disk() {
136            let dirs = test_dirs();
137            let mut cache = unwrap!(BootstrapCache::new(Default::default(), Some(&dirs)));
138
139            for _ in 0..10 {
140                cache.add_peer(rand_node_addr());
141            }
142
143            assert_eq!(cache.peers.len(), 10);
144
145            let cache = unwrap!(BootstrapCache::new(Default::default(), Some(&dirs)));
146            assert_eq!(cache.peers.len(), 10);
147        }
148
149        #[test]
150        fn when_given_peer_is_in_hard_coded_contacts_it_is_not_cached() {
151            let peer1 = rand_node_addr();
152            let peer2 = rand_node_addr();
153            let mut hard_coded: HashSet<_> = Default::default();
154            assert!(hard_coded.insert(peer1));
155
156            let dirs = test_dirs();
157            let mut cache = BootstrapCache::new(hard_coded, Some(&dirs)).unwrap();
158
159            cache.add_peer(peer1);
160            cache.add_peer(peer2);
161
162            let peers: Vec<_> = cache.peers.iter().cloned().collect();
163            assert_eq!(peers, vec![peer2]);
164        }
165
166        #[test]
167        fn it_caps_cache_size() {
168            let dirs = test_dirs();
169            let port_base = 5000;
170
171            let mut cache = unwrap!(BootstrapCache::new(Default::default(), Some(&dirs)));
172
173            for i in 0..MAX_CACHE_SIZE {
174                cache.add_peer(make_node_addr(port_base + i as u16));
175            }
176            assert_eq!(cache.peers.len(), MAX_CACHE_SIZE);
177
178            cache.add_peer(make_node_addr(port_base + MAX_CACHE_SIZE as u16));
179            assert_eq!(cache.peers.len(), MAX_CACHE_SIZE);
180        }
181    }
182
183    mod move_to_cache_top {
184        use super::*;
185
186        #[test]
187        fn it_moves_given_node_to_the_top_of_the_list() {
188            let dirs = test_dirs();
189            let mut cache = BootstrapCache::new(Default::default(), Some(&dirs)).unwrap();
190            let peer1 = rand_node_addr();
191            let peer2 = rand_node_addr();
192            let peer3 = rand_node_addr();
193            cache.add_peer(peer1);
194            cache.add_peer(peer2);
195            cache.add_peer(peer3);
196
197            cache.move_to_cache_top(peer2);
198
199            let peers: Vec<_> = cache.peers.iter().cloned().collect();
200            assert_eq!(peers, vec![peer1, peer3, peer2]);
201        }
202    }
203}