light client : failsafe crate (circuit breaker) (#9790)
* refactor(light) : N/W calls with `circuit breaker` * fix(nits) : forgot to commit new files * Add tests and change CLI args * Address grumbles * fix(failsafe-rs) : Santize input to prevent panics * chore(failsafe) : bump failsafe, (parking_lot) Bump failsafe to v0.3.0 to enable `parking_lot::Mutex` instead `spin::Mutex` * Remove `success_rate` * feat(circuit_breaker logger) * feat(CLI): separate CLI args request and response * Fix tests * Error response provide request kind When a reponse `times-out` provide the actual request or requests that failed * Update ethcore/light/src/on_demand/mod.rs Co-Authored-By: niklasad1 <niklasadolfsson1@gmail.com> * Update ethcore/light/src/on_demand/mod.rs Co-Authored-By: niklasad1 <niklasadolfsson1@gmail.com> * fix(grumbles): formatting nit * fix(grumbles) * Use second resolution on CLI args * Use `consecutive failure policy` instead of `timeOverWindow` * Add a couple of tests for `request_guard` * fix(request_guard): off-by-one error, update tests
This commit is contained in:
		
							parent
							
								
									ec886ddefb
								
							
						
					
					
						commit
						7fb33796b1
					
				
							
								
								
									
										12
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										12
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							@ -738,6 +738,7 @@ dependencies = [
 | 
			
		||||
 "ethcore-network 1.12.0",
 | 
			
		||||
 "ethcore-transaction 0.1.0",
 | 
			
		||||
 "ethereum-types 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
 | 
			
		||||
 "failsafe 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
 | 
			
		||||
 "fastmap 0.1.0",
 | 
			
		||||
 "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
 | 
			
		||||
 "hashdb 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
 | 
			
		||||
@ -1169,6 +1170,16 @@ dependencies = [
 | 
			
		||||
 "vm 0.1.0",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "failsafe"
 | 
			
		||||
version = "0.3.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
 | 
			
		||||
 "parking_lot 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)",
 | 
			
		||||
 "rand 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "failure"
 | 
			
		||||
version = "0.1.3"
 | 
			
		||||
@ -4268,6 +4279,7 @@ dependencies = [
 | 
			
		||||
"checksum ethbloom 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1a93a43ce2e9f09071449da36bfa7a1b20b950ee344b6904ff23de493b03b386"
 | 
			
		||||
"checksum ethereum-types 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "35b3c5a18bc5e73a32a110ac743ec04b02bbbcd3b71d3118d40a6113d509378a"
 | 
			
		||||
"checksum ethereum-types-serialize 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4ac59a21a9ce98e188f3dace9eb67a6c4a3c67ec7fbc7218cb827852679dc002"
 | 
			
		||||
"checksum failsafe 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ad3bf1642583ea2f1fa38a1e8546613a7488816941b33e5f0fccceac61879118"
 | 
			
		||||
"checksum failure 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6dd377bcc1b1b7ce911967e3ec24fa19c3224394ec05b54aa7b083d498341ac7"
 | 
			
		||||
"checksum failure_derive 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "64c2d913fe8ed3b6c6518eedf4538255b989945c14c2a7d5cbff62a5e2120596"
 | 
			
		||||
"checksum fake-simd 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed"
 | 
			
		||||
 | 
			
		||||
@ -21,6 +21,7 @@ hashdb = "0.3.0"
 | 
			
		||||
heapsize = "0.4"
 | 
			
		||||
vm = { path = "../vm" }
 | 
			
		||||
fastmap = { path = "../../util/fastmap" }
 | 
			
		||||
failsafe = { version = "0.3.0", default-features = false, features = ["parking_lot_mutex"] }
 | 
			
		||||
rlp = { version = "0.3.0", features = ["ethereum"] }
 | 
			
		||||
rlp_derive = { path = "../../util/rlp_derive" }
 | 
			
		||||
smallvec = "0.6"
 | 
			
		||||
 | 
			
		||||
@ -62,6 +62,7 @@ extern crate ethereum_types;
 | 
			
		||||
extern crate ethcore;
 | 
			
		||||
extern crate hashdb;
 | 
			
		||||
extern crate heapsize;
 | 
			
		||||
extern crate failsafe;
 | 
			
		||||
extern crate futures;
 | 
			
		||||
extern crate itertools;
 | 
			
		||||
extern crate keccak_hasher;
 | 
			
		||||
 | 
			
		||||
@ -19,44 +19,54 @@
 | 
			
		||||
//! will take the raw data received here and extract meaningful results from it.
 | 
			
		||||
 | 
			
		||||
use std::cmp;
 | 
			
		||||
use std::collections::{HashMap, BTreeSet};
 | 
			
		||||
use std::collections::HashMap;
 | 
			
		||||
use std::marker::PhantomData;
 | 
			
		||||
use std::sync::Arc;
 | 
			
		||||
use std::time::Duration;
 | 
			
		||||
 | 
			
		||||
use ethcore::executed::{Executed, ExecutionError};
 | 
			
		||||
 | 
			
		||||
use futures::{Poll, Future, Async};
 | 
			
		||||
use futures::sync::oneshot::{self, Receiver};
 | 
			
		||||
use network::PeerId;
 | 
			
		||||
use parking_lot::{RwLock, Mutex};
 | 
			
		||||
use rand;
 | 
			
		||||
use std::time::{Duration, SystemTime};
 | 
			
		||||
use rand::Rng;
 | 
			
		||||
 | 
			
		||||
use net::{
 | 
			
		||||
	self, Handler, PeerStatus, Status, Capabilities,
 | 
			
		||||
	Handler, PeerStatus, Status, Capabilities,
 | 
			
		||||
	Announcement, EventContext, BasicContext, ReqId,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
use cache::Cache;
 | 
			
		||||
use request::{self as basic_request, Request as NetworkRequest};
 | 
			
		||||
use self::request::CheckedRequest;
 | 
			
		||||
 | 
			
		||||
pub use self::request::{Request, Response, HeaderRef};
 | 
			
		||||
pub use self::request::{Request, Response, HeaderRef, Error as ValidityError};
 | 
			
		||||
pub use self::request_guard::{RequestGuard, Error as RequestError};
 | 
			
		||||
pub use self::response_guard::{ResponseGuard, Error as ResponseGuardError, Inner as ResponseGuardInner};
 | 
			
		||||
 | 
			
		||||
pub use types::request::ResponseError;
 | 
			
		||||
 | 
			
		||||
#[cfg(test)]
 | 
			
		||||
mod tests;
 | 
			
		||||
 | 
			
		||||
pub mod request;
 | 
			
		||||
mod request_guard;
 | 
			
		||||
mod response_guard;
 | 
			
		||||
 | 
			
		||||
/// The result of execution
 | 
			
		||||
pub type ExecutionResult = Result<Executed, ExecutionError>;
 | 
			
		||||
 | 
			
		||||
/// The default number of retries for OnDemand queries to send to the other nodes
 | 
			
		||||
pub const DEFAULT_RETRY_COUNT: usize = 10;
 | 
			
		||||
 | 
			
		||||
/// The default time limit in milliseconds for inactive (no new peer to connect to) OnDemand queries (0 for unlimited)
 | 
			
		||||
pub const DEFAULT_QUERY_TIME_LIMIT: Duration = Duration::from_millis(10000);
 | 
			
		||||
 | 
			
		||||
const NULL_DURATION: Duration = Duration::from_secs(0);
 | 
			
		||||
/// The initial backoff interval for OnDemand queries
 | 
			
		||||
pub const DEFAULT_REQUEST_MIN_BACKOFF_DURATION: Duration = Duration::from_secs(10);
 | 
			
		||||
/// The maximum request interval for OnDemand queries
 | 
			
		||||
pub const DEFAULT_REQUEST_MAX_BACKOFF_DURATION: Duration = Duration::from_secs(100);
 | 
			
		||||
/// The default window length a response is evaluated
 | 
			
		||||
pub const DEFAULT_RESPONSE_TIME_TO_LIVE: Duration = Duration::from_secs(60);
 | 
			
		||||
/// The default number of maximum backoff iterations
 | 
			
		||||
pub const DEFAULT_MAX_REQUEST_BACKOFF_ROUNDS: usize = 10;
 | 
			
		||||
/// The default number failed request to be regarded as failure
 | 
			
		||||
pub const DEFAULT_NUM_CONSECUTIVE_FAILED_REQUESTS: usize = 1;
 | 
			
		||||
 | 
			
		||||
/// OnDemand related errors
 | 
			
		||||
pub mod error {
 | 
			
		||||
@ -69,22 +79,19 @@ pub mod error {
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		errors {
 | 
			
		||||
			#[doc = "Max number of on-demand query attempts reached without result."]
 | 
			
		||||
			MaxAttemptReach(query_index: usize) {
 | 
			
		||||
				description("On-demand query limit reached")
 | 
			
		||||
				display("On-demand query limit reached on query #{}", query_index)
 | 
			
		||||
			#[doc = "Timeout bad response"]
 | 
			
		||||
			BadResponse(err: String) {
 | 
			
		||||
				description("Max response evaluation time exceeded")
 | 
			
		||||
				display("{}", err)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			#[doc = "No reply with current peer set, time out occured while waiting for new peers for additional query attempt."]
 | 
			
		||||
			TimeoutOnNewPeers(query_index: usize, remaining_attempts: usize) {
 | 
			
		||||
				description("Timeout for On-demand query")
 | 
			
		||||
				display("Timeout for On-demand query; {} query attempts remain for query #{}", remaining_attempts, query_index)
 | 
			
		||||
			#[doc = "OnDemand requests limit exceeded"]
 | 
			
		||||
			RequestLimit {
 | 
			
		||||
				description("OnDemand request maximum backoff iterations exceeded")
 | 
			
		||||
				display("OnDemand request maximum backoff iterations exceeded")
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// relevant peer info.
 | 
			
		||||
@ -113,7 +120,6 @@ impl Peer {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/// Either an array of responses or a single error.
 | 
			
		||||
type PendingResponse = self::error::Result<Vec<Response>>;
 | 
			
		||||
 | 
			
		||||
@ -124,10 +130,8 @@ struct Pending {
 | 
			
		||||
	required_capabilities: Capabilities,
 | 
			
		||||
	responses: Vec<Response>,
 | 
			
		||||
	sender: oneshot::Sender<PendingResponse>,
 | 
			
		||||
	base_query_index: usize,
 | 
			
		||||
	remaining_query_count: usize,
 | 
			
		||||
	query_id_history: BTreeSet<PeerId>,
 | 
			
		||||
	inactive_time_limit: Option<SystemTime>,
 | 
			
		||||
	request_guard: RequestGuard,
 | 
			
		||||
	response_guard: ResponseGuard,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Pending {
 | 
			
		||||
@ -190,7 +194,7 @@ impl Pending {
 | 
			
		||||
	fn try_complete(self) -> Option<Self> {
 | 
			
		||||
		if self.requests.is_complete() {
 | 
			
		||||
			if self.sender.send(Ok(self.responses)).is_err() {
 | 
			
		||||
				debug!(target: "on_demand", "Dropped oneshot channel receiver on complete request at query #{}", self.query_id_history.len());
 | 
			
		||||
				debug!(target: "on_demand", "Dropped oneshot channel receiver on request");
 | 
			
		||||
			}
 | 
			
		||||
			None
 | 
			
		||||
		} else {
 | 
			
		||||
@ -227,20 +231,38 @@ impl Pending {
 | 
			
		||||
		self.required_capabilities = capabilities;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// returning no reponse, it will result in an error.
 | 
			
		||||
	// self is consumed on purpose.
 | 
			
		||||
	fn no_response(self) {
 | 
			
		||||
		trace!(target: "on_demand", "Dropping a pending query (no reply) at query #{}", self.query_id_history.len());
 | 
			
		||||
		let err = self::error::ErrorKind::MaxAttemptReach(self.requests.num_answered());
 | 
			
		||||
	// received too many empty responses, may be away to indicate a faulty request
 | 
			
		||||
	fn bad_response(self, response_err: ResponseGuardError) {
 | 
			
		||||
		let reqs: Vec<&str> = self.requests.requests().iter().map(|req| {
 | 
			
		||||
			match req {
 | 
			
		||||
				CheckedRequest::HeaderProof(_, _) => "HeaderProof",
 | 
			
		||||
				CheckedRequest::HeaderByHash(_, _) => "HeaderByHash",
 | 
			
		||||
				CheckedRequest::HeaderWithAncestors(_, _) => "HeaderWithAncestors",
 | 
			
		||||
				CheckedRequest::TransactionIndex(_, _) => "TransactionIndex",
 | 
			
		||||
				CheckedRequest::Receipts(_, _) => "Receipts",
 | 
			
		||||
				CheckedRequest::Body(_, _) => "Body",
 | 
			
		||||
				CheckedRequest::Account(_, _) => "Account",
 | 
			
		||||
				CheckedRequest::Code(_, _) => "Code",
 | 
			
		||||
				CheckedRequest::Execution(_, _) => "Execution",
 | 
			
		||||
				CheckedRequest::Signal(_, _) => "Signal",
 | 
			
		||||
			}
 | 
			
		||||
		}).collect();
 | 
			
		||||
 | 
			
		||||
		let err = format!("Bad response on {}: [ {} ]. {}",
 | 
			
		||||
			if reqs.len() > 1 { "requests" } else { "request" },
 | 
			
		||||
			reqs.join(", "),
 | 
			
		||||
			response_err
 | 
			
		||||
		);
 | 
			
		||||
 | 
			
		||||
		let err = self::error::ErrorKind::BadResponse(err);
 | 
			
		||||
		if self.sender.send(Err(err.into())).is_err() {
 | 
			
		||||
			debug!(target: "on_demand", "Dropped oneshot channel receiver on no response");
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// returning a peer discovery timeout during query attempts
 | 
			
		||||
	fn time_out(self) {
 | 
			
		||||
		trace!(target: "on_demand", "Dropping a pending query (no new peer time out) at query #{}", self.query_id_history.len());
 | 
			
		||||
		let err = self::error::ErrorKind::TimeoutOnNewPeers(self.requests.num_answered(), self.query_id_history.len());
 | 
			
		||||
	fn request_limit_reached(self) {
 | 
			
		||||
		let err = self::error::ErrorKind::RequestLimit;
 | 
			
		||||
		if self.sender.send(Err(err.into())).is_err() {
 | 
			
		||||
			debug!(target: "on_demand", "Dropped oneshot channel receiver on time out");
 | 
			
		||||
		}
 | 
			
		||||
@ -326,30 +348,68 @@ pub struct OnDemand {
 | 
			
		||||
	in_transit: RwLock<HashMap<ReqId, Pending>>,
 | 
			
		||||
	cache: Arc<Mutex<Cache>>,
 | 
			
		||||
	no_immediate_dispatch: bool,
 | 
			
		||||
	base_retry_count: usize,
 | 
			
		||||
	query_inactive_time_limit: Option<Duration>,
 | 
			
		||||
	response_time_window: Duration,
 | 
			
		||||
	request_backoff_start: Duration,
 | 
			
		||||
	request_backoff_max: Duration,
 | 
			
		||||
	request_backoff_rounds_max: usize,
 | 
			
		||||
	request_number_of_consecutive_errors: usize
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl OnDemand {
 | 
			
		||||
 | 
			
		||||
	/// Create a new `OnDemand` service with the given cache.
 | 
			
		||||
	pub fn new(cache: Arc<Mutex<Cache>>) -> Self {
 | 
			
		||||
		OnDemand {
 | 
			
		||||
	pub fn new(
 | 
			
		||||
		cache: Arc<Mutex<Cache>>,
 | 
			
		||||
		response_time_window: Duration,
 | 
			
		||||
		request_backoff_start: Duration,
 | 
			
		||||
		request_backoff_max: Duration,
 | 
			
		||||
		request_backoff_rounds_max: usize,
 | 
			
		||||
		request_number_of_consecutive_errors: usize,
 | 
			
		||||
	) -> Self {
 | 
			
		||||
 | 
			
		||||
		Self {
 | 
			
		||||
			pending: RwLock::new(Vec::new()),
 | 
			
		||||
			peers: RwLock::new(HashMap::new()),
 | 
			
		||||
			in_transit: RwLock::new(HashMap::new()),
 | 
			
		||||
			cache,
 | 
			
		||||
			no_immediate_dispatch: false,
 | 
			
		||||
			base_retry_count: DEFAULT_RETRY_COUNT,
 | 
			
		||||
			query_inactive_time_limit: Some(DEFAULT_QUERY_TIME_LIMIT),
 | 
			
		||||
			response_time_window: Self::sanitize_circuit_breaker_input(response_time_window, "Response time window"),
 | 
			
		||||
			request_backoff_start: Self::sanitize_circuit_breaker_input(request_backoff_start, "Request initial backoff time window"),
 | 
			
		||||
			request_backoff_max: Self::sanitize_circuit_breaker_input(request_backoff_max, "Request maximum backoff time window"),
 | 
			
		||||
			request_backoff_rounds_max,
 | 
			
		||||
			request_number_of_consecutive_errors,
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	fn sanitize_circuit_breaker_input(dur: Duration, name: &'static str) -> Duration {
 | 
			
		||||
		if dur.as_secs() < 1 {
 | 
			
		||||
			warn!(target: "on_demand",
 | 
			
		||||
				"{} is too short must be at least 1 second, configuring it to 1 second", name);
 | 
			
		||||
			Duration::from_secs(1)
 | 
			
		||||
		} else {
 | 
			
		||||
			dur
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// make a test version: this doesn't dispatch pending requests
 | 
			
		||||
	// until you trigger it manually.
 | 
			
		||||
	#[cfg(test)]
 | 
			
		||||
	fn new_test(cache: Arc<Mutex<Cache>>) -> Self {
 | 
			
		||||
		let mut me = OnDemand::new(cache);
 | 
			
		||||
	fn new_test(
 | 
			
		||||
		cache: Arc<Mutex<Cache>>,
 | 
			
		||||
		request_ttl: Duration,
 | 
			
		||||
		request_backoff_start: Duration,
 | 
			
		||||
		request_backoff_max: Duration,
 | 
			
		||||
		request_backoff_rounds_max: usize,
 | 
			
		||||
		request_number_of_consecutive_errors: usize,
 | 
			
		||||
	) -> Self {
 | 
			
		||||
		let mut me = OnDemand::new(
 | 
			
		||||
			cache,
 | 
			
		||||
			request_ttl,
 | 
			
		||||
			request_backoff_start,
 | 
			
		||||
			request_backoff_max,
 | 
			
		||||
			request_backoff_rounds_max,
 | 
			
		||||
			request_number_of_consecutive_errors,
 | 
			
		||||
		);
 | 
			
		||||
		me.no_immediate_dispatch = true;
 | 
			
		||||
 | 
			
		||||
		me
 | 
			
		||||
@ -403,10 +463,13 @@ impl OnDemand {
 | 
			
		||||
			required_capabilities: capabilities,
 | 
			
		||||
			responses,
 | 
			
		||||
			sender,
 | 
			
		||||
			base_query_index: 0,
 | 
			
		||||
			remaining_query_count: 0,
 | 
			
		||||
			query_id_history: BTreeSet::new(),
 | 
			
		||||
			inactive_time_limit: None,
 | 
			
		||||
			request_guard: RequestGuard::new(
 | 
			
		||||
				self.request_number_of_consecutive_errors as u32,
 | 
			
		||||
				self.request_backoff_rounds_max,
 | 
			
		||||
				self.request_backoff_start,
 | 
			
		||||
				self.request_backoff_max,
 | 
			
		||||
			),
 | 
			
		||||
			response_guard: ResponseGuard::new(self.response_time_window),
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		Ok(receiver)
 | 
			
		||||
@ -435,82 +498,56 @@ impl OnDemand {
 | 
			
		||||
	// dispatch pending requests, and discard those for which the corresponding
 | 
			
		||||
	// receiver has been dropped.
 | 
			
		||||
	fn dispatch_pending(&self, ctx: &BasicContext) {
 | 
			
		||||
		if self.pending.read().is_empty() { return }
 | 
			
		||||
		let mut pending = self.pending.write();
 | 
			
		||||
		if self.pending.read().is_empty() {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		debug!(target: "on_demand", "Attempting to dispatch {} pending requests", pending.len());
 | 
			
		||||
		let mut pending = self.pending.write();
 | 
			
		||||
 | 
			
		||||
		// iterate over all pending requests, and check them for hang-up.
 | 
			
		||||
		// then, try and find a peer who can serve it.
 | 
			
		||||
		let peers = self.peers.read();
 | 
			
		||||
		*pending = ::std::mem::replace(&mut *pending, Vec::new()).into_iter()
 | 
			
		||||
 | 
			
		||||
		*pending = ::std::mem::replace(&mut *pending, Vec::new())
 | 
			
		||||
			.into_iter()
 | 
			
		||||
			.filter(|pending| !pending.sender.is_canceled())
 | 
			
		||||
			.filter_map(|mut pending| {
 | 
			
		||||
				// the peer we dispatch to is chosen randomly
 | 
			
		||||
 | 
			
		||||
				let num_peers = peers.len();
 | 
			
		||||
				let history_len = pending.query_id_history.len();
 | 
			
		||||
				let offset = if history_len == 0 {
 | 
			
		||||
					pending.remaining_query_count = self.base_retry_count;
 | 
			
		||||
					let rand = rand::random::<usize>();
 | 
			
		||||
					pending.base_query_index = rand;
 | 
			
		||||
					rand
 | 
			
		||||
				} else {
 | 
			
		||||
					pending.base_query_index + history_len
 | 
			
		||||
				} % cmp::max(num_peers, 1);
 | 
			
		||||
				let init_remaining_query_count = pending.remaining_query_count; // to fail in case of big reduction of nb of peers
 | 
			
		||||
				for (peer_id, peer) in peers.iter().chain(peers.iter())
 | 
			
		||||
					.skip(offset).take(num_peers) {
 | 
			
		||||
					// TODO: see which requests can be answered by the cache?
 | 
			
		||||
					if pending.remaining_query_count == 0 {
 | 
			
		||||
						break
 | 
			
		||||
				// The first peer to dispatch the request is chosen at random
 | 
			
		||||
				let rand = rand::thread_rng().gen_range(0, cmp::max(1, num_peers));
 | 
			
		||||
 | 
			
		||||
				for (peer_id, peer) in peers
 | 
			
		||||
					.iter()
 | 
			
		||||
					.cycle()
 | 
			
		||||
					.skip(rand)
 | 
			
		||||
					.take(num_peers)
 | 
			
		||||
				{
 | 
			
		||||
 | 
			
		||||
					if !peer.can_fulfill(&pending.required_capabilities) {
 | 
			
		||||
						trace!(target: "on_demand", "Peer {} without required capabilities, skipping", peer_id);
 | 
			
		||||
						continue
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					if pending.query_id_history.insert(peer_id.clone()) {
 | 
			
		||||
 | 
			
		||||
						if !peer.can_fulfill(&pending.required_capabilities) {
 | 
			
		||||
							trace!(target: "on_demand", "Peer {} without required capabilities, skipping, {} remaining attempts", peer_id, pending.remaining_query_count);
 | 
			
		||||
							continue
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						pending.remaining_query_count -= 1;
 | 
			
		||||
						pending.inactive_time_limit = None;
 | 
			
		||||
 | 
			
		||||
						match ctx.request_from(*peer_id, pending.net_requests.clone()) {
 | 
			
		||||
							Ok(req_id) => {
 | 
			
		||||
								trace!(target: "on_demand", "Dispatched request {} to peer {}, {} remaining attempts", req_id, peer_id, pending.remaining_query_count);
 | 
			
		||||
								self.in_transit.write().insert(req_id, pending);
 | 
			
		||||
								return None
 | 
			
		||||
							}
 | 
			
		||||
							Err(net::Error::NoCredits) | Err(net::Error::NotServer) => {}
 | 
			
		||||
							Err(e) => debug!(target: "on_demand", "Error dispatching request to peer: {}", e),
 | 
			
		||||
					if pending.request_guard.is_call_permitted() {
 | 
			
		||||
						if let Ok(req_id) = ctx.request_from(*peer_id, pending.net_requests.clone()) {
 | 
			
		||||
							self.in_transit.write().insert(req_id, pending);
 | 
			
		||||
							return None;
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				if pending.remaining_query_count == 0	{
 | 
			
		||||
					pending.no_response();
 | 
			
		||||
				// Register that the request round failed
 | 
			
		||||
				if let RequestError::ReachedLimit = pending.request_guard.register_error() {
 | 
			
		||||
					pending.request_limit_reached();
 | 
			
		||||
					None
 | 
			
		||||
				} else if init_remaining_query_count == pending.remaining_query_count {
 | 
			
		||||
					if let Some(query_inactive_time_limit) = self.query_inactive_time_limit {
 | 
			
		||||
						let now = SystemTime::now();
 | 
			
		||||
						if let Some(inactive_time_limit) = pending.inactive_time_limit {
 | 
			
		||||
							if now > inactive_time_limit {
 | 
			
		||||
								pending.time_out();
 | 
			
		||||
								return None
 | 
			
		||||
							}
 | 
			
		||||
						} else {
 | 
			
		||||
							debug!(target: "on_demand", "No more peers to query, waiting for {} seconds until dropping query", query_inactive_time_limit.as_secs());
 | 
			
		||||
							pending.inactive_time_limit = Some(now + query_inactive_time_limit);
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
					Some(pending)
 | 
			
		||||
				} else {
 | 
			
		||||
					Some(pending)
 | 
			
		||||
				}
 | 
			
		||||
			})
 | 
			
		||||
			.collect(); // `pending` now contains all requests we couldn't dispatch.
 | 
			
		||||
		})
 | 
			
		||||
		.collect(); // `pending` now contains all requests we couldn't dispatch
 | 
			
		||||
 | 
			
		||||
		debug!(target: "on_demand", "Was unable to dispatch {} requests.", pending.len());
 | 
			
		||||
		trace!(target: "on_demand", "Was unable to dispatch {} requests.", pending.len());
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// submit a pending request set. attempts to answer from cache before
 | 
			
		||||
@ -521,26 +558,14 @@ impl OnDemand {
 | 
			
		||||
 | 
			
		||||
		pending.answer_from_cache(&*self.cache);
 | 
			
		||||
		if let Some(mut pending) = pending.try_complete() {
 | 
			
		||||
			// update cached requests
 | 
			
		||||
			pending.update_net_requests();
 | 
			
		||||
			// push into `pending` buffer
 | 
			
		||||
			self.pending.write().push(pending);
 | 
			
		||||
			// try to dispatch
 | 
			
		||||
			self.attempt_dispatch(ctx);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/// Set the retry count for a query.
 | 
			
		||||
	pub fn default_retry_number(&mut self, nb_retry: usize) {
 | 
			
		||||
		self.base_retry_count = nb_retry;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/// Set the time limit for a query.
 | 
			
		||||
	pub fn query_inactive_time_limit(&mut self, inactive_time_limit: Duration) {
 | 
			
		||||
		self.query_inactive_time_limit = if inactive_time_limit == NULL_DURATION {
 | 
			
		||||
			None
 | 
			
		||||
		} else {
 | 
			
		||||
			Some(inactive_time_limit)
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Handler for OnDemand {
 | 
			
		||||
@ -594,13 +619,11 @@ impl Handler for OnDemand {
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		if responses.is_empty() {
 | 
			
		||||
			if pending.remaining_query_count == 0 {
 | 
			
		||||
				pending.no_response();
 | 
			
		||||
			// Max number of `bad` responses reached, drop the request
 | 
			
		||||
			if let Err(e) = pending.response_guard.register_error(&ResponseError::Validity(ValidityError::Empty)) {
 | 
			
		||||
				pending.bad_response(e);
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			// do not keep query counter for others elements of this batch
 | 
			
		||||
			pending.query_id_history.clear();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// for each incoming response
 | 
			
		||||
@ -613,7 +636,11 @@ impl Handler for OnDemand {
 | 
			
		||||
				debug!(target: "on_demand", "Peer {} gave bad response: {:?}", peer, e);
 | 
			
		||||
				ctx.disable_peer(peer);
 | 
			
		||||
 | 
			
		||||
				break;
 | 
			
		||||
				// Max number of `bad` responses reached, drop the request
 | 
			
		||||
				if let Err(err) = pending.response_guard.register_error(&e) {
 | 
			
		||||
					pending.bad_response(err);
 | 
			
		||||
					return;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										123
									
								
								ethcore/light/src/on_demand/request_guard.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								ethcore/light/src/on_demand/request_guard.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,123 @@
 | 
			
		||||
// Copyright 2015-2018 Parity Technologies (UK) Ltd.
 | 
			
		||||
// This file is part of Parity.
 | 
			
		||||
 | 
			
		||||
// Parity is free software: you can redistribute it and/or modify
 | 
			
		||||
// it under the terms of the GNU General Public License as published by
 | 
			
		||||
// the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
// (at your option) any later version.
 | 
			
		||||
 | 
			
		||||
// Parity is distributed in the hope that it will be useful,
 | 
			
		||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
// GNU General Public License for more details.
 | 
			
		||||
 | 
			
		||||
// You should have received a copy of the GNU General Public License
 | 
			
		||||
// along with Parity.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
use failsafe;
 | 
			
		||||
use std::time::Duration;
 | 
			
		||||
 | 
			
		||||
type RequestPolicy = failsafe::failure_policy::ConsecutiveFailures<failsafe::backoff::Exponential>;
 | 
			
		||||
 | 
			
		||||
/// Error wrapped on-top of `FailsafeError`
 | 
			
		||||
#[derive(Debug, PartialEq)]
 | 
			
		||||
pub enum Error {
 | 
			
		||||
	/// The call is let through
 | 
			
		||||
	LetThrough,
 | 
			
		||||
	/// The call rejected by the guard
 | 
			
		||||
	Rejected,
 | 
			
		||||
	/// The request reached the maximum of backoff iterations
 | 
			
		||||
	ReachedLimit,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Handle and register requests that can fail
 | 
			
		||||
#[derive(Debug)]
 | 
			
		||||
pub struct RequestGuard {
 | 
			
		||||
	backoff_round: usize,
 | 
			
		||||
	max_backoff_rounds: usize,
 | 
			
		||||
	state: failsafe::StateMachine<RequestPolicy, ()>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl RequestGuard {
 | 
			
		||||
	/// Constructor
 | 
			
		||||
	pub fn new(
 | 
			
		||||
		consecutive_failures: u32,
 | 
			
		||||
		max_backoff_rounds: usize,
 | 
			
		||||
		start_backoff: Duration,
 | 
			
		||||
		max_backoff: Duration,
 | 
			
		||||
	) -> Self {
 | 
			
		||||
		let backoff = failsafe::backoff::exponential(start_backoff, max_backoff);
 | 
			
		||||
		// success_rate not used because only errors are registered
 | 
			
		||||
		let policy = failsafe::failure_policy::consecutive_failures(consecutive_failures as u32, backoff);
 | 
			
		||||
 | 
			
		||||
		Self {
 | 
			
		||||
			backoff_round: 0,
 | 
			
		||||
			max_backoff_rounds,
 | 
			
		||||
			state: failsafe::StateMachine::new(policy, ()),
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/// Update the state after a `faulty` call
 | 
			
		||||
	pub fn register_error(&mut self) -> Error {
 | 
			
		||||
		trace!(target: "circuit_breaker", "RequestGuard; backoff_round: {}/{}, state {:?}",
 | 
			
		||||
			   self.backoff_round, self.max_backoff_rounds, self.state);
 | 
			
		||||
 | 
			
		||||
		if self.backoff_round >= self.max_backoff_rounds {
 | 
			
		||||
			Error::ReachedLimit
 | 
			
		||||
		} else if self.state.is_call_permitted() {
 | 
			
		||||
			self.state.on_error();
 | 
			
		||||
			if self.state.is_call_permitted() {
 | 
			
		||||
				Error::LetThrough
 | 
			
		||||
			} else {
 | 
			
		||||
				self.backoff_round += 1;
 | 
			
		||||
				Error::Rejected
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			Error::Rejected
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/// Poll the circuit breaker, to check if the call is permitted
 | 
			
		||||
	pub fn is_call_permitted(&self) -> bool {
 | 
			
		||||
		self.state.is_call_permitted()
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[cfg(test)]
 | 
			
		||||
mod tests {
 | 
			
		||||
	use std::iter;
 | 
			
		||||
	use std::time::Instant;
 | 
			
		||||
	use super::*;
 | 
			
		||||
 | 
			
		||||
	#[test]
 | 
			
		||||
	fn one_consecutive_failure_with_10_backoffs() {
 | 
			
		||||
		// 1, 2, 4, 5, 5 .... 5
 | 
			
		||||
		let binary_exp_backoff = vec![1_u64, 2, 4].into_iter().chain(iter::repeat(5_u64).take(7));
 | 
			
		||||
		let mut guard = RequestGuard::new(1, 10, Duration::from_secs(1), Duration::from_secs(5));
 | 
			
		||||
		for backoff in binary_exp_backoff {
 | 
			
		||||
			assert_eq!(guard.register_error(), Error::Rejected);
 | 
			
		||||
			let now = Instant::now();
 | 
			
		||||
			while now.elapsed() <= Duration::from_secs(backoff) {}
 | 
			
		||||
		}
 | 
			
		||||
		assert_eq!(guard.register_error(), Error::ReachedLimit, "10 backoffs should be error");
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	#[test]
 | 
			
		||||
	fn five_consecutive_failures_with_3_backoffs() {
 | 
			
		||||
		let mut guard = RequestGuard::new(5, 3, Duration::from_secs(1), Duration::from_secs(30));
 | 
			
		||||
 | 
			
		||||
		// register five errors
 | 
			
		||||
		for _ in 0..4 {
 | 
			
		||||
			assert_eq!(guard.register_error(), Error::LetThrough);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		let binary_exp_backoff = [1, 2, 4];
 | 
			
		||||
		for backoff in &binary_exp_backoff {
 | 
			
		||||
			assert_eq!(guard.register_error(), Error::Rejected);
 | 
			
		||||
			let now = Instant::now();
 | 
			
		||||
			while now.elapsed() <= Duration::from_secs(*backoff) {}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		assert_eq!(guard.register_error(), Error::ReachedLimit, "3 backoffs should be an error");
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										174
									
								
								ethcore/light/src/on_demand/response_guard.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										174
									
								
								ethcore/light/src/on_demand/response_guard.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,174 @@
 | 
			
		||||
// Copyright 2015-2018 Parity Technologies (UK) Ltd.
 | 
			
		||||
// This file is part of Parity.
 | 
			
		||||
 | 
			
		||||
// Parity is free software: you can redistribute it and/or modify
 | 
			
		||||
// it under the terms of the GNU General Public License as published by
 | 
			
		||||
// the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
// (at your option) any later version.
 | 
			
		||||
 | 
			
		||||
// Parity is distributed in the hope that it will be useful,
 | 
			
		||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
// GNU General Public License for more details.
 | 
			
		||||
 | 
			
		||||
// You should have received a copy of the GNU General Public License
 | 
			
		||||
// along with Parity.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
//! ResponseGuard implementation.
 | 
			
		||||
//! It is responsible for the receiving end of `Pending Request` (see `OnDemand` module docs for more information)
 | 
			
		||||
//! The major functionality is the following:
 | 
			
		||||
//!    1) Register non-successful responses which will reported back if it fails
 | 
			
		||||
//!    2) A timeout mechanism that will wait for successful response at most t seconds
 | 
			
		||||
 | 
			
		||||
use std::time::{Duration, Instant};
 | 
			
		||||
use std::collections::HashMap;
 | 
			
		||||
use std::fmt;
 | 
			
		||||
 | 
			
		||||
use super::{ResponseError, ValidityError};
 | 
			
		||||
 | 
			
		||||
/// Response guard error type
 | 
			
		||||
#[derive(Debug, Eq, PartialEq)]
 | 
			
		||||
pub enum Error {
 | 
			
		||||
	/// No majority, the error reason can't be determined
 | 
			
		||||
	NoMajority(usize),
 | 
			
		||||
	/// Majority, with the error reason
 | 
			
		||||
	Majority(Inner, usize, usize),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl fmt::Display for Error {
 | 
			
		||||
	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
 | 
			
		||||
		match self {
 | 
			
		||||
			Error::Majority(err, majority, total) => {
 | 
			
		||||
				write!(f, "Error cause was {:?}, (majority count: {} / total: {})",
 | 
			
		||||
					err, majority, total)
 | 
			
		||||
			}
 | 
			
		||||
			Error::NoMajority(total) => {
 | 
			
		||||
				write!(f, "Error cause couldn't be determined, the total number of responses was {}", total)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/// Dummy type to convert a generic type with no trait bounds
 | 
			
		||||
#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)]
 | 
			
		||||
pub enum Inner {
 | 
			
		||||
	/// Bad execution proof
 | 
			
		||||
	BadProof,
 | 
			
		||||
	/// RLP decoding
 | 
			
		||||
	Decoder,
 | 
			
		||||
	/// Empty response
 | 
			
		||||
	EmptyResponse,
 | 
			
		||||
	/// Wrong header sequence
 | 
			
		||||
	HeaderByNumber,
 | 
			
		||||
	/// Too few results
 | 
			
		||||
	TooFewResults,
 | 
			
		||||
	/// Too many results
 | 
			
		||||
	TooManyResults,
 | 
			
		||||
	/// Trie error
 | 
			
		||||
	Trie,
 | 
			
		||||
	/// Unresolved header
 | 
			
		||||
	UnresolvedHeader,
 | 
			
		||||
	/// No responses expected.
 | 
			
		||||
	Unexpected,
 | 
			
		||||
	/// Wrong hash
 | 
			
		||||
	WrongHash,
 | 
			
		||||
	/// Wrong Header sequence
 | 
			
		||||
	WrongHeaderSequence,
 | 
			
		||||
	/// Wrong response kind
 | 
			
		||||
	WrongKind,
 | 
			
		||||
	/// Wrong number
 | 
			
		||||
	WrongNumber,
 | 
			
		||||
	/// Wrong Trie Root
 | 
			
		||||
	WrongTrieRoot,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Handle and register responses that can fail
 | 
			
		||||
#[derive(Debug)]
 | 
			
		||||
pub struct ResponseGuard {
 | 
			
		||||
	request_start: Instant,
 | 
			
		||||
	time_to_live: Duration,
 | 
			
		||||
	responses: HashMap<Inner, usize>,
 | 
			
		||||
	number_responses: usize,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl ResponseGuard {
 | 
			
		||||
	/// Constructor
 | 
			
		||||
	pub fn new(time_to_live: Duration) -> Self {
 | 
			
		||||
		Self {
 | 
			
		||||
			request_start: Instant::now(),
 | 
			
		||||
			time_to_live,
 | 
			
		||||
			responses: HashMap::new(),
 | 
			
		||||
			number_responses: 0,
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	fn into_reason(&self, err: &ResponseError<super::request::Error>) -> Inner {
 | 
			
		||||
		match err {
 | 
			
		||||
			ResponseError::Unexpected => Inner::Unexpected,
 | 
			
		||||
			ResponseError::Validity(ValidityError::BadProof) => Inner::BadProof,
 | 
			
		||||
			ResponseError::Validity(ValidityError::Decoder(_)) => Inner::Decoder,
 | 
			
		||||
			ResponseError::Validity(ValidityError::Empty) => Inner::EmptyResponse,
 | 
			
		||||
			ResponseError::Validity(ValidityError::HeaderByNumber) => Inner::HeaderByNumber,
 | 
			
		||||
			ResponseError::Validity(ValidityError::TooFewResults(_, _)) => Inner::TooFewResults,
 | 
			
		||||
			ResponseError::Validity(ValidityError::TooManyResults(_, _)) => Inner::TooManyResults,
 | 
			
		||||
			ResponseError::Validity(ValidityError::Trie(_)) => Inner::Trie,
 | 
			
		||||
			ResponseError::Validity(ValidityError::UnresolvedHeader(_)) => Inner::UnresolvedHeader,
 | 
			
		||||
			ResponseError::Validity(ValidityError::WrongHash(_, _)) => Inner::WrongHash,
 | 
			
		||||
			ResponseError::Validity(ValidityError::WrongHeaderSequence) => Inner::WrongHeaderSequence,
 | 
			
		||||
			ResponseError::Validity(ValidityError::WrongKind) => Inner::WrongKind,
 | 
			
		||||
			ResponseError::Validity(ValidityError::WrongNumber(_, _)) => Inner::WrongNumber,
 | 
			
		||||
			ResponseError::Validity(ValidityError::WrongTrieRoot(_, _)) => Inner::WrongTrieRoot,
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/// Update the state after a `faulty` call
 | 
			
		||||
	pub fn register_error(&mut self, err: &ResponseError<super::request::Error>) -> Result<(), Error> {
 | 
			
		||||
		let err = self.into_reason(err);
 | 
			
		||||
		*self.responses.entry(err).or_insert(0) += 1;
 | 
			
		||||
		self.number_responses = self.number_responses.saturating_add(1);
 | 
			
		||||
		trace!(target: "circuit_breaker", "ResponseGuard: {:?}", self.responses);
 | 
			
		||||
		// The request has exceeded its timeout
 | 
			
		||||
		if self.request_start.elapsed() >= self.time_to_live {
 | 
			
		||||
			let (&err, &max_count) = self.responses.iter().max_by_key(|(_k, v)| *v).expect("got at least one element; qed");
 | 
			
		||||
			let majority = self.responses.values().filter(|v| **v == max_count).count() == 1;
 | 
			
		||||
			if majority {
 | 
			
		||||
				Err(Error::Majority(err, max_count, self.number_responses))
 | 
			
		||||
			} else {
 | 
			
		||||
				Err(Error::NoMajority(self.number_responses))
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			Ok(())
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[cfg(test)]
 | 
			
		||||
mod tests {
 | 
			
		||||
	use std::thread;
 | 
			
		||||
	use super::*;
 | 
			
		||||
 | 
			
		||||
	#[test]
 | 
			
		||||
	fn test_basic_by_majority() {
 | 
			
		||||
		let mut guard = ResponseGuard::new(Duration::from_secs(5));
 | 
			
		||||
		guard.register_error(&ResponseError::Validity(ValidityError::Empty)).unwrap();
 | 
			
		||||
		guard.register_error(&ResponseError::Unexpected).unwrap();
 | 
			
		||||
		guard.register_error(&ResponseError::Unexpected).unwrap();
 | 
			
		||||
		guard.register_error(&ResponseError::Unexpected).unwrap();
 | 
			
		||||
		thread::sleep(Duration::from_secs(5));
 | 
			
		||||
 | 
			
		||||
		assert_eq!(guard.register_error(&ResponseError::Validity(ValidityError::WrongKind)), Err(Error::Majority(Inner::Unexpected, 3, 5)));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	#[test]
 | 
			
		||||
	fn test_no_majority() {
 | 
			
		||||
		let mut guard = ResponseGuard::new(Duration::from_secs(5));
 | 
			
		||||
		guard.register_error(&ResponseError::Validity(ValidityError::Empty)).unwrap();
 | 
			
		||||
		guard.register_error(&ResponseError::Validity(ValidityError::Empty)).unwrap();
 | 
			
		||||
		guard.register_error(&ResponseError::Unexpected).unwrap();
 | 
			
		||||
		guard.register_error(&ResponseError::Unexpected).unwrap();
 | 
			
		||||
		thread::sleep(Duration::from_secs(5));
 | 
			
		||||
 | 
			
		||||
		assert_eq!(guard.register_error(&ResponseError::Validity(ValidityError::WrongKind)), Err(Error::NoMajority(5)));
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -23,10 +23,11 @@ use network::{PeerId, NodeId};
 | 
			
		||||
use net::*;
 | 
			
		||||
use ethereum_types::H256;
 | 
			
		||||
use parking_lot::Mutex;
 | 
			
		||||
use std::time::Duration;
 | 
			
		||||
use ::request::{self as basic_request, Response};
 | 
			
		||||
 | 
			
		||||
use std::sync::Arc;
 | 
			
		||||
use std::time::{Duration, Instant};
 | 
			
		||||
use std::thread;
 | 
			
		||||
 | 
			
		||||
use super::{request, OnDemand, Peer, HeaderRef};
 | 
			
		||||
 | 
			
		||||
@ -36,6 +37,7 @@ enum Context {
 | 
			
		||||
	WithPeer(PeerId),
 | 
			
		||||
	RequestFrom(PeerId, ReqId),
 | 
			
		||||
	Punish(PeerId),
 | 
			
		||||
	FaultyRequest,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl EventContext for Context {
 | 
			
		||||
@ -44,6 +46,7 @@ impl EventContext for Context {
 | 
			
		||||
			Context::WithPeer(id)
 | 
			
		||||
			| Context::RequestFrom(id, _)
 | 
			
		||||
			| Context::Punish(id) => id,
 | 
			
		||||
			| Context::FaultyRequest => 0,
 | 
			
		||||
			_ => panic!("didn't expect to have peer queried."),
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
@ -60,6 +63,7 @@ impl BasicContext for Context {
 | 
			
		||||
	fn request_from(&self, peer_id: PeerId, _: ::request::NetworkRequests) -> Result<ReqId, Error> {
 | 
			
		||||
		match *self {
 | 
			
		||||
			Context::RequestFrom(id, req_id) => if peer_id == id { Ok(req_id) } else { Err(Error::NoCredits) },
 | 
			
		||||
			Context::FaultyRequest => Err(Error::NoCredits),
 | 
			
		||||
			_ => panic!("didn't expect to have requests dispatched."),
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
@ -89,7 +93,17 @@ impl Harness {
 | 
			
		||||
	fn create() -> Self {
 | 
			
		||||
		let cache = Arc::new(Mutex::new(Cache::new(Default::default(), Duration::from_secs(60))));
 | 
			
		||||
		Harness {
 | 
			
		||||
			service: OnDemand::new_test(cache),
 | 
			
		||||
			service: OnDemand::new_test(
 | 
			
		||||
				cache,
 | 
			
		||||
				// Response `time_to_live`
 | 
			
		||||
				Duration::from_secs(5),
 | 
			
		||||
				// Request start backoff
 | 
			
		||||
				Duration::from_secs(1),
 | 
			
		||||
				// Request max backoff
 | 
			
		||||
				Duration::from_secs(20),
 | 
			
		||||
				super::DEFAULT_MAX_REQUEST_BACKOFF_ROUNDS,
 | 
			
		||||
				super::DEFAULT_NUM_CONSECUTIVE_FAILED_REQUESTS
 | 
			
		||||
			)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@ -495,3 +509,90 @@ fn fill_from_cache() {
 | 
			
		||||
 | 
			
		||||
	assert!(recv.wait().is_ok());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[test]
 | 
			
		||||
fn request_without_response_should_backoff_and_then_be_dropped() {
 | 
			
		||||
	let harness = Harness::create();
 | 
			
		||||
	let peer_id = 0;
 | 
			
		||||
	let req_id = ReqId(13);
 | 
			
		||||
 | 
			
		||||
	harness.inject_peer(
 | 
			
		||||
		peer_id,
 | 
			
		||||
		Peer {
 | 
			
		||||
			status: dummy_status(),
 | 
			
		||||
			capabilities: dummy_capabilities(),
 | 
			
		||||
		}
 | 
			
		||||
	);
 | 
			
		||||
 | 
			
		||||
	let binary_exp_backoff: Vec<u64> = vec![1, 2, 4, 8, 16, 20, 20, 20, 20, 20];
 | 
			
		||||
 | 
			
		||||
	let _recv = harness.service.request_raw(
 | 
			
		||||
		&Context::RequestFrom(peer_id, req_id),
 | 
			
		||||
		vec![request::HeaderByHash(Header::default().encoded().hash().into()).into()],
 | 
			
		||||
	).unwrap();
 | 
			
		||||
	assert_eq!(harness.service.pending.read().len(), 1);
 | 
			
		||||
 | 
			
		||||
	for backoff in &binary_exp_backoff {
 | 
			
		||||
		harness.service.dispatch_pending(&Context::FaultyRequest);
 | 
			
		||||
		assert_eq!(harness.service.pending.read().len(), 1, "Request should not be dropped");
 | 
			
		||||
		let now = Instant::now();
 | 
			
		||||
		while now.elapsed() < Duration::from_secs(*backoff) {}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	harness.service.dispatch_pending(&Context::FaultyRequest);
 | 
			
		||||
	assert_eq!(harness.service.pending.read().len(), 0, "Request exceeded the 10 backoff rounds should be dropped");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[test]
 | 
			
		||||
fn empty_responses_exceeds_limit_should_be_dropped() {
 | 
			
		||||
	let harness = Harness::create();
 | 
			
		||||
	let peer_id = 0;
 | 
			
		||||
	let req_id = ReqId(13);
 | 
			
		||||
 | 
			
		||||
	harness.inject_peer(
 | 
			
		||||
		peer_id,
 | 
			
		||||
		Peer {
 | 
			
		||||
			status: dummy_status(),
 | 
			
		||||
			capabilities: dummy_capabilities(),
 | 
			
		||||
		}
 | 
			
		||||
	);
 | 
			
		||||
 | 
			
		||||
	let _recv = harness.service.request_raw(
 | 
			
		||||
		&Context::RequestFrom(peer_id, req_id),
 | 
			
		||||
		vec![request::HeaderByHash(Header::default().encoded().hash().into()).into()],
 | 
			
		||||
	).unwrap();
 | 
			
		||||
 | 
			
		||||
	harness.service.dispatch_pending(&Context::RequestFrom(peer_id, req_id));
 | 
			
		||||
 | 
			
		||||
	assert_eq!(harness.service.pending.read().len(), 0);
 | 
			
		||||
	assert_eq!(harness.service.in_transit.read().len(), 1);
 | 
			
		||||
 | 
			
		||||
	let now = Instant::now();
 | 
			
		||||
 | 
			
		||||
	// Send `empty responses` in the current time window
 | 
			
		||||
	// Use only half of the `time_window` because we can't be sure exactly
 | 
			
		||||
	// when the window started and the clock accurancy
 | 
			
		||||
	while now.elapsed() < harness.service.response_time_window / 2 {
 | 
			
		||||
		harness.service.on_responses(
 | 
			
		||||
			&Context::RequestFrom(13, req_id),
 | 
			
		||||
			req_id,
 | 
			
		||||
			&[]
 | 
			
		||||
		);
 | 
			
		||||
		assert!(harness.service.pending.read().len() != 0);
 | 
			
		||||
		let pending = harness.service.pending.write().remove(0);
 | 
			
		||||
		harness.service.in_transit.write().insert(req_id, pending);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Make sure we passed the first `time window`
 | 
			
		||||
	thread::sleep(Duration::from_secs(5));
 | 
			
		||||
 | 
			
		||||
	// Now, response is in failure state but need another response to be `polled`
 | 
			
		||||
	harness.service.on_responses(
 | 
			
		||||
			&Context::RequestFrom(13, req_id),
 | 
			
		||||
			req_id,
 | 
			
		||||
			&[]
 | 
			
		||||
	);
 | 
			
		||||
 | 
			
		||||
	assert!(harness.service.in_transit.read().is_empty());
 | 
			
		||||
	assert!(harness.service.pending.read().is_empty());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -579,13 +579,25 @@ usage! {
 | 
			
		||||
			"Specify CORS header for IPFS API responses. Special options: \"all\", \"none\".",
 | 
			
		||||
 | 
			
		||||
		["Light Client Options"]
 | 
			
		||||
			ARG arg_on_demand_retry_count: (Option<usize>) = None, or |c: &Config| c.light.as_ref()?.on_demand_retry_count,
 | 
			
		||||
			"--on-demand-retry-count=[RETRIES]",
 | 
			
		||||
			"Specify the query retry count.",
 | 
			
		||||
			ARG arg_on_demand_response_time_window: (Option<u64>) = None, or |c: &Config| c.light.as_ref()?.on_demand_response_time_window,
 | 
			
		||||
			"--on-demand-time-window=[S]",
 | 
			
		||||
			"Specify the maximum time to wait for a successful response",
 | 
			
		||||
 | 
			
		||||
			ARG arg_on_demand_inactive_time_limit: (Option<u64>) = None, or |c: &Config| c.light.as_ref()?.on_demand_inactive_time_limit,
 | 
			
		||||
			"--on-demand-inactive-time-limit=[MS]",
 | 
			
		||||
			"Specify light client query inactive time limit. O for no limit.",
 | 
			
		||||
			ARG arg_on_demand_request_backoff_start: (Option<u64>) = None, or |c: &Config| c.light.as_ref()?.on_demand_request_backoff_start,
 | 
			
		||||
			"--on-demand-start-backoff=[S]",
 | 
			
		||||
			"Specify light client initial backoff time for a request",
 | 
			
		||||
 | 
			
		||||
			ARG arg_on_demand_request_backoff_max: (Option<u64>) = None, or |c: &Config| c.light.as_ref()?.on_demand_request_backoff_max,
 | 
			
		||||
			"--on-demand-end-backoff=[S]",
 | 
			
		||||
			"Specify light client maximum backoff time for a request",
 | 
			
		||||
 | 
			
		||||
			ARG arg_on_demand_request_backoff_rounds_max: (Option<usize>) = None, or |c: &Config| c.light.as_ref()?.on_demand_request_backoff_rounds_max,
 | 
			
		||||
			"--on-demand-max-backoff-rounds=[TIMES]",
 | 
			
		||||
			"Specify light client maximum number of backoff iterations for a request",
 | 
			
		||||
 | 
			
		||||
			ARG arg_on_demand_request_consecutive_failures: (Option<usize>) = None, or |c: &Config| c.light.as_ref()?.on_demand_request_consecutive_failures,
 | 
			
		||||
			"--on-demand-consecutive-failures=[TIMES]",
 | 
			
		||||
			"Specify light client the number of failures for a request until it gets exponentially backed off",
 | 
			
		||||
 | 
			
		||||
		["Secret Store Options"]
 | 
			
		||||
			FLAG flag_no_secretstore: (bool) = false, or |c: &Config| c.secretstore.as_ref()?.disable.clone(),
 | 
			
		||||
@ -1402,8 +1414,11 @@ struct Whisper {
 | 
			
		||||
#[derive(Default, Debug, PartialEq, Deserialize)]
 | 
			
		||||
#[serde(deny_unknown_fields)]
 | 
			
		||||
struct Light {
 | 
			
		||||
	on_demand_retry_count: Option<usize>,
 | 
			
		||||
	on_demand_inactive_time_limit: Option<u64>,
 | 
			
		||||
	on_demand_response_time_window: Option<u64>,
 | 
			
		||||
	on_demand_request_backoff_start: Option<u64>,
 | 
			
		||||
	on_demand_request_backoff_max: Option<u64>,
 | 
			
		||||
	on_demand_request_backoff_rounds_max: Option<usize>,
 | 
			
		||||
	on_demand_request_consecutive_failures: Option<usize>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[cfg(test)]
 | 
			
		||||
@ -1820,8 +1835,11 @@ mod tests {
 | 
			
		||||
			arg_snapshot_threads: None,
 | 
			
		||||
 | 
			
		||||
			// -- Light options.
 | 
			
		||||
			arg_on_demand_retry_count: Some(15),
 | 
			
		||||
			arg_on_demand_inactive_time_limit: Some(15000),
 | 
			
		||||
			arg_on_demand_response_time_window: Some(2000),
 | 
			
		||||
			arg_on_demand_request_backoff_start: Some(9000),
 | 
			
		||||
			arg_on_demand_request_backoff_max: Some(15000),
 | 
			
		||||
			arg_on_demand_request_backoff_rounds_max: Some(100),
 | 
			
		||||
			arg_on_demand_request_consecutive_failures: Some(1),
 | 
			
		||||
 | 
			
		||||
			// -- Whisper options.
 | 
			
		||||
			flag_whisper: false,
 | 
			
		||||
@ -2075,8 +2093,11 @@ mod tests {
 | 
			
		||||
				num_verifiers: None,
 | 
			
		||||
			}),
 | 
			
		||||
			light: Some(Light {
 | 
			
		||||
				on_demand_retry_count: Some(12),
 | 
			
		||||
				on_demand_inactive_time_limit: Some(20000),
 | 
			
		||||
				on_demand_response_time_window: Some(2000),
 | 
			
		||||
				on_demand_request_backoff_start: Some(9000),
 | 
			
		||||
				on_demand_request_backoff_max: Some(15000),
 | 
			
		||||
				on_demand_request_backoff_rounds_max: Some(10),
 | 
			
		||||
				on_demand_request_consecutive_failures: Some(1),
 | 
			
		||||
			}),
 | 
			
		||||
			snapshots: Some(Snapshots {
 | 
			
		||||
				disable_periodic: Some(true),
 | 
			
		||||
 | 
			
		||||
@ -157,8 +157,11 @@ scale_verifiers = true
 | 
			
		||||
num_verifiers = 6
 | 
			
		||||
 | 
			
		||||
[light]
 | 
			
		||||
on_demand_retry_count = 15
 | 
			
		||||
on_demand_inactive_time_limit = 15000
 | 
			
		||||
on_demand_response_time_window = 2000
 | 
			
		||||
on_demand_request_backoff_start = 9000
 | 
			
		||||
on_demand_request_backoff_max = 15000
 | 
			
		||||
on_demand_request_backoff_rounds_max = 100
 | 
			
		||||
on_demand_request_consecutive_failures = 1
 | 
			
		||||
 | 
			
		||||
[snapshots]
 | 
			
		||||
disable_periodic = false
 | 
			
		||||
 | 
			
		||||
@ -71,8 +71,11 @@ fat_db = "off"
 | 
			
		||||
scale_verifiers = false
 | 
			
		||||
 | 
			
		||||
[light]
 | 
			
		||||
on_demand_retry_count = 12
 | 
			
		||||
on_demand_inactive_time_limit = 20000
 | 
			
		||||
on_demand_response_time_window = 2000
 | 
			
		||||
on_demand_request_backoff_start = 9000
 | 
			
		||||
on_demand_request_backoff_max = 15000
 | 
			
		||||
on_demand_request_backoff_rounds_max = 10
 | 
			
		||||
on_demand_request_consecutive_failures = 1
 | 
			
		||||
 | 
			
		||||
[snapshots]
 | 
			
		||||
disable_periodic = true
 | 
			
		||||
 | 
			
		||||
@ -397,8 +397,11 @@ impl Configuration {
 | 
			
		||||
				whisper: whisper_config,
 | 
			
		||||
				no_hardcoded_sync: self.args.flag_no_hardcoded_sync,
 | 
			
		||||
				max_round_blocks_to_import: self.args.arg_max_round_blocks_to_import,
 | 
			
		||||
				on_demand_retry_count: self.args.arg_on_demand_retry_count,
 | 
			
		||||
				on_demand_inactive_time_limit: self.args.arg_on_demand_inactive_time_limit,
 | 
			
		||||
				on_demand_response_time_window: self.args.arg_on_demand_response_time_window,
 | 
			
		||||
				on_demand_request_backoff_start: self.args.arg_on_demand_request_backoff_start,
 | 
			
		||||
				on_demand_request_backoff_max: self.args.arg_on_demand_request_backoff_max,
 | 
			
		||||
				on_demand_request_backoff_rounds_max: self.args.arg_on_demand_request_backoff_rounds_max,
 | 
			
		||||
				on_demand_request_consecutive_failures: self.args.arg_on_demand_request_consecutive_failures,
 | 
			
		||||
			};
 | 
			
		||||
			Cmd::Run(run_cmd)
 | 
			
		||||
		};
 | 
			
		||||
@ -1449,8 +1452,11 @@ mod tests {
 | 
			
		||||
			no_persistent_txqueue: false,
 | 
			
		||||
			whisper: Default::default(),
 | 
			
		||||
			max_round_blocks_to_import: 12,
 | 
			
		||||
			on_demand_retry_count: None,
 | 
			
		||||
			on_demand_inactive_time_limit: None,
 | 
			
		||||
			on_demand_response_time_window: None,
 | 
			
		||||
			on_demand_request_backoff_start: None,
 | 
			
		||||
			on_demand_request_backoff_max: None,
 | 
			
		||||
			on_demand_request_backoff_rounds_max: None,
 | 
			
		||||
			on_demand_request_consecutive_failures: None,
 | 
			
		||||
		};
 | 
			
		||||
		expected.secretstore_conf.enabled = cfg!(feature = "secretstore");
 | 
			
		||||
		expected.secretstore_conf.http_enabled = cfg!(feature = "secretstore");
 | 
			
		||||
 | 
			
		||||
@ -136,8 +136,11 @@ pub struct RunCmd {
 | 
			
		||||
	pub whisper: ::whisper::Config,
 | 
			
		||||
	pub no_hardcoded_sync: bool,
 | 
			
		||||
	pub max_round_blocks_to_import: usize,
 | 
			
		||||
	pub on_demand_retry_count: Option<usize>,
 | 
			
		||||
	pub on_demand_inactive_time_limit: Option<u64>,
 | 
			
		||||
	pub on_demand_response_time_window: Option<u64>,
 | 
			
		||||
	pub on_demand_request_backoff_start: Option<u64>,
 | 
			
		||||
	pub on_demand_request_backoff_max: Option<u64>,
 | 
			
		||||
	pub on_demand_request_backoff_rounds_max: Option<usize>,
 | 
			
		||||
	pub on_demand_request_consecutive_failures: Option<usize>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// node info fetcher for the local store.
 | 
			
		||||
@ -216,12 +219,31 @@ fn execute_light_impl(cmd: RunCmd, logger: Arc<RotatingLogger>) -> Result<Runnin
 | 
			
		||||
	config.queue.verifier_settings = cmd.verifier_settings;
 | 
			
		||||
 | 
			
		||||
	// start on_demand service.
 | 
			
		||||
 | 
			
		||||
	let response_time_window = cmd.on_demand_response_time_window.map_or(
 | 
			
		||||
		::light::on_demand::DEFAULT_RESPONSE_TIME_TO_LIVE,
 | 
			
		||||
		|s| Duration::from_secs(s)
 | 
			
		||||
	);
 | 
			
		||||
 | 
			
		||||
	let request_backoff_start = cmd.on_demand_request_backoff_start.map_or(
 | 
			
		||||
		::light::on_demand::DEFAULT_REQUEST_MIN_BACKOFF_DURATION,
 | 
			
		||||
		|s| Duration::from_secs(s)
 | 
			
		||||
	);
 | 
			
		||||
 | 
			
		||||
	let request_backoff_max = cmd.on_demand_request_backoff_max.map_or(
 | 
			
		||||
		::light::on_demand::DEFAULT_REQUEST_MAX_BACKOFF_DURATION,
 | 
			
		||||
		|s| Duration::from_secs(s)
 | 
			
		||||
	);
 | 
			
		||||
 | 
			
		||||
	let on_demand = Arc::new({
 | 
			
		||||
		let mut on_demand = ::light::on_demand::OnDemand::new(cache.clone());
 | 
			
		||||
		on_demand.default_retry_number(cmd.on_demand_retry_count.unwrap_or(::light::on_demand::DEFAULT_RETRY_COUNT));
 | 
			
		||||
		on_demand.query_inactive_time_limit(cmd.on_demand_inactive_time_limit.map(Duration::from_millis)
 | 
			
		||||
																				.unwrap_or(::light::on_demand::DEFAULT_QUERY_TIME_LIMIT));
 | 
			
		||||
		on_demand
 | 
			
		||||
		::light::on_demand::OnDemand::new(
 | 
			
		||||
			cache.clone(),
 | 
			
		||||
			response_time_window,
 | 
			
		||||
			request_backoff_start,
 | 
			
		||||
			request_backoff_max,
 | 
			
		||||
			cmd.on_demand_request_backoff_rounds_max.unwrap_or(::light::on_demand::DEFAULT_MAX_REQUEST_BACKOFF_ROUNDS),
 | 
			
		||||
			cmd.on_demand_request_consecutive_failures.unwrap_or(::light::on_demand::DEFAULT_NUM_CONSECUTIVE_FAILED_REQUESTS)
 | 
			
		||||
		)
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	let sync_handle = Arc::new(RwLock::new(Weak::new()));
 | 
			
		||||
 | 
			
		||||
@ -523,8 +523,8 @@ pub fn filter_block_not_found(id: BlockId) -> Error {
 | 
			
		||||
pub fn on_demand_error(err: OnDemandError) -> Error {
 | 
			
		||||
	match err {
 | 
			
		||||
		OnDemandError(OnDemandErrorKind::ChannelCanceled(e), _) => on_demand_cancel(e),
 | 
			
		||||
		OnDemandError(OnDemandErrorKind::MaxAttemptReach(_), _) => max_attempts_reached(&err),
 | 
			
		||||
		OnDemandError(OnDemandErrorKind::TimeoutOnNewPeers(_,_), _) => timeout_new_peer(&err),
 | 
			
		||||
		OnDemandError(OnDemandErrorKind::RequestLimit, _) => timeout_new_peer(&err),
 | 
			
		||||
		OnDemandError(OnDemandErrorKind::BadResponse(_), _) => max_attempts_reached(&err),
 | 
			
		||||
		_ => on_demand_others(&err),
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user