mixnode: graceful shutdown on ctrl-c (#1304)

* mixnode: add graceful notification to most tasks

* Add note about remaining work

* task/shutdown: add shutdown timer

* changelog: add entry for shutdown

* mixnode: revert some temp changes

* common/task: make sure to use latest tokio
This commit is contained in:
Jon Häggblad
2022-06-07 11:41:58 +02:00
committed by GitHub
parent 7a74bb9ad5
commit f15ecdda06
12 changed files with 325 additions and 62 deletions
+1
View File
@@ -29,6 +29,7 @@
- mixnet-contract: replaced integer division with fixed for performance calculations ([#1284])
- mixnet-contract: Under certain circumstances nodes could not be unbonded ([#1255](https://github.com/nymtech/nym/issues/1255)) ([#1258])
- mixnode, gateway: attempting to determine reconnection backoff to persistently failing mixnode could result in a crash ([#1260])
- mixnode: the mixnode learned how to shutdown gracefully.
- vesting-contract: replaced `checked_sub` with `saturating_sub` to fix the underflow in `get_vesting_tokens` ([#1275])
### Changed
Generated
+10
View File
@@ -3181,6 +3181,7 @@ dependencies = [
"rocket",
"serde",
"serial_test",
"task",
"tokio",
"tokio-util 0.7.3",
"toml",
@@ -5509,6 +5510,15 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]]
name = "task"
version = "0.1.0"
dependencies = [
"log",
"tokio",
"tokio-util 0.6.9",
]
[[package]]
name = "tempfile"
version = "3.3.0"
+1
View File
@@ -55,6 +55,7 @@ members = [
"common/pemstore",
"common/socks5/proxy-helpers",
"common/socks5/requests",
"common/task",
"common/topology",
"common/wasm-utils",
"explorer-api",
+14
View File
@@ -0,0 +1,14 @@
[package]
name = "task"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
log = "0.4"
tokio = { version = "1.19.1", features = ["rt-multi-thread", "net", "signal"] }
tokio-util = { version = "0.7.3", features = ["codec"] }
[dev-dependencies]
tokio = { version = "1.19.1", features = ["rt-multi-thread", "net", "signal", "test-util", "macros"] }
+6
View File
@@ -0,0 +1,6 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod shutdown;
pub use shutdown::{ShutdownListener, ShutdownNotifier};
+106
View File
@@ -0,0 +1,106 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::time::Duration;
use tokio::sync::watch::{self, error::SendError};
const SHUTDOWN_TIMER_SECS: u64 = 5;
/// Used to notify other tasks to gracefully shutdown
#[derive(Debug)]
pub struct ShutdownNotifier {
notify_tx: watch::Sender<()>,
notify_rx: Option<watch::Receiver<()>>,
}
impl Default for ShutdownNotifier {
fn default() -> Self {
let (notify_tx, notify_rx) = watch::channel(());
Self {
notify_tx,
notify_rx: Some(notify_rx),
}
}
}
impl ShutdownNotifier {
pub fn subscribe(&self) -> ShutdownListener {
ShutdownListener::new(
self.notify_rx
.as_ref()
.expect("Unable to subscribe to shutdown notifier that is already shutdown")
.clone(),
)
}
pub fn signal_shutdown(&self) -> Result<(), SendError<()>> {
self.notify_tx.send(())
}
pub async fn wait_for_shutdown(&mut self) {
if let Some(notify_rx) = self.notify_rx.take() {
drop(notify_rx);
}
tokio::select! {
_ = self.notify_tx.closed() => {
log::info!("All registered tasks succesfully shutdown");
},
_ = tokio::signal::ctrl_c() => {
log::info!("Forcing shutdown");
}
_ = tokio::time::sleep(Duration::from_secs(SHUTDOWN_TIMER_SECS)) => {
log::info!("Timout reached, forcing shutdown");
},
}
}
}
/// Listen for shutdown notifications
#[derive(Clone, Debug)]
pub struct ShutdownListener {
shutdown: bool,
notify: watch::Receiver<()>,
}
impl ShutdownListener {
pub fn new(notify: watch::Receiver<()>) -> ShutdownListener {
ShutdownListener {
shutdown: false,
notify,
}
}
pub fn is_shutdown(&self) -> bool {
self.shutdown
}
pub async fn recv(&mut self) {
if self.shutdown {
return;
}
let _ = self.notify.changed().await;
self.shutdown = true;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn signal_shutdown() {
let shutdown = ShutdownNotifier::default();
let mut listener = shutdown.subscribe();
let task = tokio::spawn(async move {
tokio::select! {
_ = listener.recv() => 42,
}
});
shutdown.signal_shutdown().unwrap();
assert_eq!(task.await.unwrap(), 42);
}
}
+1
View File
@@ -43,6 +43,7 @@ mixnode-common = { path="../common/mixnode-common" }
nonexhaustive-delayqueue = { path="../common/nonexhaustive-delayqueue" }
nymsphinx = { path="../common/nymsphinx" }
pemstore = { path="../common/pemstore" }
task = { path = "../common/task" }
topology = { path="../common/topology" }
validator-client = { path="../common/client-libs/validator-client" }
version-checker = { path="../common/version-checker" }
@@ -5,6 +5,7 @@ use crate::node::listener::connection_handler::packet_processing::{
MixProcessingResult, PacketProcessor,
};
use crate::node::packet_delayforwarder::PacketDelayForwardSender;
use crate::node::ShutdownListener;
use futures::StreamExt;
use log::{error, info};
use nymsphinx::forwarding::packet::MixPacket;
@@ -69,28 +70,40 @@ impl ConnectionHandler {
}
}
pub(crate) async fn handle_connection(self, conn: TcpStream, remote: SocketAddr) {
pub(crate) async fn handle_connection(
self,
conn: TcpStream,
remote: SocketAddr,
mut shutdown: ShutdownListener,
) {
debug!("Starting connection handler for {:?}", remote);
let mut framed_conn = Framed::new(conn, SphinxCodec);
while let Some(framed_sphinx_packet) = framed_conn.next().await {
match framed_sphinx_packet {
Ok(framed_sphinx_packet) => {
// TODO: benchmark spawning tokio task with full processing vs just processing it
// synchronously (without delaying inside of course,
// delay is moved to a global DelayQueue)
// under higher load in single and multi-threaded situation.
while !shutdown.is_shutdown() {
tokio::select! {
Some(framed_sphinx_packet) = framed_conn.next() => {
match framed_sphinx_packet {
Ok(framed_sphinx_packet) => {
// TODO: benchmark spawning tokio task with full processing vs just processing it
// synchronously (without delaying inside of course,
// delay is moved to a global DelayQueue)
// under higher load in single and multi-threaded situation.
// in theory we could process multiple sphinx packet from the same connection in parallel,
// but we already handle multiple concurrent connections so if anything, making
// that change would only slow things down
self.handle_received_packet(framed_sphinx_packet);
}
Err(err) => {
error!(
"The socket connection got corrupted with error: {:?}. Closing the socket",
err
);
return;
// in theory we could process multiple sphinx packet from the same connection in parallel,
// but we already handle multiple concurrent connections so if anything, making
// that change would only slow things down
self.handle_received_packet(framed_sphinx_packet);
}
Err(err) => {
error!(
"The socket connection got corrupted with error: {:?}. Closing the socket",
err
);
return;
}
}
},
_ = shutdown.recv() => {
log::trace!("ConnectionHandler: received shutdown");
}
}
}
@@ -99,5 +112,6 @@ impl ConnectionHandler {
"Closing connection from {:?}",
framed_conn.into_inner().peer_addr()
);
log::trace!("ConnectionHandler: Exiting");
}
}
+21 -9
View File
@@ -8,18 +8,22 @@ use std::process;
use tokio::net::TcpListener;
use tokio::task::JoinHandle;
use super::ShutdownListener;
pub(crate) mod connection_handler;
pub(crate) struct Listener {
address: SocketAddr,
shutdown: ShutdownListener,
}
impl Listener {
pub(crate) fn new(address: SocketAddr) -> Self {
Listener { address }
pub(crate) fn new(address: SocketAddr, shutdown: ShutdownListener) -> Self {
Listener { address, shutdown }
}
async fn run(&mut self, connection_handler: ConnectionHandler) {
log::trace!("Starting Listener");
let listener = match TcpListener::bind(self.address).await {
Ok(listener) => listener,
Err(err) => {
@@ -28,15 +32,23 @@ impl Listener {
}
};
loop {
match listener.accept().await {
Ok((socket, remote_addr)) => {
let handler = connection_handler.clone();
tokio::spawn(handler.handle_connection(socket, remote_addr));
while !self.shutdown.is_shutdown() {
tokio::select! {
connection = listener.accept() => {
match connection {
Ok((socket, remote_addr)) => {
let handler = connection_handler.clone();
tokio::spawn(handler.handle_connection(socket, remote_addr, self.shutdown.clone()));
}
Err(err) => warn!("Failed to accept incoming connection - {:?}", err),
}
},
_ = self.shutdown.recv() => {
log::trace!("Listener: Received shutdown");
}
Err(err) => warn!("Failed to accept incoming connection - {:?}", err),
}
};
}
log::trace!("Listener: Exiting");
}
pub(crate) fn start(mut self, connection_handler: ConnectionHandler) -> JoinHandle<()> {
+40 -11
View File
@@ -25,6 +25,7 @@ use rand::thread_rng;
use std::net::SocketAddr;
use std::process;
use std::sync::Arc;
use task::{ShutdownListener, ShutdownNotifier};
use version_checker::parse_version;
mod http;
@@ -149,11 +150,15 @@ impl MixNode {
});
}
fn start_node_stats_controller(&self) -> (SharedNodeStats, node_statistics::UpdateSender) {
fn start_node_stats_controller(
&self,
shutdown: ShutdownListener,
) -> (SharedNodeStats, node_statistics::UpdateSender) {
info!("Starting node stats controller...");
let controller = node_statistics::Controller::new(
self.config.get_node_stats_logging_delay(),
self.config.get_node_stats_updating_delay(),
shutdown,
);
let node_stats_pointer = controller.get_node_stats_data_pointer();
let update_sender = controller.start();
@@ -165,6 +170,7 @@ impl MixNode {
&self,
node_stats_update_sender: node_statistics::UpdateSender,
delay_forwarding_channel: PacketDelayForwardSender,
shutdown: ShutdownListener,
) {
info!("Starting socket listener...");
@@ -178,12 +184,13 @@ impl MixNode {
self.config.get_mix_port(),
);
Listener::new(listening_address).start(connection_handler);
Listener::new(listening_address, shutdown).start(connection_handler);
}
fn start_packet_delay_forwarder(
&mut self,
node_stats_update_sender: node_statistics::UpdateSender,
shutdown: ShutdownListener,
) -> PacketDelayForwardSender {
info!("Starting packet delay-forwarder...");
@@ -197,6 +204,7 @@ impl MixNode {
let mut packet_forwarder = DelayForwarder::new(
mixnet_client::Client::new(client_config),
node_stats_update_sender,
shutdown,
);
let packet_sender = packet_forwarder.sender();
@@ -259,7 +267,10 @@ impl MixNode {
let existing_nodes = match validator_client.get_cached_mixnodes().await {
Ok(nodes) => nodes,
Err(err) => {
error!("failed to grab initial network mixnodes - {}\n Please try to startup again in few minutes", err);
error!(
"failed to grab initial network mixnodes - {err}\n \
Please try to startup again in few minutes",
);
process::exit(1);
}
};
@@ -272,16 +283,26 @@ impl MixNode {
.map(|node| node.mix_node.identity_key.clone())
}
async fn wait_for_interrupt(&self) {
async fn wait_for_interrupt(&self, mut shutdown: ShutdownNotifier) {
if let Err(e) = tokio::signal::ctrl_c().await {
error!(
"There was an error while capturing SIGINT - {:?}. We will terminate regardless",
"There was an error while capturing SIGINT - {:?}. \
We will terminate regardless",
e
);
}
println!(
"Received SIGINT - the mixnode will terminate now (threads are not yet nicely stopped, if you see stack traces that's alright)."
"Received SIGINT - the mixnode will terminate now \
(threads are not yet nicely stopped, if you see stack traces that's alright)."
);
log::info!("Sending shutdown");
shutdown.signal_shutdown().ok();
log::info!("Waiting for tasks to finish... (Press ctrl-c to force)");
shutdown.wait_for_shutdown().await;
log::info!("Stopping nym mixnode");
}
pub async fn run(&mut self) {
@@ -299,15 +320,23 @@ impl MixNode {
}
}
let (node_stats_pointer, node_stats_update_sender) = self.start_node_stats_controller();
let delay_forwarding_channel =
self.start_packet_delay_forwarder(node_stats_update_sender.clone());
self.start_socket_listener(node_stats_update_sender, delay_forwarding_channel);
let shutdown = ShutdownNotifier::default();
let (node_stats_pointer, node_stats_update_sender) =
self.start_node_stats_controller(shutdown.subscribe());
let delay_forwarding_channel = self
.start_packet_delay_forwarder(node_stats_update_sender.clone(), shutdown.subscribe());
self.start_socket_listener(
node_stats_update_sender,
delay_forwarding_channel,
shutdown.subscribe(),
);
// TODO: these two also needs to be shutdown
let atomic_verloc_results = self.start_verloc_measurements();
self.start_http_api(atomic_verloc_results, node_stats_pointer);
info!("Finished nym mixnode startup procedure - it should now be able to receive mix traffic!");
self.wait_for_interrupt().await
self.wait_for_interrupt(shutdown).await
}
}
+72 -21
View File
@@ -9,6 +9,8 @@ use std::sync::Arc;
use std::time::{Duration, SystemTime};
use tokio::sync::{RwLock, RwLockReadGuard};
use super::ShutdownListener;
// convenience aliases
type PacketsMap = HashMap<String, u64>;
type PacketDataReceiver = mpsc::UnboundedReceiver<PacketEvent>;
@@ -209,28 +211,45 @@ impl CurrentPacketData {
struct UpdateHandler {
current_data: CurrentPacketData,
update_receiver: PacketDataReceiver,
shutdown: ShutdownListener,
}
impl UpdateHandler {
fn new(current_data: CurrentPacketData, update_receiver: PacketDataReceiver) -> Self {
fn new(
current_data: CurrentPacketData,
update_receiver: PacketDataReceiver,
shutdown: ShutdownListener,
) -> Self {
UpdateHandler {
current_data,
update_receiver,
shutdown,
}
}
async fn run(&mut self) {
while let Some(packet_data) = self.update_receiver.next().await {
match packet_data {
PacketEvent::Received => self.current_data.increment_received(),
PacketEvent::Sent(destination) => {
self.current_data.increment_sent(destination).await
log::trace!("Starting UpdateHandler");
while !self.shutdown.is_shutdown() {
tokio::select! {
Some(packet_data) = self.update_receiver.next() => {
match packet_data {
PacketEvent::Received => self.current_data.increment_received(),
PacketEvent::Sent(destination) => {
self.current_data.increment_sent(destination).await
}
PacketEvent::Dropped(destination) => {
self.current_data.increment_dropped(destination).await
}
}
}
PacketEvent::Dropped(destination) => {
self.current_data.increment_dropped(destination).await
_ = self.shutdown.recv() => {
log::trace!("UpdateHandler: Received shutdown");
break;
}
}
}
log::trace!("UpdateHandler: Exiting");
}
}
@@ -274,6 +293,7 @@ struct StatsUpdater {
updating_delay: Duration,
current_packet_data: CurrentPacketData,
current_stats: SharedNodeStats,
shutdown: ShutdownListener,
}
impl StatsUpdater {
@@ -281,11 +301,13 @@ impl StatsUpdater {
updating_delay: Duration,
current_packet_data: CurrentPacketData,
current_stats: SharedNodeStats,
shutdown: ShutdownListener,
) -> Self {
StatsUpdater {
updating_delay,
current_packet_data,
current_stats,
shutdown,
}
}
@@ -295,11 +317,16 @@ impl StatsUpdater {
self.current_stats.update(received, sent, dropped).await;
}
async fn run(&self) {
loop {
tokio::time::sleep(self.updating_delay).await;
self.update_stats().await
async fn run(&mut self) {
while !self.shutdown.is_shutdown() {
tokio::select! {
_ = tokio::time::sleep(self.updating_delay) => self.update_stats().await,
_ = self.shutdown.recv() => {
log::trace!("StatsUpdater: Received shutdown");
}
}
}
log::trace!("StatsUpdater: Exiting");
}
}
@@ -308,13 +335,15 @@ impl StatsUpdater {
struct PacketStatsConsoleLogger {
logging_delay: Duration,
stats: SharedNodeStats,
shutdown: ShutdownListener,
}
impl PacketStatsConsoleLogger {
fn new(logging_delay: Duration, stats: SharedNodeStats) -> Self {
fn new(logging_delay: Duration, stats: SharedNodeStats, shutdown: ShutdownListener) -> Self {
PacketStatsConsoleLogger {
logging_delay,
stats,
shutdown,
}
}
@@ -387,10 +416,16 @@ impl PacketStatsConsoleLogger {
}
async fn run(&mut self) {
loop {
tokio::time::sleep(self.logging_delay).await;
self.log_running_stats().await;
log::trace!("Starting PacketStatsConsoleLogger");
while !self.shutdown.is_shutdown() {
tokio::select! {
_ = tokio::time::sleep(self.logging_delay) => self.log_running_stats().await,
_ = self.shutdown.recv() => {
log::trace!("PacketStatsConsoleLogger: Received shutdown");
}
};
}
log::trace!("PacketStatsConsoleLogger: Exiting");
}
}
@@ -413,19 +448,32 @@ pub struct Controller {
}
impl Controller {
pub(crate) fn new(logging_delay: Duration, stats_updating_delay: Duration) -> Self {
pub(crate) fn new(
logging_delay: Duration,
stats_updating_delay: Duration,
shutdown: ShutdownListener,
) -> Self {
let (sender, receiver) = mpsc::unbounded();
let shared_packet_data = CurrentPacketData::new();
let shared_node_stats = SharedNodeStats::new();
Controller {
update_handler: UpdateHandler::new(shared_packet_data.clone(), receiver),
update_handler: UpdateHandler::new(
shared_packet_data.clone(),
receiver,
shutdown.clone(),
),
update_sender: UpdateSender::new(sender),
console_logger: PacketStatsConsoleLogger::new(logging_delay, shared_node_stats.clone()),
console_logger: PacketStatsConsoleLogger::new(
logging_delay,
shared_node_stats.clone(),
shutdown.clone(),
),
stats_updater: StatsUpdater::new(
stats_updating_delay,
shared_packet_data,
shared_node_stats.clone(),
shutdown,
),
node_stats: shared_node_stats,
}
@@ -441,7 +489,7 @@ impl Controller {
pub(crate) fn start(self) -> UpdateSender {
// move out of self
let mut update_handler = self.update_handler;
let stats_updater = self.stats_updater;
let mut stats_updater = self.stats_updater;
let mut console_logger = self.console_logger;
tokio::spawn(async move { update_handler.run().await });
@@ -455,12 +503,15 @@ impl Controller {
#[cfg(test)]
mod tests {
use super::*;
use task::ShutdownNotifier;
#[tokio::test]
async fn node_stats_reported_are_received() {
let logging_delay = Duration::from_millis(20);
let stats_updating_delay = Duration::from_millis(10);
let node_stats_controller = Controller::new(logging_delay, stats_updating_delay);
let shutdown = ShutdownNotifier::default();
let node_stats_controller =
Controller::new(logging_delay, stats_updating_delay, shutdown.subscribe());
let node_stats_pointer = node_stats_controller.get_node_stats_data_pointer();
let update_sender = node_stats_controller.start();
+20 -2
View File
@@ -9,6 +9,8 @@ use nymsphinx::forwarding::packet::MixPacket;
use std::io;
use tokio::time::Instant;
use super::ShutdownListener;
// Delay + MixPacket vs Instant + MixPacket
// rather than using Duration directly, we use an Instant, this way we minimise skew due to
@@ -26,13 +28,18 @@ where
packet_sender: PacketDelayForwardSender,
packet_receiver: PacketDelayForwardReceiver,
node_stats_update_sender: UpdateSender,
shutdown: ShutdownListener,
}
impl<C> DelayForwarder<C>
where
C: mixnet_client::SendWithoutResponse,
{
pub(crate) fn new(client: C, node_stats_update_sender: UpdateSender) -> DelayForwarder<C> {
pub(crate) fn new(
client: C,
node_stats_update_sender: UpdateSender,
shutdown: ShutdownListener,
) -> DelayForwarder<C> {
let (packet_sender, packet_receiver) = mpsc::unbounded();
DelayForwarder::<C> {
@@ -41,6 +48,7 @@ where
packet_sender,
packet_receiver,
node_stats_update_sender,
shutdown,
}
}
@@ -97,6 +105,7 @@ where
}
pub(crate) async fn run(&mut self) {
log::trace!("Starting DelayForwarder");
loop {
tokio::select! {
delayed = self.delay_queue.next() => {
@@ -107,8 +116,13 @@ where
// and hence it can't happen that ALL senders are dropped
self.handle_new_packet(new_packet.unwrap())
}
_ = self.shutdown.recv() => {
log::trace!("DelayForwarder: Received shutdown");
break;
}
}
}
log::trace!("DelayForwarder: Exiting");
}
}
@@ -120,6 +134,8 @@ mod tests {
use std::sync::{Arc, Mutex};
use std::time::Duration;
use task::ShutdownNotifier;
use nymsphinx::addressing::nodes::NymNodeRoutingAddress;
use nymsphinx_params::packet_sizes::PacketSize;
use nymsphinx_params::PacketMode;
@@ -189,7 +205,9 @@ mod tests {
let node_stats_update_sender = UpdateSender::new(stats_sender);
let client = TestClient::default();
let client_packets_sent = client.packets_sent.clone();
let mut delay_forwarder = DelayForwarder::new(client, node_stats_update_sender);
let shutdown = ShutdownNotifier::default();
let mut delay_forwarder =
DelayForwarder::new(client, node_stats_update_sender, shutdown.subscribe());
let packet_sender = delay_forwarder.sender();
// Spawn the worker, listening on packet_sender channel