feat(event-notifier): improve environment variable handling

- Fix deserialization error when parsing config from environment variables
- Add proper array format support for adapters configuration
- Update environment variable examples with correct format
- Improve documentation for configuration loading
- Implement helper functions for environment variable validation

This change fixes the "invalid type: map, expected a sequence" error
by ensuring proper formatting of array-type fields in environment variables.
This commit is contained in:
houseme
2025-04-22 20:31:38 +08:00
parent 15b6a426fb
commit e4453adf82
18 changed files with 345 additions and 261 deletions
Generated
+1 -102
View File
@@ -664,15 +664,6 @@ dependencies = [
"num-traits",
]
[[package]]
name = "atomic"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d818003e740b63afc82337e3160717f4f63078720a810b7b903e70a5d1d2994"
dependencies = [
"bytemuck",
]
[[package]]
name = "atomic-waker"
version = "1.1.2"
@@ -1026,12 +1017,6 @@ version = "3.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
[[package]]
name = "bytemuck"
version = "1.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6b1fc10dbac614ebc03540c9dbd60e83887fda27794998c6528f1782047d540"
[[package]]
name = "byteorder"
version = "1.5.0"
@@ -3034,12 +3019,6 @@ dependencies = [
"const-random",
]
[[package]]
name = "dotenvy"
version = "0.15.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
[[package]]
name = "dpi"
version = "0.1.1"
@@ -3288,21 +3267,6 @@ dependencies = [
"rustc_version",
]
[[package]]
name = "figment"
version = "0.10.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8cb01cd46b0cf372153850f4c6c272d9cbea2da513e07538405148f95bd789f3"
dependencies = [
"atomic",
"pear",
"serde",
"serde_yaml",
"toml",
"uncased",
"version_check",
]
[[package]]
name = "fixedbitset"
version = "0.5.7"
@@ -4435,12 +4399,6 @@ dependencies = [
"cfb",
]
[[package]]
name = "inlinable_string"
version = "0.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb"
[[package]]
name = "inout"
version = "0.1.4"
@@ -6048,29 +6006,6 @@ dependencies = [
"hmac 0.12.1",
]
[[package]]
name = "pear"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bdeeaa00ce488657faba8ebf44ab9361f9365a97bd39ffb8a60663f57ff4b467"
dependencies = [
"inlinable_string",
"pear_codegen",
"yansi",
]
[[package]]
name = "pear_codegen"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bab5b985dc082b345f812b7df84e1bef27e7207b39e448439ba8bd69c93f147"
dependencies = [
"proc-macro2",
"proc-macro2-diagnostics",
"quote",
"syn 2.0.100",
]
[[package]]
name = "pem"
version = "3.0.5"
@@ -6549,7 +6484,6 @@ dependencies = [
"quote",
"syn 2.0.100",
"version_check",
"yansi",
]
[[package]]
@@ -7351,8 +7285,7 @@ version = "0.0.1"
dependencies = [
"async-trait",
"axum",
"dotenvy",
"figment",
"config",
"http",
"rdkafka",
"reqwest",
@@ -7832,19 +7765,6 @@ dependencies = [
"syn 2.0.100",
]
[[package]]
name = "serde_yaml"
version = "0.9.34+deprecated"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
dependencies = [
"indexmap 2.9.0",
"itoa 1.0.15",
"ryu",
"serde",
"unsafe-libyaml",
]
[[package]]
name = "server_fn"
version = "0.6.15"
@@ -9175,15 +9095,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "uncased"
version = "0.9.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1b88fcfe09e89d3866a5c11019378088af2d24c3fbd4f0543f96b479ec90697"
dependencies = [
"version_check",
]
[[package]]
name = "unicase"
version = "2.8.1"
@@ -9224,12 +9135,6 @@ dependencies = [
"subtle",
]
[[package]]
name = "unsafe-libyaml"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
[[package]]
name = "untrusted"
version = "0.9.0"
@@ -10223,12 +10128,6 @@ dependencies = [
"hashlink",
]
[[package]]
name = "yansi"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
[[package]]
name = "yoke"
version = "0.7.5"
-2
View File
@@ -65,8 +65,6 @@ datafusion = "46.0.1"
derive_builder = "0.20.2"
dioxus = { version = "0.6.3", features = ["router"] }
dirs = "6.0.0"
dotenvy = "0.15.7"
figment = { version = "0.10.19", features = ["toml", "yaml", "env"] }
flatbuffers = "25.2.10"
futures = "0.3.31"
futures-core = "0.3.31"
+1 -2
View File
@@ -14,8 +14,7 @@ mqtt = ["rumqttc"]
[dependencies]
async-trait = { workspace = true }
dotenvy = { workspace = true }
figment = { workspace = true, features = ["toml", "yaml", "env"] }
config = { workspace = true }
rdkafka = { workspace = true, features = ["tokio"], optional = true }
reqwest = { workspace = true, optional = true }
rumqttc = { workspace = true, optional = true }
@@ -0,0 +1,28 @@
# ===== 全局配置 =====
NOTIFIER__STORE_PATH=/var/log/event-notification
NOTIFIER__CHANNEL_CAPACITY=5000
# ===== 适配器配置(数组格式) =====
# Webhook 适配器(索引 0
NOTIFIER__ADAPTERS_0__type=Webhook
NOTIFIER__ADAPTERS_0__endpoint=http://127.0.0.1:3000/webhook
NOTIFIER__ADAPTERS_0__auth_token=your-auth-token
NOTIFIER__ADAPTERS_0__max_retries=3
NOTIFIER__ADAPTERS_0__timeout=50
NOTIFIER__ADAPTERS_0__custom_headers__x_custom_server=value
NOTIFIER__ADAPTERS_0__custom_headers__x_custom_client=value
# Kafka 适配器(索引 1
NOTIFIER__ADAPTERS_1__type=Kafka
NOTIFIER__ADAPTERS_1__brokers=localhost:9092
NOTIFIER__ADAPTERS_1__topic=notifications
NOTIFIER__ADAPTERS_1__max_retries=3
NOTIFIER__ADAPTERS_1__timeout=60
# MQTT 适配器(索引 2
NOTIFIER__ADAPTERS_2__type=Mqtt
NOTIFIER__ADAPTERS_2__broker=mqtt.example.com
NOTIFIER__ADAPTERS_2__port=1883
NOTIFIER__ADAPTERS_2__client_id=event-notifier
NOTIFIER__ADAPTERS_2__topic=events
NOTIFIER__ADAPTERS_2__max_retries=3
+25 -24
View File
@@ -1,27 +1,28 @@
# basic configuration
EVENT_NOTIF_STORE_PATH=/var/log/event-notification
EVENT_NOTIF_CHANNEL_CAPACITY=5000
# ===== global configuration =====
NOTIFIER__STORE_PATH=/var/log/event-notification
NOTIFIER__CHANNEL_CAPACITY=5000
# webhook adapter configuration
EVENT_NOTIF_ADAPTERS__0__TYPE=Webhook
EVENT_NOTIF_ADAPTERS__0__ENDPOINT=https://api.example.com/webhook
EVENT_NOTIF_ADAPTERS__0__AUTH_TOKEN=your-secret-token
EVENT_NOTIF_ADAPTERS__0__MAX_RETRIES=3
EVENT_NOTIF_ADAPTERS__0__TIMEOUT=5000
# ===== adapter configuration array format =====
# webhook adapter index 0
NOTIFIER__ADAPTERS_0__type=Webhook
NOTIFIER__ADAPTERS_0__endpoint=http://127.0.0.1:3000/webhook
NOTIFIER__ADAPTERS_0__auth_token=your-auth-token
NOTIFIER__ADAPTERS_0__max_retries=3
NOTIFIER__ADAPTERS_0__timeout=50
NOTIFIER__ADAPTERS_0__custom_headers__x_custom_server=server-value
NOTIFIER__ADAPTERS_0__custom_headers__x_custom_client=client-value
# kafka adapter configuration
EVENT_NOTIF_ADAPTERS__1__TYPE=Kafka
EVENT_NOTIF_ADAPTERS__1__BROKERS=localhost:9092
EVENT_NOTIF_ADAPTERS__1__TOPIC=notifications
EVENT_NOTIF_ADAPTERS__1__MAX_RETRIES=3
EVENT_NOTIF_ADAPTERS__1__TIMEOUT=5000
# kafka adapter index 1
NOTIFIER__ADAPTERS_1__type=Kafka
NOTIFIER__ADAPTERS_1__brokers=localhost:9092
NOTIFIER__ADAPTERS_1__topic=notifications
NOTIFIER__ADAPTERS_1__max_retries=3
NOTIFIER__ADAPTERS_1__timeout=60
# mqtt adapter configuration
EVENT_NOTIF_ADAPTERS__2__TYPE=Mqtt
EVENT_NOTIF_ADAPTERS__2__BROKER=mqtt.example.com
EVENT_NOTIF_ADAPTERS__2__PORT=1883
EVENT_NOTIF_ADAPTERS__2__CLIENT_ID=event-notifier
EVENT_NOTIF_ADAPTERS__2__TOPIC=events
EVENT_NOTIF_ADAPTERS__2__MAX_RETRIES=3
EVENT_NOTIF_HTTP__PORT=8080
# mqtt adapter index 2
NOTIFIER__ADAPTERS_2__type=Mqtt
NOTIFIER__ADAPTERS_2__broker=mqtt.example.com
NOTIFIER__ADAPTERS_2__port=1883
NOTIFIER__ADAPTERS_2__client_id=event-notifier
NOTIFIER__ADAPTERS_2__topic=events
NOTIFIER__ADAPTERS_2__max_retries=3
+9 -8
View File
@@ -1,20 +1,24 @@
# config.toml
store_path = "/var/log/event-notification"
store_path = "/var/log/event-notifier"
channel_capacity = 5000
[[adapters]]
type = "Webhook"
endpoint = "https://api.example.com/webhook"
endpoint = "http://127.0.0.1:3000/webhook"
auth_token = "your-auth-token"
max_retries = 3
timeout = 5000
timeout = 50
[adapters.custom_headers]
custom_server = "value_server"
custom_client = "value_client"
[[adapters]]
type = "Kafka"
brokers = "localhost:9092"
topic = "notifications"
max_retries = 3
timeout = 5000
timeout = 60
[[adapters]]
type = "Mqtt"
@@ -22,7 +26,4 @@ broker = "mqtt.example.com"
port = 1883
client_id = "event-notifier"
topic = "events"
max_retries = 3
[http]
port = 8080
max_retries = 3
+5 -7
View File
@@ -1,6 +1,5 @@
use rustfs_event_notifier::{
AdapterConfig, Bucket, Error as NotifierError, Event, Identity, Metadata, Name, NotificationConfig, Object, Source,
WebhookConfig,
AdapterConfig, Bucket, Error as NotifierError, Event, Identity, Metadata, Name, NotifierConfig, Object, Source, WebhookConfig,
};
use std::collections::HashMap;
use tokio::signal;
@@ -8,7 +7,7 @@ use tracing::Level;
use tracing_subscriber::FmtSubscriber;
async fn setup_notification_system() -> Result<(), NotifierError> {
let config = NotificationConfig {
let config = NotifierConfig {
store_path: "./deploy/logs/event_store".into(),
channel_capacity: 100,
adapters: vec![AdapterConfig::Webhook(WebhookConfig {
@@ -40,11 +39,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
// tracing_subscriber::fmt::init();
let subscriber = FmtSubscriber::builder()
.with_max_level(Level::DEBUG) // set to debug or lower level
.with_target(false) // simplify output
.with_max_level(Level::DEBUG) // set to debug or lower level
.with_target(false) // simplify output
.finish();
tracing::subscriber::set_global_default(subscriber)
.expect("failed to set up log subscriber");
tracing::subscriber::set_global_default(subscriber).expect("failed to set up log subscriber");
// set up notification system
if let Err(e) = setup_notification_system().await {
+25 -17
View File
@@ -1,21 +1,27 @@
use rustfs_event_notifier::create_adapters;
use rustfs_event_notifier::NotificationSystem;
use rustfs_event_notifier::{AdapterConfig, NotificationConfig, WebhookConfig};
use rustfs_event_notifier::NotifierSystem;
use rustfs_event_notifier::{AdapterConfig, NotifierConfig, WebhookConfig};
use rustfs_event_notifier::{Bucket, Event, Identity, Metadata, Name, Object, Source};
use std::collections::HashMap;
use std::error;
use std::sync::Arc;
use tokio::signal;
use tracing::Level;
use tracing_subscriber::FmtSubscriber;
#[tokio::main]
async fn main() -> Result<(), Box<dyn error::Error>> {
tracing_subscriber::fmt::init();
let subscriber = FmtSubscriber::builder()
.with_max_level(Level::DEBUG) // set to debug or lower level
.with_target(false) // simplify output
.finish();
tracing::subscriber::set_global_default(subscriber).expect("failed to set up log subscriber");
let config = NotificationConfig {
let config = NotifierConfig {
store_path: "./events".to_string(),
channel_capacity: 100,
adapters: vec![AdapterConfig::Webhook(WebhookConfig {
endpoint: "http://localhost:8080/webhook".to_string(),
endpoint: "http://127.0.0.1:3000/webhook".to_string(),
auth_token: Some("secret-token".to_string()),
custom_headers: Some(HashMap::from([("X-Custom".to_string(), "value".to_string())])),
max_retries: 3,
@@ -23,16 +29,12 @@ async fn main() -> Result<(), Box<dyn error::Error>> {
})],
};
// loading configuration from specific env files
let _config = NotificationConfig::from_env_file(".env.example")?;
// load_config
// loading configuration from environment variables
let _config = NotifierConfig::load_config(Some("./crates/event-notifier/examples/event.toml".to_string()));
tracing::info!("load_config config: {:?} \n", _config);
// loading from a specific file
let _config = NotificationConfig::from_file("event.toml")?;
// Automatically load from multiple sources (Priority: Environment Variables > YAML > TOML)
let _config = NotificationConfig::load()?;
let system = Arc::new(tokio::sync::Mutex::new(NotificationSystem::new(config.clone()).await?));
let system = Arc::new(tokio::sync::Mutex::new(NotifierSystem::new(config.clone()).await?));
let adapters = create_adapters(&config.adapters)?;
// create an s3 metadata object
@@ -90,9 +92,15 @@ async fn main() -> Result<(), Box<dyn error::Error>> {
signal::ctrl_c().await?;
tracing::info!("Received shutdown signal");
{
let system = system.lock().await;
system.shutdown();
let result = {
let mut system = system.lock().await;
system.shutdown().await
};
if let Err(e) = result {
tracing::error!("Failed to shut down the notification system: {}", e);
} else {
tracing::info!("Notification system shut down successfully");
}
system_handle.await??;
+66 -1
View File
@@ -12,6 +12,71 @@ async fn main() {
}
async fn receive_webhook(Json(payload): Json<Value>) -> StatusCode {
println!("收到 webhook 请求 time: {},内容:{}", SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs().to_string(), serde_json::to_string_pretty(&payload).unwrap());
let start = SystemTime::now();
let since_the_epoch = start.duration_since(UNIX_EPOCH).expect("Time went backwards");
// get the number of seconds since the unix era
let seconds = since_the_epoch.as_secs();
// Manually calculate year, month, day, hour, minute, and second
let (year, month, day, hour, minute, second) = convert_seconds_to_date(seconds);
// output result
println!("current time:{:04}-{:02}-{:02} {:02}:{:02}:{:02}", year, month, day, hour, minute, second);
println!(
"received a webhook request time:{} content:\n {}",
seconds.to_string(),
serde_json::to_string_pretty(&payload).unwrap()
);
StatusCode::OK
}
fn convert_seconds_to_date(seconds: u64) -> (u32, u32, u32, u32, u32, u32) {
// assume that the time zone is utc
let seconds_per_minute = 60;
let seconds_per_hour = 3600;
let seconds_per_day = 86400;
// Calculate the year, month, day, hour, minute, and second corresponding to the number of seconds
let mut total_seconds = seconds;
let mut year = 1970;
let mut month = 1;
let mut day = 1;
let mut hour = 0;
let mut minute = 0;
let mut second = 0;
// calculate year
while total_seconds >= 31536000 {
year += 1;
total_seconds -= 31536000; // simplified processing no leap year considered
}
// calculate month
let days_in_month = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
for m in 0..12 {
if total_seconds >= days_in_month[m] * seconds_per_day {
month += 1;
total_seconds -= days_in_month[m] * seconds_per_day;
} else {
break;
}
}
// calculate the number of days
day += total_seconds / seconds_per_day;
total_seconds %= seconds_per_day;
// calculate hours
hour += total_seconds / seconds_per_hour;
total_seconds %= seconds_per_hour;
// calculate minutes
minute += total_seconds / seconds_per_minute;
total_seconds %= seconds_per_minute;
// calculate the number of seconds
second += total_seconds;
(year as u32, month as u32, day as u32, hour as u32, minute as u32, second as u32)
}
+8 -4
View File
@@ -80,16 +80,20 @@ pub async fn event_bus(
} else {
tracing::info!("no unhandled events need to be saved");
}
tracing::info!("shutdown_complete is Some: {}", shutdown_complete.is_some());
// send a completion signal
tracing::debug!("shutdown_complete is Some: {}", shutdown_complete.is_some());
if let Some(complete_sender) = shutdown_complete {
let _ = complete_sender.send(());
// send a completion signal
let result = complete_sender.send(());
match result {
Ok(_) => tracing::info!("Event bus shutdown signal sent"),
Err(e) => tracing::error!("Failed to send event bus shutdown signal: {:?}", e),
}
tracing::info!("Shutting down event bus");
}
tracing::info!("Event bus shutdown complete");
break;
}
// else => break,
}
}
Ok(())
+76 -34
View File
@@ -1,7 +1,7 @@
use crate::Error;
use figment::providers::Format;
use config::{Config, Environment, File, FileFormat};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::env;
/// Configuration for the notification system.
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -65,7 +65,7 @@ pub enum AdapterConfig {
/// Configuration for the notification system.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NotificationConfig {
pub struct NotifierConfig {
#[serde(default = "default_store_path")]
pub store_path: String,
#[serde(default = "default_channel_capacity")]
@@ -73,7 +73,7 @@ pub struct NotificationConfig {
pub adapters: Vec<AdapterConfig>,
}
impl Default for NotificationConfig {
impl Default for NotifierConfig {
fn default() -> Self {
Self {
store_path: default_store_path(),
@@ -83,46 +83,88 @@ impl Default for NotificationConfig {
}
}
impl NotificationConfig {
impl NotifierConfig {
/// create a new configuration with default values
pub fn new() -> Self {
Self::default()
}
/// create a configuration from a configuration file
pub fn from_file(path: &str) -> Result<Self, Error> {
let config = figment::Figment::new()
.merge(figment::providers::Toml::file(path))
.extract()?;
/// Loading the configuration file
/// Supports TOML, YAML and .env formats, read in order by priority
///
/// # Parameters
/// - `config_dir`: Configuration file path
///
/// # Returns
/// Configuration information
///
/// # Example
/// ```
/// use rustfs_event_notifier::NotifierConfig;
///
/// let config = NotifierConfig::load_config(None);
/// ```
pub fn load_config(config_dir: Option<String>) -> NotifierConfig {
let config_dir = if let Some(path) = config_dir {
// If a path is provided, check if it's empty
if path.is_empty() {
// If empty, use the default config file name
DEFAULT_CONFIG_FILE.to_string()
} else {
// Use the provided path
let path = std::path::Path::new(&path);
if path.extension().is_some() {
// If path has extension, use it as is (extension will be added by Config::builder)
path.with_extension("").to_string_lossy().into_owned()
} else {
// If path is a directory, append the default config file name
path.to_string_lossy().into_owned()
}
}
} else {
// If no path provided, use current directory + default config file
match env::current_dir() {
Ok(dir) => dir.join(DEFAULT_CONFIG_FILE).to_string_lossy().into_owned(),
Err(_) => {
eprintln!("Warning: Failed to get current directory, using default config file");
DEFAULT_CONFIG_FILE.to_string()
}
}
};
Ok(config)
}
// Log using proper logging instead of println when possible
println!("Using config file base: {}", config_dir);
/// Read configuration from multiple sources (support TOML, YAML, .env)
pub fn load() -> Result<Self, Error> {
let figment = figment::Figment::new()
// First try to read the config.toml of the current directory
.merge(figment::providers::Toml::file("event.toml"))
// Then try to read the config.yaml of the current directory
.merge(figment::providers::Yaml::file("event.yaml"))
// Finally read the environment variable and overwrite the previous value
.merge(figment::providers::Env::prefixed("EVENT_NOTIF_"));
Ok(figment.extract()?)
}
/// loading configuration from env file
pub fn from_env_file(path: &str) -> Result<Self, Error> {
// loading env files
dotenvy::from_path(path).map_err(|e| Error::ConfigError(format!("unable to load env file: {}", e)))?;
// Extract configuration from environment variables using figurement
let figment = figment::Figment::new().merge(figment::providers::Env::prefixed("EVENT_NOTIF_"));
Ok(figment.extract()?)
let app_config = Config::builder()
.add_source(File::with_name(config_dir.as_str()).format(FileFormat::Toml).required(false))
.add_source(File::with_name(config_dir.as_str()).format(FileFormat::Yaml).required(false))
.add_source(
Environment::default()
.prefix("NOTIFIER")
.prefix_separator("__")
.separator("__")
.list_separator("_")
.with_list_parse_key("adapters")
.try_parsing(true),
)
.build()
.unwrap_or_default();
println!("Loaded config: {:?}", app_config);
match app_config.try_deserialize::<NotifierConfig>() {
Ok(app_config) => {
println!("Parsed AppConfig: {:?} \n", app_config);
app_config
}
Err(e) => {
println!("Failed to deserialize config: {}", e);
NotifierConfig::default()
}
}
}
}
const DEFAULT_CONFIG_FILE: &str = "obs";
/// Provide temporary directories as default storage paths
fn default_store_path() -> String {
std::env::temp_dir().join("event-notification").to_string_lossy().to_string()
+2 -1
View File
@@ -1,3 +1,4 @@
use config::ConfigError;
use thiserror::Error;
use tokio::sync::mpsc::error;
use tokio::task::JoinError;
@@ -35,7 +36,7 @@ pub enum Error {
#[error("Configuration error: {0}")]
ConfigError(String),
#[error("Configuration loading error: {0}")]
Figment(#[from] figment::Error),
Config(#[from] ConfigError),
}
impl Error {
+28 -23
View File
@@ -1,8 +1,8 @@
use crate::{create_adapters, Error, Event, NotificationConfig, NotificationSystem};
use crate::{create_adapters, Error, Event, NotifierConfig, NotifierSystem};
use std::sync::{atomic, Arc};
use tokio::sync::{Mutex, OnceCell};
static GLOBAL_SYSTEM: OnceCell<Arc<Mutex<NotificationSystem>>> = OnceCell::const_new();
static GLOBAL_SYSTEM: OnceCell<Arc<Mutex<NotifierSystem>>> = OnceCell::const_new();
static INITIALIZED: atomic::AtomicBool = atomic::AtomicBool::new(false);
static READY: atomic::AtomicBool = atomic::AtomicBool::new(false);
static INIT_LOCK: Mutex<()> = Mutex::const_new(());
@@ -24,7 +24,7 @@ static INIT_LOCK: Mutex<()> = Mutex::const_new(());
/// - Creating adapters fails.
/// - Starting the notification system fails.
/// - Setting the global system instance fails.
pub async fn initialize(config: NotificationConfig) -> Result<(), Error> {
pub async fn initialize(config: NotifierConfig) -> Result<(), Error> {
let _lock = INIT_LOCK.lock().await;
// Check if the system is already initialized.
@@ -52,7 +52,7 @@ pub async fn initialize(config: NotificationConfig) -> Result<(), Error> {
// Attempt to initialize, and reset the INITIALIZED flag if it fails.
let result: Result<(), Error> = async {
let system = NotificationSystem::new(config.clone()).await.map_err(|e| {
let system = NotifierSystem::new(config.clone()).await.map_err(|e| {
tracing::error!("Failed to create NotificationSystem: {:?}", e);
e
})?;
@@ -127,24 +127,29 @@ pub async fn send_event(event: Event) -> Result<(), Error> {
pub async fn shutdown() -> Result<(), Error> {
if let Some(system) = GLOBAL_SYSTEM.get() {
tracing::info!("Shutting down notification system start");
let (complete_tx, complete_rx) = tokio::sync::oneshot::channel();
{
let result = {
let mut system_guard = system.lock().await;
// set the complete channel and trigger cancellation
system_guard.set_shutdown_complete_channel(complete_tx);
system_guard.shutdown();
tracing::info!("Notification system shutdown triggered");
system_guard.shutdown().await
};
if let Err(e) = &result {
tracing::error!("Notification system shutdown failed: {}", e);
} else {
tracing::info!("Event bus shutdown completed");
}
// wait for the cleaning to be completed
let _ = complete_rx.await;
tracing::info!("Event bus shutdown completed");
tracing::info!(
"Shutdown method called set static value start, READY: {}, INITIALIZED: {}",
READY.load(atomic::Ordering::SeqCst),
INITIALIZED.load(atomic::Ordering::SeqCst)
);
READY.store(false, atomic::Ordering::SeqCst);
INITIALIZED.store(false, atomic::Ordering::SeqCst);
tracing::info!("Notification system is ready to process events");
Ok(())
tracing::info!(
"Shutdown method called set static value end, READY: {}, INITIALIZED: {}",
READY.load(atomic::Ordering::SeqCst),
INITIALIZED.load(atomic::Ordering::SeqCst)
);
result
} else {
Err(Error::custom("Notification system not initialized"))
}
@@ -155,7 +160,7 @@ pub async fn shutdown() -> Result<(), Error> {
/// # Errors
///
/// Returns an error if the system is not initialized.
async fn get_system() -> Result<Arc<Mutex<NotificationSystem>>, Error> {
async fn get_system() -> Result<Arc<Mutex<NotifierSystem>>, Error> {
GLOBAL_SYSTEM
.get()
.cloned()
@@ -165,13 +170,13 @@ async fn get_system() -> Result<Arc<Mutex<NotificationSystem>>, Error> {
#[cfg(test)]
mod tests {
use super::*;
use crate::{AdapterConfig, NotificationConfig, WebhookConfig};
use crate::{AdapterConfig, NotifierConfig, WebhookConfig};
use std::collections::HashMap;
#[tokio::test]
async fn test_initialize_success() {
tracing_subscriber::fmt::init();
let config = NotificationConfig::default(); // assume there is a default configuration
let config = NotifierConfig::default(); // assume there is a default configuration
let result = initialize(config).await;
assert!(!result.is_ok(), "Initialization should succeed");
assert!(!is_initialized(), "System should be marked as initialized");
@@ -181,7 +186,7 @@ mod tests {
#[tokio::test]
async fn test_initialize_twice() {
tracing_subscriber::fmt::init();
let config = NotificationConfig::default();
let config = NotifierConfig::default();
let _ = initialize(config.clone()).await; // first initialization
let result = initialize(config).await; // second initialization
assert!(!result.is_ok(), "Initialization should succeed");
@@ -192,7 +197,7 @@ mod tests {
async fn test_initialize_failure_resets_state() {
tracing_subscriber::fmt::init();
// simulate wrong configuration
let config = NotificationConfig {
let config = NotifierConfig {
adapters: vec![
// assuming that the empty adapter will cause failure
AdapterConfig::Webhook(WebhookConfig {
@@ -217,7 +222,7 @@ mod tests {
assert!(!is_initialized(), "System should not be initialized initially");
assert!(!is_ready(), "System should not be ready initially");
let config = NotificationConfig::default();
let config = NotifierConfig::default();
let _ = initialize(config).await;
assert!(!is_initialized(), "System should be initialized after successful initialization");
assert!(!is_ready(), "System should be ready after successful initialization");
+2 -2
View File
@@ -22,10 +22,10 @@ pub use config::KafkaConfig;
pub use config::MqttConfig;
#[cfg(feature = "webhook")]
pub use config::WebhookConfig;
pub use config::{AdapterConfig, NotificationConfig};
pub use config::{AdapterConfig, NotifierConfig};
pub use error::Error;
pub use event::{Bucket, Event, EventBuilder, Identity, Log, Metadata, Name, Object, Source};
pub use global::{initialize, is_initialized, is_ready, send_event, shutdown};
pub use notifier::NotificationSystem;
pub use notifier::NotifierSystem;
pub use store::EventStore;
+55 -16
View File
@@ -1,4 +1,4 @@
use crate::{event_bus, ChannelAdapter, Error, Event, EventStore, NotificationConfig};
use crate::{event_bus, ChannelAdapter, Error, Event, EventStore, NotifierConfig};
use std::sync::Arc;
use tokio::sync::mpsc;
use tokio_util::sync::CancellationToken;
@@ -7,17 +7,18 @@ use tokio_util::sync::CancellationToken;
/// It manages the event bus and the adapters.
/// It is responsible for sending and receiving events.
/// It also handles the shutdown process.
pub struct NotificationSystem {
pub struct NotifierSystem {
tx: mpsc::Sender<Event>,
rx: Option<mpsc::Receiver<Event>>,
store: Arc<EventStore>,
shutdown: CancellationToken,
shutdown_complete: Option<tokio::sync::oneshot::Sender<()>>,
shutdown_receiver: Option<tokio::sync::oneshot::Receiver<()>>,
}
impl NotificationSystem {
impl NotifierSystem {
/// Creates a new `NotificationSystem` instance.
pub async fn new(config: NotificationConfig) -> Result<Self, Error> {
pub async fn new(config: NotifierConfig) -> Result<Self, Error> {
let (tx, rx) = mpsc::channel::<Event>(config.channel_capacity);
let store = Arc::new(EventStore::new(&config.store_path).await?);
let shutdown = CancellationToken::new();
@@ -29,13 +30,15 @@ impl NotificationSystem {
tx.send(event).await.map_err(|e| Error::ChannelSend(Box::new(e)))?;
}
}
// Initialize shutdown_complete to Some(tx)
let (complete_tx, complete_rx) = tokio::sync::oneshot::channel();
Ok(Self {
tx,
rx: Some(rx),
store,
shutdown,
shutdown_complete: None,
shutdown_complete: Some(complete_tx),
shutdown_receiver: Some(complete_rx),
})
}
@@ -43,37 +46,65 @@ impl NotificationSystem {
/// It initializes the event bus and the producer.
pub async fn start(&mut self, adapters: Vec<Arc<dyn ChannelAdapter>>) -> Result<(), Error> {
if self.shutdown.is_cancelled() {
return Err(Error::custom("System is shutting down"));
let error = Error::custom("System is shutting down");
self.handle_error("start", &error);
return Err(error);
}
self.log(tracing::Level::INFO, "start", "Starting the notification system");
let rx = self.rx.take().ok_or_else(|| Error::EventBusStarted)?;
let shutdown_clone = self.shutdown.clone();
let store_clone = self.store.clone();
let shutdown_complete = self.shutdown_complete.take();
tokio::spawn(async move {
if let Err(e) = event_bus(rx, adapters, store_clone, shutdown_clone, shutdown_complete).await {
tracing::error!("Event bus failed: {}", e);
}
});
self.log(tracing::Level::INFO, "start", "Notification system started successfully");
Ok(())
}
/// Sends an event to the notification system.
/// This method is used to send events to the event bus.
pub async fn send_event(&self, event: Event) -> Result<(), Error> {
self.log(tracing::Level::DEBUG, "send_event", &format!("Sending event: {:?}", event));
if self.shutdown.is_cancelled() {
return Err(Error::custom("System is shutting down"));
let error = Error::custom("System is shutting down");
self.handle_error("send_event", &error);
return Err(error);
}
self.tx.send(event).await.map_err(|e| Error::ChannelSend(Box::new(e)))?;
if let Err(e) = self.tx.send(event).await {
let error = Error::ChannelSend(Box::new(e));
self.handle_error("send_event", &error);
return Err(error);
}
self.log(tracing::Level::INFO, "send_event", "Event sent successfully");
Ok(())
}
/// Shuts down the notification system.
/// This method is used to cancel the event bus and producer tasks.
pub fn shutdown(&self) {
pub async fn shutdown(&mut self) -> Result<(), Error> {
tracing::info!("Shutting down the notification system");
self.shutdown.cancel();
// wait for the event bus to be completely closed
if let Some(receiver) = self.shutdown_receiver.take() {
match receiver.await {
Ok(_) => {
tracing::info!("Event bus shutdown completed successfully");
Ok(())
}
Err(e) => {
let error = Error::custom(format!("Failed to receive shutdown completion: {}", e).as_str());
self.handle_error("shutdown", &error);
Err(error)
}
}
} else {
tracing::warn!("Shutdown receiver not available, the event bus might still be running");
Err(Error::custom("Shutdown receiver not available"))
}
}
/// shutdown state
@@ -81,9 +112,17 @@ impl NotificationSystem {
self.shutdown.is_cancelled()
}
pub fn set_shutdown_complete_channel(&mut self, tx: tokio::sync::oneshot::Sender<()>) {
// storage completion channel for use by event bus
tracing::info!("Shutting down the notification system set shutdown complete channel");
self.shutdown_complete = Some(tx);
fn handle_error(&self, context: &str, error: &Error) {
self.log(tracing::Level::ERROR, context, &format!("{:?}", error));
// TODO Can be extended to record to files or send to monitoring systems
}
fn log(&self, level: tracing::Level, context: &str, message: &str) {
match level {
tracing::Level::ERROR => tracing::error!("[{}] {}", context, message),
tracing::Level::WARN => tracing::warn!("[{}] {}", context, message),
tracing::Level::INFO => tracing::info!("[{}] {}", context, message),
tracing::Level::DEBUG => tracing::debug!("[{}] {}", context, message),
tracing::Level::TRACE => tracing::trace!("[{}] {}", context, message),
}
}
}
+5 -5
View File
@@ -1,4 +1,4 @@
use rustfs_event_notifier::{AdapterConfig, NotificationSystem, WebhookConfig};
use rustfs_event_notifier::{AdapterConfig, NotifierSystem, WebhookConfig};
use rustfs_event_notifier::{Bucket, Event, EventBuilder, Identity, Metadata, Name, Object, Source};
use rustfs_event_notifier::{ChannelAdapter, WebhookAdapter};
use std::collections::HashMap;
@@ -67,7 +67,7 @@ async fn test_webhook_adapter() {
#[tokio::test]
async fn test_notification_system() {
let config = rustfs_event_notifier::NotificationConfig {
let config = rustfs_event_notifier::NotifierConfig {
store_path: "./test_events".to_string(),
channel_capacity: 100,
adapters: vec![AdapterConfig::Webhook(WebhookConfig {
@@ -78,7 +78,7 @@ async fn test_notification_system() {
timeout: 5,
})],
};
let system = Arc::new(tokio::sync::Mutex::new(NotificationSystem::new(config.clone()).await.unwrap()));
let system = Arc::new(tokio::sync::Mutex::new(NotifierSystem::new(config.clone()).await.unwrap()));
let adapters: Vec<Arc<dyn ChannelAdapter>> = vec![Arc::new(WebhookAdapter::new(WebhookConfig {
endpoint: "http://localhost:8080/webhook".to_string(),
auth_token: None,
@@ -148,8 +148,8 @@ async fn test_notification_system() {
// create a new task to handle the timeout
let system = Arc::clone(&system);
tokio::spawn(async move {
if let Ok(guard) = system.try_lock() {
guard.shutdown();
if let Ok(mut guard) = system.try_lock() {
guard.shutdown().await.unwrap();
}
});
// give the system some time to clean up resources
+8 -12
View File
@@ -122,7 +122,7 @@ impl LocalDisk {
let root = fs::canonicalize(ep.get_file_path()).await?;
if cleanup {
// TODO: 删除tmp数据
// TODO: 删除 tmp 数据
}
let format_path = Path::new(super::RUSTFS_META_BUCKET)
@@ -631,13 +631,13 @@ impl LocalDisk {
}
}
// 没有版本了,删除xl.meta
// 没有版本了,删除 xl.meta
if fm.versions.is_empty() {
self.delete_file(&volume_dir, &xlpath, true, false).await?;
return Ok(());
}
// 更新xl.meta
// 更新 xl.meta
let buf = fm.marshal_msg()?;
let volume_dir = self.get_bucket_path(volume)?;
@@ -875,7 +875,7 @@ impl LocalDisk {
.read_metadata(self.get_object_path(bucket, format!("{}/{}", &current, &entry).as_str())?)
.await?;
// 用strip_suffix只删除一次
// 用 strip_suffix 只删除一次
let entry = entry.strip_suffix(STORAGE_FORMAT_FILE).unwrap_or_default().to_owned();
let name = entry.trim_end_matches(SLASH_SEPARATOR);
let name = decode_dir_object(format!("{}/{}", &current, &name).as_str());
@@ -1723,7 +1723,7 @@ impl DiskAPI for LocalDisk {
}
}
// xl.meta路径
// xl.meta 路径
let src_file_path = src_volume_dir.join(Path::new(format!("{}/{}", &src_path, super::STORAGE_FORMAT_FILE).as_str()));
let dst_file_path = dst_volume_dir.join(Path::new(format!("{}/{}", &dst_path, super::STORAGE_FORMAT_FILE).as_str()));
@@ -1754,7 +1754,7 @@ impl DiskAPI for LocalDisk {
check_path_length(src_file_path.to_string_lossy().to_string().as_str())?;
check_path_length(dst_file_path.to_string_lossy().to_string().as_str())?;
// 读旧xl.meta
// 读旧 xl.meta
let has_dst_buf = match utils::fs::read_file(&dst_file_path).await {
Ok(res) => Some(res),
@@ -2268,7 +2268,7 @@ impl DiskAPI for LocalDisk {
async fn delete_volume(&self, volume: &str) -> Result<()> {
let p = self.get_bucket_path(volume)?;
// TODO: 不能用递归删除,如果目录下面有文件,返回errVolumeNotEmpty
// TODO: 不能用递归删除,如果目录下面有文件,返回 errVolumeNotEmpty
if let Err(err) = fs::remove_dir_all(&p).await {
match err.kind() {
@@ -2328,10 +2328,7 @@ impl DiskAPI for LocalDisk {
}
}
let vcfg = match BucketVersioningSys::get(&cache.info.name).await {
Ok(vcfg) => Some(vcfg),
Err(_) => None,
};
let vcfg = (BucketVersioningSys::get(&cache.info.name).await).ok();
let loc = self.get_disk_location();
let disks = store.get_disks(loc.pool_idx.unwrap(), loc.disk_idx.unwrap()).await?;
@@ -2491,7 +2488,6 @@ async fn get_disk_info(drive_path: PathBuf) -> Result<(Info, bool)> {
#[cfg(test)]
mod test {
use super::*;
#[tokio::test]
+1 -1
View File
@@ -1101,7 +1101,7 @@ pub fn lc_has_active_rules(config: &BucketLifecycleConfiguration, prefix: &str)
return true;
}
if let Some(Some(true)) = rule.expiration.as_ref().map(|e| e.expired_object_delete_marker.map(|m| m)) {
if let Some(Some(true)) = rule.expiration.as_ref().map(|e| e.expired_object_delete_marker) {
return true;
}