use std::{
sync::Arc,
time::{Duration, Instant},
};
use super::{
config::BaseNodeServiceConfig,
error::BaseNodeServiceError,
handle::{BaseNodeEvent, BaseNodeEventSender, BaseNodeServiceRequest, BaseNodeServiceResponse},
};
use chrono::{NaiveDateTime, Utc};
use futures::{pin_mut, Stream, StreamExt};
use log::*;
use rand::rngs::OsRng;
use std::convert::TryInto;
use tari_common_types::{
chain_metadata::ChainMetadata,
waiting_requests::{generate_request_key, RequestKey},
};
use tari_comms::peer_manager::Peer;
use tari_comms_dht::{domain_message::OutboundDomainMessage, outbound::OutboundMessageRequester};
use tari_core::proto::{base_node as proto, base_node::base_node_service_request::Request as BaseNodeRequestProto};
use tari_p2p::{domain_message::DomainMessage, tari_message::TariMessageType};
use tari_service_framework::reply_channel::Receiver;
use tari_shutdown::ShutdownSignal;
use tokio::time;
const LOG_TARGET: &str = "wallet::base_node_service::service";
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct BaseNodeState {
pub chain_metadata: Option<ChainMetadata>,
pub is_synced: Option<bool>,
pub updated: Option<NaiveDateTime>,
pub latency: Option<Duration>,
pub online: OnlineState,
}
impl BaseNodeState {
fn set_online(&mut self, online: OnlineState) -> Self {
self.online = online;
self.clone()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum OnlineState {
Connecting,
Online,
Offline,
}
impl Default for BaseNodeState {
fn default() -> Self {
Self {
chain_metadata: None,
is_synced: None,
updated: None,
latency: None,
online: OnlineState::Connecting,
}
}
}
#[derive(Debug, Clone, Copy)]
struct RequestMetadata {
request_key: RequestKey,
sent: NaiveDateTime,
}
pub struct BaseNodeService<BNResponseStream> {
config: BaseNodeServiceConfig,
base_node_response_stream: Option<BNResponseStream>,
request_stream: Option<Receiver<BaseNodeServiceRequest, Result<BaseNodeServiceResponse, BaseNodeServiceError>>>,
outbound_messaging: OutboundMessageRequester,
event_publisher: BaseNodeEventSender,
base_node_peer: Option<Peer>,
shutdown_signal: Option<ShutdownSignal>,
state: BaseNodeState,
requests: Vec<RequestMetadata>,
}
impl<BNResponseStream> BaseNodeService<BNResponseStream>
where BNResponseStream: Stream<Item = DomainMessage<proto::BaseNodeServiceResponse>>
{
pub fn new(
config: BaseNodeServiceConfig,
base_node_response_stream: BNResponseStream,
request_stream: Receiver<BaseNodeServiceRequest, Result<BaseNodeServiceResponse, BaseNodeServiceError>>,
outbound_messaging: OutboundMessageRequester,
event_publisher: BaseNodeEventSender,
shutdown_signal: ShutdownSignal,
) -> Self
{
Self {
config,
base_node_response_stream: Some(base_node_response_stream),
request_stream: Some(request_stream),
outbound_messaging,
event_publisher,
base_node_peer: None,
shutdown_signal: Some(shutdown_signal),
state: Default::default(),
requests: Default::default(),
}
}
fn set_state(&mut self, state: BaseNodeState) {
self.state = state;
}
pub fn get_state(&self) -> &BaseNodeState {
&self.state
}
pub async fn start(mut self) -> Result<(), BaseNodeServiceError> {
let request_stream = self
.request_stream
.take()
.expect("Wallet Base Node Service initialized without request_stream")
.fuse();
pin_mut!(request_stream);
let base_node_response_stream = self
.base_node_response_stream
.take()
.expect("Wallet Base Node Service initialized without base_node_response_stream")
.fuse();
pin_mut!(base_node_response_stream);
let interval = self.config.refresh_interval;
let mut refresh_tick = time::interval_at((Instant::now() + interval).into(), interval).fuse();
let mut shutdown_signal = self
.shutdown_signal
.take()
.expect("Wallet Base Node Service initialized without shutdown signal");
info!(target: LOG_TARGET, "Wallet Base Node Service started");
loop {
futures::select! {
request_context = request_stream.select_next_some() => {
trace!(target: LOG_TARGET, "Handling Base Node Service API Request");
let (request, reply_tx) = request_context.split();
let response = self.handle_request(request).await.map_err(|e| {
error!(target: LOG_TARGET, "Error handling request: {:?}", e);
e
});
let _ = reply_tx.send(response).map_err(|e| {
warn!(target: LOG_TARGET, "Failed to send reply");
e
});
},
response = base_node_response_stream.select_next_some() => {
if let Err(e) = self.handle_base_node_response(response).await {
warn!(target: LOG_TARGET, "Failed to handle base node response: {}", e);
}
},
_ = refresh_tick.select_next_some() => {
if let Err(e) = self.refresh_chain_metadata().await {
warn!(target: LOG_TARGET, "Error when sending refresh chain metadata request: {}", e);
}
},
_ = shutdown_signal => {
info!(target: LOG_TARGET, "Wallet Base Node Service shutting down because the shutdown signal was received");
break Ok(());
}
}
}
}
async fn refresh_chain_metadata(&mut self) -> Result<(), BaseNodeServiceError> {
trace!(target: LOG_TARGET, "Refresh chain metadata");
let base_node_peer = self
.base_node_peer
.clone()
.ok_or_else(|| BaseNodeServiceError::NoBaseNodePeer)?;
let request_key = generate_request_key(&mut OsRng);
let now = Utc::now().naive_utc();
self.requests.push(RequestMetadata { request_key, sent: now });
let (current_requests, old_requests): (Vec<RequestMetadata>, Vec<RequestMetadata>) =
self.requests.iter().partition(|r| {
let age = Utc::now().naive_utc() - r.sent;
let age = Duration::from_millis(age.num_milliseconds() as u64);
age <= self.config.request_max_age
});
trace!(target: LOG_TARGET, "current requests: {:?}", current_requests);
trace!(target: LOG_TARGET, "discarded requests : {:?}", old_requests);
self.requests = current_requests;
self.check_online_status(old_requests.len());
let request = BaseNodeRequestProto::GetChainMetadata(true);
let service_request = proto::BaseNodeServiceRequest {
request_key,
request: Some(request),
};
let message = OutboundDomainMessage::new(TariMessageType::BaseNodeRequest, service_request);
let dest_public_key = base_node_peer.public_key;
self.outbound_messaging
.send_direct(dest_public_key, message)
.await
.map_err(BaseNodeServiceError::OutboundError)?;
debug!(target: LOG_TARGET, "Refresh chain metadata message sent");
Ok(())
}
fn check_online_status(&mut self, discarded: usize) {
let never_connected = discarded > 0 && self.state.updated.is_none();
let timing_out = if let Some(updated) = self.state.updated {
let now = Utc::now().naive_utc();
let millis = (now - updated).num_milliseconds() as u64;
let last_updated = Duration::from_millis(millis);
matches!(self.state.online, OnlineState::Online) && last_updated > self.config.request_max_age
} else {
false
};
if never_connected || timing_out {
info!(
target: LOG_TARGET,
"Base node is offline. Either we never connected ({}), or haven't received a response newer than the \
max request age ({}).",
never_connected,
timing_out
);
let state = self.state.set_online(OnlineState::Offline);
let event = BaseNodeEvent::BaseNodeState(state);
self.publish_event(event);
}
}
async fn handle_base_node_response(
&mut self,
response: DomainMessage<proto::BaseNodeServiceResponse>,
) -> Result<(), BaseNodeServiceError>
{
let DomainMessage::<_> { inner: message, .. } = response;
let (found, remaining): (Vec<RequestMetadata>, Vec<RequestMetadata>) = self
.requests
.iter()
.partition(|&&r| r.request_key == message.request_key);
if !found.is_empty() {
debug!(target: LOG_TARGET, "Handle base node response message: {:?}", message);
let now = Utc::now().naive_utc();
let time_sent = found.first().unwrap().sent;
let millis = (now - time_sent).num_milliseconds() as u64;
let latency = Duration::from_millis(millis);
match message.response {
Some(proto::base_node_service_response::Response::ChainMetadata(chain_metadata)) => {
trace!(target: LOG_TARGET, "Chain Metadata response {:?}", chain_metadata);
info!(target: LOG_TARGET, "Base node latency: {}ms", millis);
let state = BaseNodeState {
is_synced: Some(message.is_synced),
chain_metadata: Some(
chain_metadata
.try_into()
.map_err(BaseNodeServiceError::InvalidBaseNodeResponse)?,
),
updated: Some(now),
latency: Some(latency),
online: OnlineState::Online,
};
self.publish_event(BaseNodeEvent::BaseNodeState(state.clone()));
self.set_state(state);
},
_ => {
trace!(
target: LOG_TARGET,
"Received a base node response currently unaccounted for: {:?}",
message
);
},
}
self.requests = remaining;
}
Ok(())
}
fn reset_state(&mut self) {
self.requests = Vec::new();
let state = BaseNodeState::default();
self.publish_event(BaseNodeEvent::BaseNodeState(state.clone()));
self.set_state(state);
}
fn set_base_node_peer(&mut self, peer: Peer) {
self.reset_state();
self.base_node_peer = Some(peer.clone());
self.publish_event(BaseNodeEvent::BaseNodePeerSet(Box::new(peer)));
}
fn publish_event(&mut self, event: BaseNodeEvent) {
trace!(target: LOG_TARGET, "Publishing event: {:?}", event);
let _ = self.event_publisher.send(Arc::new(event)).map_err(|_| {
trace!(
target: LOG_TARGET,
"Could not publish BaseNodeEvent as there are no subscribers"
)
});
}
async fn handle_request(
&mut self,
request: BaseNodeServiceRequest,
) -> Result<BaseNodeServiceResponse, BaseNodeServiceError>
{
debug!(
target: LOG_TARGET,
"Handling Wallet Base Node Service Request: {:?}", request
);
match request {
BaseNodeServiceRequest::SetBaseNodePeer(peer) => {
self.set_base_node_peer(*peer);
Ok(BaseNodeServiceResponse::BaseNodePeerSet)
},
BaseNodeServiceRequest::GetChainMetadata => Ok(BaseNodeServiceResponse::ChainMetadata(
self.state.chain_metadata.clone(),
)),
}
}
}