Skip to main content

soil_client/storage_monitor/
mod.rs

1// This file is part of Soil.
2
3// Copyright (C) Soil contributors.
4// Copyright (C) Parity Technologies (UK) Ltd.
5// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
6
7use clap::Args;
8use std::{
9	io,
10	path::{Path, PathBuf},
11	time::Duration,
12};
13use subsoil::core::traits::SpawnEssentialNamed;
14
15const LOG_TARGET: &str = "storage-monitor";
16
17/// Result type used in this crate.
18pub type Result<T> = std::result::Result<T, Error>;
19
20/// Error type used in this crate.
21#[derive(Debug, thiserror::Error)]
22pub enum Error {
23	#[error("IO Error")]
24	IOError(#[from] io::Error),
25	#[error("Out of storage space: available {0}MiB, required {1}MiB")]
26	StorageOutOfSpace(u64, u64),
27}
28
29/// Parameters used to create the storage monitor.
30#[derive(Default, Debug, Clone, Args)]
31pub struct StorageMonitorParams {
32	/// Required available space on database storage.
33	///
34	/// If available space for DB storage drops below the given threshold, node will
35	/// be gracefully terminated.
36	///
37	/// If `0` is given monitoring will be disabled.
38	#[arg(long = "db-storage-threshold", value_name = "MiB", default_value_t = 1024)]
39	pub threshold: u64,
40
41	/// How often available space is polled.
42	#[arg(long = "db-storage-polling-period", value_name = "SECONDS", default_value_t = 5, value_parser = clap::value_parser!(u32).range(1..))]
43	pub polling_period: u32,
44}
45
46/// Storage monitor service: checks the available space for the filesystem for given path.
47pub struct StorageMonitorService {
48	/// watched path
49	path: PathBuf,
50	/// number of megabytes that shall be free on the filesystem for watched path
51	threshold: u64,
52	/// storage space polling period
53	polling_period: Duration,
54}
55
56impl StorageMonitorService {
57	/// Creates new StorageMonitorService for given client config
58	pub fn try_spawn(
59		parameters: StorageMonitorParams,
60		path: PathBuf,
61		spawner: &impl SpawnEssentialNamed,
62	) -> Result<()> {
63		if parameters.threshold == 0 {
64			log::info!(
65				target: LOG_TARGET,
66				"StorageMonitorService: threshold `0` given, storage monitoring disabled",
67			);
68		} else {
69			log::debug!(
70				target: LOG_TARGET,
71				"Initializing StorageMonitorService for db path: {}",
72				path.display()
73			);
74
75			Self::check_free_space(&path, parameters.threshold)?;
76
77			let storage_monitor_service = StorageMonitorService {
78				path,
79				threshold: parameters.threshold,
80				polling_period: Duration::from_secs(parameters.polling_period.into()),
81			};
82
83			spawner.spawn_essential(
84				"storage-monitor",
85				None,
86				Box::pin(storage_monitor_service.run()),
87			);
88		}
89
90		Ok(())
91	}
92
93	/// Main monitoring loop, intended to be spawned as essential task. Quits if free space drop
94	/// below threshold.
95	async fn run(self) {
96		loop {
97			tokio::time::sleep(self.polling_period).await;
98			if Self::check_free_space(&self.path, self.threshold).is_err() {
99				break;
100			};
101		}
102	}
103
104	/// Returns free space in MiB, or error if statvfs failed.
105	fn free_space(path: &Path) -> Result<u64> {
106		Ok(fs4::available_space(path).map(|s| s / 1024 / 1024)?)
107	}
108
109	/// Checks if the amount of free space for given `path` is above given `threshold` in MiB.
110	/// If it dropped below, error is returned.
111	/// System errors are silently ignored.
112	fn check_free_space(path: &Path, threshold: u64) -> Result<()> {
113		match StorageMonitorService::free_space(path) {
114			Ok(available_space) => {
115				log::trace!(
116					target: LOG_TARGET,
117					"free: {available_space} , threshold: {threshold}.",
118				);
119
120				if available_space < threshold {
121					log::error!(target: LOG_TARGET, "Available space {available_space}MiB for path `{}` dropped below threshold: {threshold}MiB , terminating...", path.display());
122					Err(Error::StorageOutOfSpace(available_space, threshold))
123				} else {
124					Ok(())
125				}
126			},
127			Err(e) => {
128				log::error!(target: LOG_TARGET, "Could not read available space: {e:?}.");
129				Err(e)
130			},
131		}
132	}
133}