Node table limiting and cache for node filter (#10288)
* Fix nasty typo in NodeTable::update (add ;) * Add limiting for NodeTable * Add cache for NodeFilter * Use expect instead of unwrap * Move node in ordered_ids if it exists there in note_failure and note_success + fix expect msg * Add comment * Improve code style * DRY in note_failure and note_success * Fix nodes ordering * Simplify match expression * Add tests for get_index_to_insert * Remove get_mut method from NodeTable, Add get method to NodeTable * Fix table_last_contact_order for macos failing because of lost nanosecond precision
This commit is contained in:
parent
10e1787ad1
commit
8132d38b50
@ -37,13 +37,17 @@ extern crate tempdir;
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::sync::Weak;
|
||||
|
||||
use ethcore::client::{BlockChainClient, BlockId};
|
||||
use ethcore::client::{BlockChainClient, BlockId, ChainNotify, NewBlocks};
|
||||
|
||||
use ethereum_types::{H256, Address};
|
||||
use ethabi::FunctionOutputDecoder;
|
||||
use network::{ConnectionFilter, ConnectionDirection};
|
||||
use devp2p::NodeId;
|
||||
use devp2p::MAX_NODES_IN_TABLE;
|
||||
use parking_lot::RwLock;
|
||||
|
||||
use_contract!(peer_set, "res/peer_set.json");
|
||||
|
||||
@ -51,14 +55,27 @@ use_contract!(peer_set, "res/peer_set.json");
|
||||
pub struct NodeFilter {
|
||||
client: Weak<BlockChainClient>,
|
||||
contract_address: Address,
|
||||
cache: RwLock<Cache>
|
||||
}
|
||||
|
||||
struct Cache {
|
||||
cache: HashMap<NodeId, bool>,
|
||||
order: VecDeque<NodeId>
|
||||
}
|
||||
|
||||
// Increase cache size due to possible reserved peers, which do not count in the node table size
|
||||
pub const CACHE_SIZE: usize = MAX_NODES_IN_TABLE + 1024;
|
||||
|
||||
impl NodeFilter {
|
||||
/// Create a new instance. Accepts a contract address.
|
||||
pub fn new(client: Weak<BlockChainClient>, contract_address: Address) -> NodeFilter {
|
||||
NodeFilter {
|
||||
client,
|
||||
contract_address,
|
||||
cache: RwLock::new(Cache{
|
||||
cache: HashMap::with_capacity(CACHE_SIZE),
|
||||
order: VecDeque::with_capacity(CACHE_SIZE)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -70,6 +87,10 @@ impl ConnectionFilter for NodeFilter {
|
||||
None => return false,
|
||||
};
|
||||
|
||||
if let Some(allowed) = self.cache.read().cache.get(connecting_id) {
|
||||
return *allowed;
|
||||
}
|
||||
|
||||
let address = self.contract_address;
|
||||
let own_low = H256::from_slice(&own_id[0..32]);
|
||||
let own_high = H256::from_slice(&own_id[32..64]);
|
||||
@ -83,11 +104,26 @@ impl ConnectionFilter for NodeFilter {
|
||||
debug!("Error callling peer set contract: {:?}", e);
|
||||
false
|
||||
});
|
||||
|
||||
let mut cache = self.cache.write();
|
||||
if cache.cache.len() == CACHE_SIZE {
|
||||
let poped = cache.order.pop_front().unwrap();
|
||||
cache.cache.remove(&poped).is_none();
|
||||
};
|
||||
if cache.cache.insert(*connecting_id, allowed).is_none() {
|
||||
cache.order.push_back(*connecting_id);
|
||||
}
|
||||
allowed
|
||||
}
|
||||
}
|
||||
|
||||
impl ChainNotify for NodeFilter {
|
||||
fn new_blocks(&self, _new_blocks: NewBlocks) {
|
||||
let mut cache = self.cache.write();
|
||||
cache.cache.clear();
|
||||
cache.order.clear();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::sync::{Arc, Weak};
|
||||
|
@ -583,7 +583,9 @@ fn execute_impl<Cr, Rr>(cmd: RunCmd, logger: Arc<RotatingLogger>, on_client_rq:
|
||||
let private_tx_provider = private_tx_service.provider();
|
||||
let connection_filter = connection_filter_address.map(|a| Arc::new(NodeFilter::new(Arc::downgrade(&client) as Weak<BlockChainClient>, a)));
|
||||
let snapshot_service = service.snapshot_service();
|
||||
|
||||
if let Some(filter) = connection_filter.clone() {
|
||||
service.add_notify(filter.clone());
|
||||
}
|
||||
// initialize the local node information store.
|
||||
let store = {
|
||||
let db = service.db();
|
||||
|
@ -616,8 +616,8 @@ impl Host {
|
||||
|
||||
let socket = {
|
||||
let address = {
|
||||
let mut nodes = self.nodes.write();
|
||||
if let Some(node) = nodes.get_mut(id) {
|
||||
let mut nodes = self.nodes.read();
|
||||
if let Some(node) = nodes.get(id) {
|
||||
node.endpoint.address
|
||||
} else {
|
||||
debug!(target: "network", "Connection to expired node aborted");
|
||||
|
@ -113,6 +113,6 @@ pub use service::NetworkService;
|
||||
pub use host::NetworkContext;
|
||||
|
||||
pub use io::TimerToken;
|
||||
pub use node_table::{validate_node_url, NodeId};
|
||||
pub use node_table::{validate_node_url, NodeId, MAX_NODES_IN_TABLE};
|
||||
|
||||
const PROTOCOL_VERSION: u32 = 5;
|
||||
|
@ -23,6 +23,7 @@ use serde_json;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::iter::FromIterator;
|
||||
use std::net::{SocketAddr, ToSocketAddrs, SocketAddrV4, SocketAddrV6, Ipv4Addr, Ipv6Addr};
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
@ -235,22 +236,27 @@ impl Hash for Node {
|
||||
}
|
||||
}
|
||||
|
||||
const MAX_NODES: usize = 1024;
|
||||
pub const MAX_NODES_IN_TABLE: usize = 4096;
|
||||
const MAX_NODES_IN_FILE: usize = 1024;
|
||||
const NODES_FILE: &str = "nodes.json";
|
||||
|
||||
/// Node table backed by disk file.
|
||||
pub struct NodeTable {
|
||||
nodes: HashMap<NodeId, Node>,
|
||||
ordered_ids: Vec<NodeId>,
|
||||
useless_nodes: HashSet<NodeId>,
|
||||
path: Option<String>,
|
||||
}
|
||||
|
||||
impl NodeTable {
|
||||
pub fn new(path: Option<String>) -> NodeTable {
|
||||
let nodes = NodeTable::load(path.clone());
|
||||
let ordered_ids = NodeTable::make_ordered_entries(&nodes).iter().map(|m| m.id).collect();
|
||||
NodeTable {
|
||||
path: path.clone(),
|
||||
nodes: NodeTable::load(path),
|
||||
path,
|
||||
nodes,
|
||||
useless_nodes: HashSet::new(),
|
||||
ordered_ids
|
||||
}
|
||||
}
|
||||
|
||||
@ -258,24 +264,72 @@ impl NodeTable {
|
||||
pub fn add_node(&mut self, mut node: Node) {
|
||||
// preserve node last_contact
|
||||
node.last_contact = self.nodes.get(&node.id).and_then(|n| n.last_contact);
|
||||
self.nodes.insert(node.id, node);
|
||||
let id = node.id;
|
||||
if self.ordered_ids.len() == MAX_NODES_IN_TABLE {
|
||||
self.nodes.remove(&self.ordered_ids.pop().expect("ordered_ids is not empty; qed"));
|
||||
};
|
||||
let index = self.get_index_to_insert(node.last_contact);
|
||||
if self.nodes.insert(node.id, node).is_none() {
|
||||
self.ordered_ids.insert(index, id);
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns a list of ordered nodes according to their most recent contact
|
||||
/// and filtering useless nodes. The algorithm for creating the sorted nodes
|
||||
/// is:
|
||||
/// Get index in the ordered entries vector to insert node based on its last contact value
|
||||
fn get_index_to_insert(&self, last_contact: Option<NodeContact>) -> usize {
|
||||
let len = self.ordered_ids.len();
|
||||
let mut index = len;
|
||||
match last_contact {
|
||||
Some(NodeContact::Success(last_contact_time)) => {
|
||||
if let Some(i) = self.ordered_ids.iter().position(|&item| {
|
||||
match self.nodes.get(&item).expect("nodes and ordered_ids do not get out of sync; qed").last_contact {
|
||||
Some(NodeContact::Success(last)) => last < last_contact_time,
|
||||
_ => true
|
||||
}
|
||||
}) { index = i; };
|
||||
},
|
||||
None => {
|
||||
if let Some(i) = self.ordered_ids.iter().position(|&item| {
|
||||
match self.nodes.get(&item).expect("nodes and ordered_ids do not get out of sync; qed").last_contact {
|
||||
Some(NodeContact::Success(_)) => false,
|
||||
_ => true
|
||||
}
|
||||
}) { index = i; };
|
||||
},
|
||||
Some(NodeContact::Failure(last_contact_time)) => {
|
||||
if let Some(i) = self.ordered_ids.iter().rev().position(|&item| {
|
||||
match self.nodes.get(&item).expect("nodes and ordered_ids do not get out of sync; qed").last_contact {
|
||||
Some(NodeContact::Failure(last)) => last < last_contact_time,
|
||||
_ => true
|
||||
}
|
||||
}) { index = len - i; };
|
||||
}
|
||||
};
|
||||
index
|
||||
}
|
||||
|
||||
/// Returns a list of ordered entries from table
|
||||
fn ordered(&self) -> Vec<&Node> {
|
||||
Vec::from_iter(
|
||||
self.ordered_ids
|
||||
.iter()
|
||||
.filter(|id| !self.useless_nodes.contains(&id))
|
||||
.map(|id| self.nodes.get(&id).expect("nodes and ordered_ids do not get out of sync; qed"))
|
||||
)
|
||||
}
|
||||
|
||||
/// Makes a list of ordered nodes according to their most recent contact.
|
||||
/// The algorithm for creating the sorted nodes is:
|
||||
/// - Contacts that aren't recent (older than 1 week) are discarded
|
||||
/// - (1) Nodes with a successful contact are ordered (most recent success first)
|
||||
/// - (2) Nodes with unknown contact (older than 1 week or new nodes) are randomly shuffled
|
||||
/// - (3) Nodes with a failed contact are ordered (oldest failure first)
|
||||
/// - The final result is the concatenation of (1), (2) and (3)
|
||||
fn ordered_entries(&self) -> Vec<&Node> {
|
||||
fn make_ordered_entries(node_table: &HashMap<NodeId, Node>) -> Vec<&Node> {
|
||||
let mut success = Vec::new();
|
||||
let mut failures = Vec::new();
|
||||
let mut unknown = Vec::new();
|
||||
|
||||
let nodes = self.nodes.values()
|
||||
.filter(|n| !self.useless_nodes.contains(&n.id));
|
||||
let nodes = node_table.values();
|
||||
|
||||
for node in nodes {
|
||||
// discard contact points older that aren't recent
|
||||
@ -316,7 +370,7 @@ impl NodeTable {
|
||||
/// Returns node ids sorted by failure percentage, for nodes with the same failure percentage the absolute number of
|
||||
/// failures is considered.
|
||||
pub fn nodes(&self, filter: &IpFilter) -> Vec<NodeId> {
|
||||
self.ordered_entries().iter()
|
||||
self.ordered().iter()
|
||||
.filter(|n| n.endpoint.is_allowed(&filter))
|
||||
.map(|n| n.id)
|
||||
.collect()
|
||||
@ -325,15 +379,15 @@ impl NodeTable {
|
||||
/// Ordered list of all entries by failure percentage, for nodes with the same failure percentage the absolute
|
||||
/// number of failures is considered.
|
||||
pub fn entries(&self) -> Vec<NodeEntry> {
|
||||
self.ordered_entries().iter().map(|n| NodeEntry {
|
||||
self.ordered().iter().map(|n| NodeEntry {
|
||||
endpoint: n.endpoint.clone(),
|
||||
id: n.id,
|
||||
}).collect()
|
||||
}
|
||||
|
||||
/// Get particular node
|
||||
pub fn get_mut(&mut self, id: &NodeId) -> Option<&mut Node> {
|
||||
self.nodes.get_mut(id)
|
||||
pub fn get(&self, id: &NodeId) -> Option<&Node> {
|
||||
self.nodes.get(id)
|
||||
}
|
||||
|
||||
/// Check if a node exists in the table.
|
||||
@ -344,28 +398,49 @@ impl NodeTable {
|
||||
/// Apply table changes coming from discovery
|
||||
pub fn update(&mut self, mut update: TableUpdates, reserved: &HashSet<NodeId>) {
|
||||
for (_, node) in update.added.drain() {
|
||||
let entry = self.nodes.entry(node.id).or_insert_with(|| Node::new(node.id, node.endpoint.clone()));
|
||||
let mut add = false;
|
||||
{
|
||||
let entry = self.nodes.entry(node.id).or_insert_with(|| {
|
||||
add = true;
|
||||
Node::new(node.id, node.endpoint.clone())
|
||||
});
|
||||
entry.endpoint = node.endpoint;
|
||||
}
|
||||
if add {
|
||||
if self.ordered_ids.len() == MAX_NODES_IN_TABLE {
|
||||
self.nodes.remove(&self.ordered_ids.pop().expect("ordered_ids is not empty; qed"));
|
||||
};
|
||||
let index = self.get_index_to_insert(None);
|
||||
self.ordered_ids.insert(index, node.id);
|
||||
};
|
||||
};
|
||||
for r in update.removed {
|
||||
if !reserved.contains(&r) {
|
||||
self.ordered_ids.iter().position(|&i| r == i).map(|p| self.ordered_ids.remove(p));
|
||||
self.nodes.remove(&r);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn update_ordered_ids(&mut self, id: &NodeId, last_contact: Option<NodeContact>) {
|
||||
if let Some(node) = self.nodes.get_mut(id) {
|
||||
node.last_contact = last_contact;
|
||||
}
|
||||
if let Some(pos) = self.ordered_ids.iter().position(|i| id == i) {
|
||||
self.ordered_ids.remove(pos);
|
||||
let index = self.get_index_to_insert(last_contact);
|
||||
self.ordered_ids.insert(index, *id);
|
||||
}
|
||||
}
|
||||
|
||||
/// Set last contact as failure for a node
|
||||
pub fn note_failure(&mut self, id: &NodeId) {
|
||||
if let Some(node) = self.nodes.get_mut(id) {
|
||||
node.last_contact = Some(NodeContact::failure());
|
||||
}
|
||||
self.update_ordered_ids(id, Some(NodeContact::failure()));
|
||||
}
|
||||
|
||||
/// Set last contact as success for a node
|
||||
pub fn note_success(&mut self, id: &NodeId) {
|
||||
if let Some(node) = self.nodes.get_mut(id) {
|
||||
node.last_contact = Some(NodeContact::success());
|
||||
}
|
||||
self.update_ordered_ids(id, Some(NodeContact::success()));
|
||||
}
|
||||
|
||||
/// Mark as useless, no further attempts to connect until next call to `clear_useless`.
|
||||
@ -392,7 +467,7 @@ impl NodeTable {
|
||||
let node_ids = self.nodes(&IpFilter::default());
|
||||
let nodes = node_ids.into_iter()
|
||||
.map(|id| self.nodes.get(&id).expect("self.nodes() only returns node IDs from self.nodes"))
|
||||
.take(MAX_NODES)
|
||||
.take(MAX_NODES_IN_FILE)
|
||||
.map(Into::into)
|
||||
.collect();
|
||||
let table = json::NodeTable { nodes };
|
||||
@ -523,6 +598,8 @@ mod tests {
|
||||
use super::*;
|
||||
use std::net::{SocketAddr, SocketAddrV4, Ipv4Addr};
|
||||
use ethereum_types::H512;
|
||||
use std::thread::sleep;
|
||||
use std::time::Duration;
|
||||
use std::str::FromStr;
|
||||
use tempdir::TempDir;
|
||||
use ipnetwork::IpNetwork;
|
||||
@ -604,49 +681,85 @@ mod tests {
|
||||
let id6 = H512::from_str("f979fb575495b8d6db44f750317d0f4622bf4c2aa3365d6af7c284339968eef29b69ad0dce72a4d8db5ebb4968de0e3bec910127f134779fbcb0cb6d3331163c").unwrap();
|
||||
let mut table = NodeTable::new(None);
|
||||
|
||||
assert_eq!(table.get_index_to_insert(Some(NodeContact::success())), 0);
|
||||
assert_eq!(table.get_index_to_insert(Some(NodeContact::failure())), 0);
|
||||
assert_eq!(table.get_index_to_insert(None), 0);
|
||||
|
||||
// sleep 1 mcs is added because nanosecond precision was lost since mac os x high sierra update
|
||||
// https://github.com/paritytech/parity-ethereum/issues/9632
|
||||
|
||||
table.add_node(node1);
|
||||
sleep(Duration::from_micros(1));
|
||||
|
||||
assert_eq!(table.get_index_to_insert(Some(NodeContact::success())), 0);
|
||||
assert_eq!(table.get_index_to_insert(Some(NodeContact::failure())), 1);
|
||||
assert_eq!(table.get_index_to_insert(None), 0);
|
||||
|
||||
table.add_node(node2);
|
||||
sleep(Duration::from_micros(1));
|
||||
|
||||
assert_eq!(table.get_index_to_insert(Some(NodeContact::success())), 0);
|
||||
assert_eq!(table.get_index_to_insert(Some(NodeContact::failure())), 2);
|
||||
assert_eq!(table.get_index_to_insert(None), 0);
|
||||
|
||||
table.add_node(node3);
|
||||
sleep(Duration::from_micros(1));
|
||||
table.add_node(node4);
|
||||
sleep(Duration::from_micros(1));
|
||||
table.add_node(node5);
|
||||
sleep(Duration::from_micros(1));
|
||||
table.add_node(node6);
|
||||
sleep(Duration::from_micros(1));
|
||||
|
||||
// failures - nodes 1 & 2
|
||||
table.note_failure(&id1);
|
||||
sleep(Duration::from_micros(1));
|
||||
let time_in_between = SystemTime::now();
|
||||
sleep(Duration::from_micros(1));
|
||||
table.note_failure(&id2);
|
||||
sleep(Duration::from_micros(1));
|
||||
|
||||
// success - nodes 3 & 4
|
||||
assert_eq!(table.get_index_to_insert(Some(NodeContact::success())), 0);
|
||||
assert_eq!(table.get_index_to_insert(Some(NodeContact::failure())), 6);
|
||||
assert_eq!(table.get_index_to_insert(Some(NodeContact::Failure(time_in_between))), 5);
|
||||
assert_eq!(table.get_index_to_insert(Some(NodeContact::Failure(time::UNIX_EPOCH))), 4);
|
||||
assert_eq!(table.get_index_to_insert(None), 0);
|
||||
|
||||
// success - nodes 3,4,5 (5 - the oldest)
|
||||
table.note_success(&id5);
|
||||
sleep(Duration::from_micros(1));
|
||||
table.note_success(&id3);
|
||||
table.note_success(&id4);
|
||||
sleep(Duration::from_micros(1));
|
||||
|
||||
// success - node 5 (old contact)
|
||||
table.get_mut(&id5).unwrap().last_contact = Some(NodeContact::Success(time::UNIX_EPOCH));
|
||||
assert_eq!(table.get_index_to_insert(Some(NodeContact::Success(time::UNIX_EPOCH))), 2);
|
||||
assert_eq!(table.get_index_to_insert(None), 2);
|
||||
|
||||
let time_in_between = SystemTime::now();
|
||||
sleep(Duration::from_micros(1));
|
||||
table.note_success(&id4);
|
||||
sleep(Duration::from_micros(1));
|
||||
|
||||
assert_eq!(table.get_index_to_insert(Some(NodeContact::success())), 0);
|
||||
assert_eq!(table.get_index_to_insert(Some(NodeContact::Success(time_in_between))), 1);
|
||||
assert_eq!(table.get_index_to_insert(Some(NodeContact::Success(time::UNIX_EPOCH))), 3);
|
||||
assert_eq!(table.get_index_to_insert(None), 3);
|
||||
|
||||
// unknown - node 6
|
||||
|
||||
// nodes are also ordered according to their addition time
|
||||
//
|
||||
// nanosecond precision lost since mac os x high sierra update so let's not compare their order
|
||||
// https://github.com/paritytech/parity-ethereum/issues/9632
|
||||
let r = table.nodes(&IpFilter::default());
|
||||
|
||||
// most recent success
|
||||
assert!(
|
||||
(r[0] == id4 && r[1] == id3) ||
|
||||
(r[0] == id3 && r[1] == id4)
|
||||
);
|
||||
assert_eq!(r[0][..], id4[..]); // most recent success
|
||||
assert_eq!(r[1][..], id3[..]);
|
||||
|
||||
// unknown (old contacts and new nodes), randomly shuffled
|
||||
assert!(
|
||||
(r[2] == id5 && r[3] == id6) ||
|
||||
(r[2] == id6 && r[3] == id5)
|
||||
r[2][..] == id5[..] && r[3][..] == id6[..] ||
|
||||
r[2][..] == id6[..] && r[3][..] == id5[..]
|
||||
);
|
||||
|
||||
// oldest failure
|
||||
assert!(
|
||||
(r[4] == id1 && r[5] == id2) ||
|
||||
(r[4] == id2 && r[5] == id1)
|
||||
);
|
||||
assert_eq!(r[4][..], id1[..]); // oldest failure
|
||||
assert_eq!(r[5][..], id2[..]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
Loading…
Reference in New Issue
Block a user