Skip to content

Commit

Permalink
Monitor free disk space of the node path and show warning below 10GiB
Browse files Browse the repository at this point in the history
  • Loading branch information
nazar-pc committed Jan 26, 2024
1 parent 9412a13 commit e59a151
Show file tree
Hide file tree
Showing 5 changed files with 161 additions and 28 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ duct = "0.13.6"
event-listener-primitives = "2.0.1"
file-rotate = "0.7.5"
frame-system = { git = "https://github.com/subspace/polkadot-sdk", rev = "c63a8b28a9fd26d42116b0dcef1f2a5cefb9cd1c", default-features = false }
fs4 = "0.7.0"
futures = "0.3.29"
gtk = { version = "0.7.3", package = "gtk4" }
hex = "0.4.3"
Expand Down
8 changes: 8 additions & 0 deletions res/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ progressbar progress {
color: #ff3800;
}

.free-disk-space > trough > block.low {
background-color: #ff3800;
}

.free-disk-space > trough > block.high {
background-color: #ffA400;
}

farm-sectors > child {
padding: 0;
}
Expand Down
1 change: 1 addition & 0 deletions src/frontend/running.rs
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ impl RunningView {
self.node_view.emit(NodeInput::Initialize {
best_block_number,
chain_info,
node_path: raw_config.node_path().clone(),
});
}
RunningInput::NodeNotification(node_notification) => {
Expand Down
178 changes: 150 additions & 28 deletions src/frontend/running/node.rs
Original file line number Diff line number Diff line change
@@ -1,40 +1,55 @@
use crate::backend::node::{ChainInfo, SyncKind, SyncState};
use crate::backend::NodeNotification;
use bytesize::ByteSize;
use gtk::prelude::*;
use parking_lot::Mutex;
use relm4::prelude::*;
use relm4::{Sender, ShutdownReceiver};
use relm4_icons::icon_name;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use subspace_core_primitives::BlockNumber;
use tracing::error;

/// Maximum blocks to store in the import queue.
// HACK: This constant comes from Substrate's sync, but it is not public in there
const MAX_IMPORTING_BLOCKS: BlockNumber = 2048;
/// How frequently to check for free disk space
const FREE_DISK_SPACE_CHECK_INTERVAL: Duration = Duration::from_secs(5);
/// Free disk space below which warning must be shown
const FREE_DISK_SPACE_CHECK_WARNING_THRESHOLD: u64 = 10 * 1024 * 1024 * 1024;

#[derive(Debug)]
pub enum NodeInput {
Initialize {
best_block_number: BlockNumber,
chain_info: ChainInfo,
node_path: PathBuf,
},
NodeNotification(NodeNotification),
}

#[derive(Debug, Default)]
struct NodeState {
best_block_number: BlockNumber,
sync_state: SyncState,
#[derive(Debug)]
pub enum NodeCommandOutput {
FreeDiskSpace(ByteSize),
}

#[derive(Debug)]
pub struct NodeView {
node_state: NodeState,
best_block_number: BlockNumber,
sync_state: SyncState,
free_disk_space: Option<ByteSize>,
chain_name: String,
node_path: Arc<Mutex<PathBuf>>,
}

#[relm4::component(pub)]
impl Component for NodeView {
type Init = ();
type Input = NodeInput;
type Output = ();
type CommandOutput = ();
type CommandOutput = NodeCommandOutput;

view! {
#[root]
Expand All @@ -43,21 +58,60 @@ impl Component for NodeView {
set_orientation: gtk::Orientation::Vertical,
set_spacing: 10,

gtk::Label {
add_css_class: "heading",
set_halign: gtk::Align::Start,
#[watch]
set_label: &model.chain_name,
gtk::Box {
gtk::Label {
add_css_class: "heading",
set_halign: gtk::Align::Start,
#[watch]
set_label: &model.chain_name,
},

gtk::Box {
set_halign: gtk::Align::End,
set_hexpand: true,

gtk::Box {
set_spacing: 10,
#[watch]
set_tooltip: &format!(
"Free disk space: {} remaining",
model.free_disk_space
.map(|bytes| bytes.to_string_as(true))
.unwrap_or_default()
),
#[watch]
set_visible: model.free_disk_space
.map(|bytes| bytes.as_u64() <= FREE_DISK_SPACE_CHECK_WARNING_THRESHOLD)
.unwrap_or_default(),

gtk::Image {
set_icon_name: Some(icon_name::SSD),
},

gtk::LevelBar {
add_css_class: "free-disk-space",
set_min_value: 0.1,
#[watch]
set_value: {
let free_space = model.free_disk_space
.map(|bytes| bytes.as_u64())
.unwrap_or_default();
free_space as f64 / FREE_DISK_SPACE_CHECK_WARNING_THRESHOLD as f64
},
set_width_request: 100,
},
},
},
},

#[transition = "SlideUpDown"]
match model.node_state.sync_state {
match model.sync_state {
SyncState::Unknown => gtk::Box {
gtk::Label {
#[watch]
set_label: &format!(
"Connecting to the network, best block #{}",
model.node_state.best_block_number
model.best_block_number
),
}
},
Expand All @@ -81,7 +135,7 @@ impl Component for NodeView {
format!(
"{} #{}/{}{}",
kind,
model.node_state.best_block_number,
model.best_block_number,
target,
speed
.map(|speed| format!(", {:.2} blocks/s", speed))
Expand All @@ -97,13 +151,13 @@ impl Component for NodeView {

gtk::ProgressBar {
#[watch]
set_fraction: model.node_state.best_block_number as f64 / target as f64,
set_fraction: model.best_block_number as f64 / target as f64,
},
},
SyncState::Idle => gtk::Box {
gtk::Label {
#[watch]
set_label: &format!("Synced, best block #{}", model.node_state.best_block_number),
set_label: &format!("Synced, best block #{}", model.best_block_number),
}
},
},
Expand All @@ -113,21 +167,38 @@ impl Component for NodeView {
fn init(
_init: Self::Init,
_root: Self::Root,
_sender: ComponentSender<Self>,
sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
let node_path = Arc::<Mutex<PathBuf>>::default();
let model = Self {
node_state: NodeState::default(),
best_block_number: 0,
sync_state: SyncState::default(),
free_disk_space: None,
chain_name: String::new(),
node_path: node_path.clone(),
};

let widgets = view_output!();

sender.command(move |sender, shutdown_receiver| async move {
Self::check_free_disk_space(sender, shutdown_receiver, node_path).await;
});

ComponentParts { model, widgets }
}

fn update(&mut self, input: Self::Input, _sender: ComponentSender<Self>, _root: &Self::Root) {
self.process_input(input);
}

fn update_cmd(
&mut self,
input: Self::CommandOutput,
_sender: ComponentSender<Self>,
_root: &Self::Root,
) {
self.process_command(input);
}
}

impl NodeView {
Expand All @@ -136,31 +207,30 @@ impl NodeView {
NodeInput::Initialize {
best_block_number,
chain_info,
node_path,
} => {
self.node_state = NodeState {
best_block_number,
sync_state: SyncState::default(),
};
self.best_block_number = best_block_number;
self.chain_name = format!(
"{} consensus node",
chain_info
.chain_name
.strip_prefix("Subspace ")
.unwrap_or(&chain_info.chain_name)
);
*self.node_path.lock() = node_path;
}
NodeInput::NodeNotification(node_notification) => match node_notification {
NodeNotification::SyncStateUpdate(mut sync_state) => {
if let SyncState::Syncing {
target: new_target, ..
} = &mut sync_state
{
*new_target = (*new_target).max(self.node_state.best_block_number);
*new_target = (*new_target).max(self.best_block_number);

// Ensure target is never below current block
if let SyncState::Syncing {
target: old_target, ..
} = &self.node_state.sync_state
} = &self.sync_state
{
// If old target was within `MAX_IMPORTING_BLOCKS` from new target, keep old target
if old_target
Expand All @@ -172,16 +242,68 @@ impl NodeView {
}
}
}
self.node_state.sync_state = sync_state;
self.sync_state = sync_state;
}
NodeNotification::BlockImported(imported_block) => {
self.node_state.best_block_number = imported_block.number;
self.best_block_number = imported_block.number;
// Ensure target is never below current block
if let SyncState::Syncing { target, .. } = &mut self.node_state.sync_state {
*target = (*target).max(self.node_state.best_block_number);
if let SyncState::Syncing { target, .. } = &mut self.sync_state {
*target = (*target).max(self.best_block_number);
}
}
},
}
}

fn process_command(&mut self, command_output: NodeCommandOutput) {
match command_output {
NodeCommandOutput::FreeDiskSpace(bytes) => {
self.free_disk_space.replace(bytes);
}
}
}

async fn check_free_disk_space(
sender: Sender<NodeCommandOutput>,
shutdown_receiver: ShutdownReceiver,
node_path: Arc<Mutex<PathBuf>>,
) {
shutdown_receiver
.register(async move {
loop {
let node_path = node_path.lock().clone();

if node_path == PathBuf::default() {
tokio::time::sleep(FREE_DISK_SPACE_CHECK_INTERVAL).await;
continue;
}

match tokio::task::spawn_blocking(move || fs4::available_space(node_path)).await
{
Ok(Ok(free_disk_space)) => {
if sender
.send(NodeCommandOutput::FreeDiskSpace(ByteSize::b(
free_disk_space,
)))
.is_err()
{
break;
}
}
Ok(Err(error)) => {
error!(%error, "Failed to check free disk space");
break;
}
Err(error) => {
error!(%error, "Free disk space task panicked");
break;
}
}

tokio::time::sleep(FREE_DISK_SPACE_CHECK_INTERVAL).await;
}
})
.drop_on_shutdown()
.await
}
}

0 comments on commit e59a151

Please sign in to comment.