Simplify error handling after engines fallback removal (#3283)

## Issue Addressed
Part of #3118, continuation of #3257

## Proposed Changes
- the [ `first_success_without_retry` ](9c429d0764/beacon_node/execution_layer/src/engines.rs (L348-L351)) function returns a single error.
- the [`first_success`](9c429d0764/beacon_node/execution_layer/src/engines.rs (L324)) function returns a single error.
- [ `EngineErrors` ](9c429d0764/beacon_node/execution_layer/src/lib.rs (L69)) carries a single error.
- [`EngineError`](9c429d0764/beacon_node/execution_layer/src/engines.rs (L173-L177)) now does not need to carry an Id
- [`process_multiple_payload_statuses`](9c429d0764/beacon_node/execution_layer/src/payload_status.rs (L46-L50)) now doesn't need to receive an iterator of statuses and weight in different errors

## Additional Info
This is built on top of #3294
This commit is contained in:
Divma 2022-07-04 02:56:13 +00:00
parent 61ed5f0ec6
commit 1219da9a45
3 changed files with 154 additions and 248 deletions

View File

@ -57,7 +57,6 @@ struct PayloadIdCacheKey {
/// An execution engine. /// An execution engine.
pub struct Engine<T> { pub struct Engine<T> {
pub id: String,
pub api: HttpJsonRpc<T>, pub api: HttpJsonRpc<T>,
payload_id_cache: Mutex<LruCache<PayloadIdCacheKey, PayloadId>>, payload_id_cache: Mutex<LruCache<PayloadIdCacheKey, PayloadId>>,
state: RwLock<EngineState>, state: RwLock<EngineState>,
@ -65,9 +64,8 @@ pub struct Engine<T> {
impl<T> Engine<T> { impl<T> Engine<T> {
/// Creates a new, offline engine. /// Creates a new, offline engine.
pub fn new(id: String, api: HttpJsonRpc<T>) -> Self { pub fn new(api: HttpJsonRpc<T>) -> Self {
Self { Self {
id,
api, api,
payload_id_cache: Mutex::new(LruCache::new(PAYLOAD_ID_LRU_CACHE_SIZE)), payload_id_cache: Mutex::new(LruCache::new(PAYLOAD_ID_LRU_CACHE_SIZE)),
state: RwLock::new(EngineState::Offline), state: RwLock::new(EngineState::Offline),
@ -135,10 +133,10 @@ pub struct Engines {
#[derive(Debug)] #[derive(Debug)]
pub enum EngineError { pub enum EngineError {
Offline { id: String }, Offline,
Api { id: String, error: EngineApiError }, Api { error: EngineApiError },
BuilderApi { error: EngineApiError }, BuilderApi { error: EngineApiError },
Auth { id: String }, Auth,
} }
impl Engines { impl Engines {
@ -159,7 +157,6 @@ impl Engines {
self.log, self.log,
"No need to call forkchoiceUpdated"; "No need to call forkchoiceUpdated";
"msg" => "head does not have execution enabled", "msg" => "head does not have execution enabled",
"id" => &self.engine.id,
); );
return; return;
} }
@ -168,7 +165,6 @@ impl Engines {
self.log, self.log,
"Issuing forkchoiceUpdated"; "Issuing forkchoiceUpdated";
"forkchoice_state" => ?forkchoice_state, "forkchoice_state" => ?forkchoice_state,
"id" => &self.engine.id,
); );
// For simplicity, payload attributes are never included in this call. It may be // For simplicity, payload attributes are never included in this call. It may be
@ -183,14 +179,12 @@ impl Engines {
self.log, self.log,
"Failed to issue latest head to engine"; "Failed to issue latest head to engine";
"error" => ?e, "error" => ?e,
"id" => &self.engine.id,
); );
} }
} else { } else {
debug!( debug!(
self.log, self.log,
"No head, not sending to engine"; "No head, not sending to engine";
"id" => &self.engine.id,
); );
} }
} }
@ -261,45 +255,36 @@ impl Engines {
} }
} }
/// Run `func` on all engines, in the order in which they are defined, returning the first /// Run `func` on the node.
/// successful result that is found.
/// ///
/// This function might try to run `func` twice. If all nodes return an error on the first time /// This function might try to run `func` twice. If the node returns an error it will try to
/// it runs, it will try to upcheck all offline nodes and then run the function again. /// upcheck it and then run the function again.
pub async fn first_success<'a, F, G, H>(&'a self, func: F) -> Result<H, Vec<EngineError>> pub async fn first_success<'a, F, G, H>(&'a self, func: F) -> Result<H, EngineError>
where where
F: Fn(&'a Engine<EngineApi>) -> G + Copy, F: Fn(&'a Engine<EngineApi>) -> G + Copy,
G: Future<Output = Result<H, EngineApiError>>, G: Future<Output = Result<H, EngineApiError>>,
{ {
match self.first_success_without_retry(func).await { match self.first_success_without_retry(func).await {
Ok(result) => Ok(result), Ok(result) => Ok(result),
Err(mut first_errors) => { Err(e) => {
// Try to recover some nodes. debug!(self.log, "First engine call failed. Retrying"; "err" => ?e);
// Try to recover the node.
self.upcheck_not_synced(Logging::Enabled).await; self.upcheck_not_synced(Logging::Enabled).await;
// Retry the call on all nodes. // Try again.
match self.first_success_without_retry(func).await { self.first_success_without_retry(func).await
Ok(result) => Ok(result),
Err(second_errors) => {
first_errors.extend(second_errors);
Err(first_errors)
}
}
} }
} }
} }
/// Run `func` on all engines, in the order in which they are defined, returning the first /// Run `func` on the node.
/// successful result that is found.
pub async fn first_success_without_retry<'a, F, G, H>( pub async fn first_success_without_retry<'a, F, G, H>(
&'a self, &'a self,
func: F, func: F,
) -> Result<H, Vec<EngineError>> ) -> Result<H, EngineError>
where where
F: Fn(&'a Engine<EngineApi>) -> G, F: Fn(&'a Engine<EngineApi>) -> G,
G: Future<Output = Result<H, EngineApiError>>, G: Future<Output = Result<H, EngineApiError>>,
{ {
let mut errors = vec![];
let (engine_synced, engine_auth_failed) = { let (engine_synced, engine_auth_failed) = {
let state = self.engine.state.read().await; let state = self.engine.state.read().await;
( (
@ -309,32 +294,22 @@ impl Engines {
}; };
if engine_synced { if engine_synced {
match func(&self.engine).await { match func(&self.engine).await {
Ok(result) => return Ok(result), Ok(result) => Ok(result),
Err(error) => { Err(error) => {
debug!( debug!(
self.log, self.log,
"Execution engine call failed"; "Execution engine call failed";
"error" => ?error, "error" => ?error,
"id" => &&self.engine.id
); );
*self.engine.state.write().await = EngineState::Offline; *self.engine.state.write().await = EngineState::Offline;
errors.push(EngineError::Api { Err(EngineError::Api { error })
id: self.engine.id.clone(),
error,
})
} }
} }
} else if engine_auth_failed { } else if engine_auth_failed {
errors.push(EngineError::Auth { Err(EngineError::Auth)
id: self.engine.id.clone(),
})
} else { } else {
errors.push(EngineError::Offline { Err(EngineError::Offline)
id: self.engine.id.clone(),
})
} }
Err(errors)
} }
/// Runs `func` on the node. /// Runs `func` on the node.
@ -363,9 +338,7 @@ impl Engines {
{ {
let func = &func; let func = &func;
if *self.engine.state.read().await == EngineState::Offline { if *self.engine.state.read().await == EngineState::Offline {
Err(EngineError::Offline { Err(EngineError::Offline)
id: self.engine.id.clone(),
})
} else { } else {
match func(&self.engine).await { match func(&self.engine).await {
Ok(res) => Ok(res), Ok(res) => Ok(res),
@ -376,10 +349,7 @@ impl Engines {
"error" => ?error, "error" => ?error,
); );
*self.engine.state.write().await = EngineState::Offline; *self.engine.state.write().await = EngineState::Offline;
Err(EngineError::Api { Err(EngineError::Api { error })
id: self.engine.id.clone(),
error,
})
} }
} }
} }

View File

@ -12,7 +12,7 @@ pub use engine_api::{http, http::deposit_methods, http::HttpJsonRpc};
pub use engines::ForkChoiceState; pub use engines::ForkChoiceState;
use engines::{Engine, EngineError, Engines, Logging}; use engines::{Engine, EngineError, Engines, Logging};
use lru::LruCache; use lru::LruCache;
use payload_status::process_multiple_payload_statuses; use payload_status::process_payload_status;
pub use payload_status::PayloadStatus; pub use payload_status::PayloadStatus;
use sensitive_url::SensitiveUrl; use sensitive_url::SensitiveUrl;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -68,11 +68,10 @@ pub enum Error {
NoPayloadBuilder, NoPayloadBuilder,
ApiError(ApiError), ApiError(ApiError),
Builder(builder_client::Error), Builder(builder_client::Error),
EngineErrors(Vec<EngineError>), EngineError(Box<EngineError>),
NotSynced, NotSynced,
ShuttingDown, ShuttingDown,
FeeRecipientUnspecified, FeeRecipientUnspecified,
ConsensusFailure,
MissingLatestValidHash, MissingLatestValidHash,
InvalidJWTSecret(String), InvalidJWTSecret(String),
} }
@ -200,12 +199,11 @@ impl<T: EthSpec> ExecutionLayer<T> {
}?; }?;
let engine: Engine<EngineApi> = { let engine: Engine<EngineApi> = {
let id = execution_url.to_string();
let auth = Auth::new(jwt_key, jwt_id, jwt_version); let auth = Auth::new(jwt_key, jwt_id, jwt_version);
debug!(log, "Loaded execution endpoint"; "endpoint" => %id, "jwt_path" => ?secret_file.as_path()); debug!(log, "Loaded execution endpoint"; "endpoint" => %execution_url, "jwt_path" => ?secret_file.as_path());
let api = HttpJsonRpc::<EngineApi>::new_with_auth(execution_url, auth) let api = HttpJsonRpc::<EngineApi>::new_with_auth(execution_url, auth)
.map_err(Error::ApiError)?; .map_err(Error::ApiError)?;
Engine::<EngineApi>::new(id, api) Engine::<EngineApi>::new(api)
}; };
let builder = builder_url let builder = builder_url
@ -709,7 +707,8 @@ impl<T: EthSpec> ExecutionLayer<T> {
}) })
}) })
.await .await
.map_err(Error::EngineErrors) .map_err(Box::new)
.map_err(Error::EngineError)
} }
/// Maps to the `engine_newPayload` JSON-RPC call. /// Maps to the `engine_newPayload` JSON-RPC call.
@ -742,16 +741,14 @@ impl<T: EthSpec> ExecutionLayer<T> {
"block_number" => execution_payload.block_number, "block_number" => execution_payload.block_number,
); );
let broadcast_results = self let broadcast_result = self
.engines() .engines()
.broadcast(|engine| engine.api.new_payload_v1(execution_payload.clone())) .broadcast(|engine| engine.api.new_payload_v1(execution_payload.clone()))
.await; .await;
process_multiple_payload_statuses( process_payload_status(execution_payload.block_hash, broadcast_result, self.log())
execution_payload.block_hash, .map_err(Box::new)
Some(broadcast_results).into_iter(), .map_err(Error::EngineError)
self.log(),
)
} }
/// Register that the given `validator_index` is going to produce a block at `slot`. /// Register that the given `validator_index` is going to produce a block at `slot`.
@ -879,7 +876,7 @@ impl<T: EthSpec> ExecutionLayer<T> {
.set_latest_forkchoice_state(forkchoice_state) .set_latest_forkchoice_state(forkchoice_state)
.await; .await;
let broadcast_results = self let broadcast_result = self
.engines() .engines()
.broadcast(|engine| async move { .broadcast(|engine| async move {
engine engine
@ -888,13 +885,13 @@ impl<T: EthSpec> ExecutionLayer<T> {
}) })
.await; .await;
process_multiple_payload_statuses( process_payload_status(
head_block_hash, head_block_hash,
Some(broadcast_results) broadcast_result.map(|response| response.payload_status),
.into_iter()
.map(|result| result.map(|response| response.payload_status)),
self.log(), self.log(),
) )
.map_err(Box::new)
.map_err(Error::EngineError)
} }
pub async fn exchange_transition_configuration(&self, spec: &ChainSpec) -> Result<(), Error> { pub async fn exchange_transition_configuration(&self, spec: &ChainSpec) -> Result<(), Error> {
@ -909,9 +906,6 @@ impl<T: EthSpec> ExecutionLayer<T> {
.broadcast(|engine| engine.api.exchange_transition_configuration_v1(local)) .broadcast(|engine| engine.api.exchange_transition_configuration_v1(local))
.await; .await;
let mut errors = vec![];
// Having no fallbacks, the id of the used node is 0
let i = 0usize;
match broadcast_result { match broadcast_result {
Ok(remote) => { Ok(remote) => {
if local.terminal_total_difficulty != remote.terminal_total_difficulty if local.terminal_total_difficulty != remote.terminal_total_difficulty
@ -922,20 +916,18 @@ impl<T: EthSpec> ExecutionLayer<T> {
"Execution client config mismatch"; "Execution client config mismatch";
"msg" => "ensure lighthouse and the execution client are up-to-date and \ "msg" => "ensure lighthouse and the execution client are up-to-date and \
configured consistently", configured consistently",
"execution_endpoint" => i,
"remote" => ?remote, "remote" => ?remote,
"local" => ?local, "local" => ?local,
); );
errors.push(EngineError::Api { Err(Error::EngineError(Box::new(EngineError::Api {
id: i.to_string(),
error: ApiError::TransitionConfigurationMismatch, error: ApiError::TransitionConfigurationMismatch,
}); })))
} else { } else {
debug!( debug!(
self.log(), self.log(),
"Execution client config is OK"; "Execution client config is OK";
"execution_endpoint" => i
); );
Ok(())
} }
} }
Err(e) => { Err(e) => {
@ -943,17 +935,10 @@ impl<T: EthSpec> ExecutionLayer<T> {
self.log(), self.log(),
"Unable to get transition config"; "Unable to get transition config";
"error" => ?e, "error" => ?e,
"execution_endpoint" => i,
); );
errors.push(e); Err(Error::EngineError(Box::new(e)))
} }
} }
if errors.is_empty() {
Ok(())
} else {
Err(Error::EngineErrors(errors))
}
} }
/// Used during block production to determine if the merge has been triggered. /// Used during block production to determine if the merge has been triggered.
@ -992,7 +977,8 @@ impl<T: EthSpec> ExecutionLayer<T> {
.await .await
}) })
.await .await
.map_err(Error::EngineErrors)?; .map_err(Box::new)
.map_err(Error::EngineError)?;
if let Some(hash) = &hash_opt { if let Some(hash) = &hash_opt {
info!( info!(
@ -1102,7 +1088,8 @@ impl<T: EthSpec> ExecutionLayer<T> {
Ok(None) Ok(None)
}) })
.await .await
.map_err(|e| Error::EngineErrors(vec![e])) .map_err(Box::new)
.map_err(Error::EngineError)
} }
/// This function should remain internal. /// This function should remain internal.
@ -1160,7 +1147,8 @@ impl<T: EthSpec> ExecutionLayer<T> {
.await .await
}) })
.await .await
.map_err(Error::EngineErrors) .map_err(Box::new)
.map_err(Error::EngineError)
} }
async fn get_payload_by_block_hash_from_engine( async fn get_payload_by_block_hash_from_engine(

View File

@ -1,7 +1,6 @@
use crate::engine_api::{Error as ApiError, PayloadStatusV1, PayloadStatusV1Status}; use crate::engine_api::{Error as ApiError, PayloadStatusV1, PayloadStatusV1Status};
use crate::engines::EngineError; use crate::engines::EngineError;
use crate::Error; use slog::{warn, Logger};
use slog::{crit, warn, Logger};
use types::ExecutionBlockHash; use types::ExecutionBlockHash;
/// Provides a simpler, easier to parse version of `PayloadStatusV1` for upstream users. /// Provides a simpler, easier to parse version of `PayloadStatusV1` for upstream users.
@ -24,168 +23,117 @@ pub enum PayloadStatus {
}, },
} }
/// Processes the responses from multiple execution engines, finding the "best" status and returning /// Processes the response from the execution engine.
/// it (if any). pub fn process_payload_status(
///
/// This function has the following basic goals:
///
/// - Detect a consensus failure between nodes.
/// - Find the most-synced node by preferring a definite response (valid/invalid) over a
/// syncing/accepted response or error.
///
/// # Details
///
/// - If there are conflicting valid/invalid responses, always return an error.
/// - If there are syncing/accepted responses but valid/invalid responses exist, return the
/// valid/invalid responses since they're definite.
/// - If there are multiple valid responses, return the first one processed.
/// - If there are multiple invalid responses, return the first one processed.
/// - Syncing/accepted responses are grouped, if there are multiple of them, return the first one
/// processed.
/// - If there are no responses (only errors or nothing), return an error.
pub fn process_multiple_payload_statuses(
head_block_hash: ExecutionBlockHash, head_block_hash: ExecutionBlockHash,
statuses: impl Iterator<Item = Result<PayloadStatusV1, EngineError>>, status: Result<PayloadStatusV1, EngineError>,
log: &Logger, log: &Logger,
) -> Result<PayloadStatus, Error> { ) -> Result<PayloadStatus, EngineError> {
let mut errors = vec![]; match status {
let mut valid_statuses = vec![]; Err(error) => {
let mut invalid_statuses = vec![]; warn!(
let mut other_statuses = vec![];
for status in statuses {
match status {
Err(e) => errors.push(e),
Ok(response) => match &response.status {
PayloadStatusV1Status::Valid => {
if response
.latest_valid_hash
.map_or(false, |h| h == head_block_hash)
{
// The response is only valid if `latest_valid_hash` is not `null` and
// equal to the provided `block_hash`.
valid_statuses.push(PayloadStatus::Valid)
} else {
errors.push(EngineError::Api {
id: "unknown".to_string(),
error: ApiError::BadResponse(
format!(
"new_payload: response.status = VALID but invalid latest_valid_hash. Expected({:?}) Found({:?})",
head_block_hash,
response.latest_valid_hash,
)
),
});
}
}
PayloadStatusV1Status::Invalid => {
if let Some(latest_valid_hash) = response.latest_valid_hash {
// The response is only valid if `latest_valid_hash` is not `null`.
invalid_statuses.push(PayloadStatus::Invalid {
latest_valid_hash,
validation_error: response.validation_error.clone(),
})
} else {
errors.push(EngineError::Api {
id: "unknown".to_string(),
error: ApiError::BadResponse(
"new_payload: response.status = INVALID but null latest_valid_hash"
.to_string(),
),
});
}
}
PayloadStatusV1Status::InvalidBlockHash => {
// In the interests of being liberal with what we accept, only raise a
// warning here.
if response.latest_valid_hash.is_some() {
warn!(
log,
"Malformed response from execution engine";
"msg" => "expected a null latest_valid_hash",
"status" => ?response.status
)
}
invalid_statuses.push(PayloadStatus::InvalidBlockHash {
validation_error: response.validation_error.clone(),
});
}
PayloadStatusV1Status::InvalidTerminalBlock => {
// In the interests of being liberal with what we accept, only raise a
// warning here.
if response.latest_valid_hash.is_some() {
warn!(
log,
"Malformed response from execution engine";
"msg" => "expected a null latest_valid_hash",
"status" => ?response.status
)
}
invalid_statuses.push(PayloadStatus::InvalidTerminalBlock {
validation_error: response.validation_error.clone(),
});
}
PayloadStatusV1Status::Syncing => {
// In the interests of being liberal with what we accept, only raise a
// warning here.
if response.latest_valid_hash.is_some() {
warn!(
log,
"Malformed response from execution engine";
"msg" => "expected a null latest_valid_hash",
"status" => ?response.status
)
}
other_statuses.push(PayloadStatus::Syncing)
}
PayloadStatusV1Status::Accepted => {
// In the interests of being liberal with what we accept, only raise a
// warning here.
if response.latest_valid_hash.is_some() {
warn!(
log,
"Malformed response from execution engine";
"msg" => "expected a null latest_valid_hash",
"status" => ?response.status
)
}
other_statuses.push(PayloadStatus::Accepted)
}
},
}
}
if !valid_statuses.is_empty() && !invalid_statuses.is_empty() {
crit!(
log,
"Consensus failure between execution nodes";
"invalid_statuses" => ?invalid_statuses,
"valid_statuses" => ?valid_statuses,
);
// Choose to exit and ignore the valid response. This preferences correctness over
// liveness.
return Err(Error::ConsensusFailure);
}
// Log any errors to assist with troubleshooting.
for error in &errors {
warn!(
log, log,
"Error whilst processing payload status"; "Error whilst processing payload status";
"error" => ?error, "error" => ?error,
); );
} Err(error)
}
Ok(response) => match &response.status {
PayloadStatusV1Status::Valid => {
if response
.latest_valid_hash
.map_or(false, |h| h == head_block_hash)
{
// The response is only valid if `latest_valid_hash` is not `null` and
// equal to the provided `block_hash`.
Ok(PayloadStatus::Valid)
} else {
let error = format!(
"new_payload: response.status = VALID but invalid latest_valid_hash. Expected({:?}) Found({:?})",
head_block_hash,
response.latest_valid_hash
);
Err(EngineError::Api {
error: ApiError::BadResponse(error),
})
}
}
PayloadStatusV1Status::Invalid => {
if let Some(latest_valid_hash) = response.latest_valid_hash {
// The response is only valid if `latest_valid_hash` is not `null`.
Ok(PayloadStatus::Invalid {
latest_valid_hash,
validation_error: response.validation_error.clone(),
})
} else {
Err(EngineError::Api {
error: ApiError::BadResponse(
"new_payload: response.status = INVALID but null latest_valid_hash"
.to_string(),
),
})
}
}
PayloadStatusV1Status::InvalidBlockHash => {
// In the interests of being liberal with what we accept, only raise a
// warning here.
if response.latest_valid_hash.is_some() {
warn!(
log,
"Malformed response from execution engine";
"msg" => "expected a null latest_valid_hash",
"status" => ?response.status
)
}
valid_statuses Ok(PayloadStatus::InvalidBlockHash {
.first() validation_error: response.validation_error.clone(),
.or_else(|| invalid_statuses.first()) })
.or_else(|| other_statuses.first()) }
.cloned() PayloadStatusV1Status::InvalidTerminalBlock => {
.map(Result::Ok) // In the interests of being liberal with what we accept, only raise a
.unwrap_or_else(|| Err(Error::EngineErrors(errors))) // warning here.
if response.latest_valid_hash.is_some() {
warn!(
log,
"Malformed response from execution engine";
"msg" => "expected a null latest_valid_hash",
"status" => ?response.status
)
}
Ok(PayloadStatus::InvalidTerminalBlock {
validation_error: response.validation_error.clone(),
})
}
PayloadStatusV1Status::Syncing => {
// In the interests of being liberal with what we accept, only raise a
// warning here.
if response.latest_valid_hash.is_some() {
warn!(
log,
"Malformed response from execution engine";
"msg" => "expected a null latest_valid_hash",
"status" => ?response.status
)
}
Ok(PayloadStatus::Syncing)
}
PayloadStatusV1Status::Accepted => {
// In the interests of being liberal with what we accept, only raise a
// warning here.
if response.latest_valid_hash.is_some() {
warn!(
log,
"Malformed response from execution engine";
"msg" => "expected a null latest_valid_hash",
"status" => ?response.status
)
}
Ok(PayloadStatus::Accepted)
}
},
}
} }