feat(baker/finalize): notification system and consolidate status types
- Rename types/build_status.rs to types/buildstatus.rs - Rename StageInfo.sub_stage field to substage for consistency - Move notification logic from finalize to dedicated notify module - Remove notification module from finalize (moved to notify) - Add notify module with event queuing, retry, and priority scheduling - Add exponential backoff for temporary notification failures - Implement plugin filtering by name list during registration - Add fetch_plugins helper for batch plugin download - Update finalize plugin registration to accept optional plugin filter - Fix finalize config template indentation for notification policy - Move all YAML templates to templates/ directory
This commit is contained in:
@@ -156,11 +156,16 @@ notification:
|
|||||||
sms-notify:
|
sms-notify:
|
||||||
to: "+8610012345678"
|
to: "+8610012345678"
|
||||||
schema: "custom"
|
schema: "custom"
|
||||||
on_success: "[HBW Notify] Build Succeeded: {{ build.status }} ({{ build.duration }})"
|
policy:
|
||||||
on_failure: "[HBW Notify] Build Failed: {{ build.status }} ({{ build.duration }})"
|
- on_success: "[HBW Notify] Build Succeeded: {{ build.status }} ({{ build.duration }})"
|
||||||
on_finish_of:
|
to: "+8610012345678"
|
||||||
|
- on_failure: "[HBW Notify] Build Failed: {{ build.status }} ({{ build.duration }})"
|
||||||
|
to: "+8610012345679"
|
||||||
|
|
||||||
|
- on_finish_of:
|
||||||
stage: "bake.build"
|
stage: "bake.build"
|
||||||
content: "[HBW Notify] Build Progress: {{ build.status }} ({{ build.duration }})"
|
content: "[HBW Notify] Build Progress: {{ build.status }} ({{ build.duration }})"
|
||||||
|
to: "+8610012345677"
|
||||||
fallible: true
|
fallible: true
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
@@ -7,7 +7,7 @@ use crate::ExecutionContext;
|
|||||||
use crate::prebake::PrebakeConfig;
|
use crate::prebake::PrebakeConfig;
|
||||||
use crate::prebake::security::get_drop_after;
|
use crate::prebake::security::get_drop_after;
|
||||||
use crate::prebake::stage::PrebakeStage;
|
use crate::prebake::stage::PrebakeStage;
|
||||||
use crate::types::build_status::{write_stage_status, StageInfo, StagePhase, StageResult};
|
use crate::types::buildstatus::{write_stage_status, StageInfo, StagePhase, StageResult};
|
||||||
use crate::{Engine, prebake};
|
use crate::{Engine, prebake};
|
||||||
use std::path::{PathBuf, Path};
|
use std::path::{PathBuf, Path};
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
@@ -71,7 +71,7 @@ pub async fn bake(
|
|||||||
StageInfo {
|
StageInfo {
|
||||||
name: "trivial".to_string(),
|
name: "trivial".to_string(),
|
||||||
phase: StagePhase::Bake,
|
phase: StagePhase::Bake,
|
||||||
sub_stage: "trivial".to_string(),
|
substage: "trivial".to_string(),
|
||||||
result: stage_result,
|
result: stage_result,
|
||||||
duration_ms: Utc::now().signed_duration_since(stage_start).num_milliseconds() as u64,
|
duration_ms: Utc::now().signed_duration_since(stage_start).num_milliseconds() as u64,
|
||||||
started_at: stage_start,
|
started_at: stage_start,
|
||||||
@@ -101,7 +101,7 @@ pub async fn bake(
|
|||||||
StageInfo {
|
StageInfo {
|
||||||
name: func.name.clone(),
|
name: func.name.clone(),
|
||||||
phase: StagePhase::Bake,
|
phase: StagePhase::Bake,
|
||||||
sub_stage: func.name.clone(),
|
substage: func.name.clone(),
|
||||||
result: stage_result,
|
result: stage_result,
|
||||||
duration_ms: Utc::now().signed_duration_since(stage_start).num_milliseconds() as u64,
|
duration_ms: Utc::now().signed_duration_since(stage_start).num_milliseconds() as u64,
|
||||||
started_at: stage_start,
|
started_at: stage_start,
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod event;
|
pub mod event;
|
||||||
pub mod notification;
|
|
||||||
pub mod plugin;
|
pub mod plugin;
|
||||||
pub mod stage;
|
pub mod stage;
|
||||||
pub mod template;
|
pub mod template;
|
||||||
@@ -12,35 +11,15 @@ use std::path::PathBuf;
|
|||||||
use crate::finalize::plugin::fetch::{self, FetchArgument};
|
use crate::finalize::plugin::fetch::{self, FetchArgument};
|
||||||
use crate::cli::Cli;
|
use crate::cli::Cli;
|
||||||
use crate::engine::EventSender;
|
use crate::engine::EventSender;
|
||||||
use crate::types::build_status::{read_build_status, BuildStatus, StageResult};
|
use crate::types::buildstatus::{read_build_status, BuildStatus, StageResult};
|
||||||
use crate::types::build_status::{StageInfo, StagePhase};
|
use crate::types::buildstatus::{StageInfo, StagePhase};
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
// use crate::finalize::error::PluginError;
|
// use crate::finalize::error::PluginError;
|
||||||
pub use config::*;
|
pub use config::*;
|
||||||
pub use error::FinalizeError;
|
pub use error::FinalizeError;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
fn _extract_notification_plugins(
|
pub fn parse(path: &Path) -> Result<FinalizeConfig, FinalizeError> {
|
||||||
all_plugins: &plugin::PluginMap,
|
|
||||||
_notification_config: &config::NotificationConfig,
|
|
||||||
) -> Vec<String> {
|
|
||||||
use std::collections::HashSet;
|
|
||||||
|
|
||||||
let notification_names: HashSet<&str> = HashSet::from([
|
|
||||||
"mail",
|
|
||||||
"webhook",
|
|
||||||
"satori",
|
|
||||||
"in-site-notify",
|
|
||||||
]);
|
|
||||||
|
|
||||||
all_plugins
|
|
||||||
.iter()
|
|
||||||
.filter(|(name, _)| notification_names.contains(name.as_str()))
|
|
||||||
.map(|(name, _)| name.clone())
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse(path: &Path) -> Result<FinalizeConfig, FinalizeError> {
|
|
||||||
let content = std::fs::read_to_string(path).map_err(FinalizeError::IoError)?;
|
let content = std::fs::read_to_string(path).map_err(FinalizeError::IoError)?;
|
||||||
let config: FinalizeConfig = serde_yaml::from_str(&content)?;
|
let config: FinalizeConfig = serde_yaml::from_str(&content)?;
|
||||||
Ok(config)
|
Ok(config)
|
||||||
@@ -86,7 +65,7 @@ pub async fn finalize(
|
|||||||
|
|
||||||
// Register Plugins
|
// Register Plugins
|
||||||
log::info!("Registering {} plugins", finalize.plugin.len());
|
log::info!("Registering {} plugins", finalize.plugin.len());
|
||||||
let registered_plugins = plugin::register(finalize.plugin)?;
|
let registered_plugins = plugin::register(finalize.plugin, None)?;
|
||||||
|
|
||||||
// Early hook
|
// Early hook
|
||||||
let earlyhook_result = stage::hook::hook(
|
let earlyhook_result = stage::hook::hook(
|
||||||
@@ -106,43 +85,6 @@ pub async fn finalize(
|
|||||||
// If prior stages failed, we still send notification but skip artifacts
|
// If prior stages failed, we still send notification but skip artifacts
|
||||||
let prior_failed = build_status.has_failures() || earlyhook_result.is_err();
|
let prior_failed = build_status.has_failures() || earlyhook_result.is_err();
|
||||||
|
|
||||||
// Send notification (before late hook, as per standalone flow)
|
|
||||||
if !finalize.notification.groups.is_empty() {
|
|
||||||
let core = notification::NotificationCore::new(registered_plugins.clone());
|
|
||||||
let request = notification::NotificationRequest {
|
|
||||||
pipeline_name: ctx.pipeline_name.clone(),
|
|
||||||
build_id: ctx.task_id.clone(),
|
|
||||||
build_status: build_status.clone(),
|
|
||||||
final_result: final_result.clone(),
|
|
||||||
config: finalize.notification,
|
|
||||||
};
|
|
||||||
if let Err(e) = core.dispatch(request, ctx, event_tx.clone()).await {
|
|
||||||
log::warn!("Notification dispatch failed: {}", e);
|
|
||||||
// Notification failure does not block finalize
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log::debug!("No notification groups configured, skipping notification");
|
|
||||||
}
|
|
||||||
|
|
||||||
if prior_failed {
|
|
||||||
log::warn!("Prior stages failed, skipping artifacts and late hook");
|
|
||||||
// Write finalize stage status as failure
|
|
||||||
let _ = crate::types::build_status::write_stage_status(
|
|
||||||
STANDALONE_STATUS_DIR,
|
|
||||||
StageInfo {
|
|
||||||
name: "finalize".to_string(),
|
|
||||||
phase: StagePhase::Finalize,
|
|
||||||
sub_stage: "notify".to_string(),
|
|
||||||
result: StageResult::Failure,
|
|
||||||
duration_ms: 0,
|
|
||||||
started_at: Utc::now(),
|
|
||||||
finished_at: Utc::now(),
|
|
||||||
error_message: Some("Prior stage failed".to_string()),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prepare artifacts
|
// Prepare artifacts
|
||||||
// TODO: artifact stage implementation
|
// TODO: artifact stage implementation
|
||||||
|
|
||||||
@@ -156,12 +98,12 @@ pub async fn finalize(
|
|||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// Write finalize success status
|
// Write finalize success status
|
||||||
let _ = crate::types::build_status::write_stage_status(
|
let _ = crate::types::buildstatus::write_stage_status(
|
||||||
STANDALONE_STATUS_DIR,
|
STANDALONE_STATUS_DIR,
|
||||||
StageInfo {
|
StageInfo {
|
||||||
name: "finalize".to_string(),
|
name: "finalize".to_string(),
|
||||||
phase: StagePhase::Finalize,
|
phase: StagePhase::Finalize,
|
||||||
sub_stage: "clean".to_string(),
|
substage: "clean".to_string(),
|
||||||
result: StageResult::Success,
|
result: StageResult::Success,
|
||||||
duration_ms: 0,
|
duration_ms: 0,
|
||||||
started_at: Utc::now(),
|
started_at: Utc::now(),
|
||||||
|
|||||||
@@ -3,16 +3,16 @@ use crate::finalize::RawPluginMap;
|
|||||||
use crate::finalize::plugin::metadata::{PluginMetadata, PluginType};
|
use crate::finalize::plugin::metadata::{PluginMetadata, PluginType};
|
||||||
use crate::{EventSender, ExecutionContext, finalize::error::PluginError};
|
use crate::{EventSender, ExecutionContext, finalize::error::PluginError};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use std::collections::HashMap;
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::path::{PathBuf, Path};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
mod dylib;
|
mod dylib;
|
||||||
|
pub mod fetch;
|
||||||
mod internal;
|
mod internal;
|
||||||
mod metadata;
|
mod metadata;
|
||||||
mod rhai;
|
mod rhai;
|
||||||
mod shell;
|
mod shell;
|
||||||
pub mod fetch;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct FinalizePlugin {
|
pub struct FinalizePlugin {
|
||||||
@@ -99,7 +99,7 @@ fn read_manifest(path: Option<PathBuf>, name: &str) -> Result<PluginMetadata, Pl
|
|||||||
for ext in FINALIZE_PLUGIN_MANIFEST_EXTENSION {
|
for ext in FINALIZE_PLUGIN_MANIFEST_EXTENSION {
|
||||||
match load_manifest(&path, name, ext) {
|
match load_manifest(&path, name, ext) {
|
||||||
Ok(content) => break 'load content,
|
Ok(content) => break 'load content,
|
||||||
Err(PluginError::ManifestNotFound(_)) => {},
|
Err(PluginError::ManifestNotFound(_)) => {}
|
||||||
Err(e) => return Err(e),
|
Err(e) => return Err(e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -107,9 +107,12 @@ fn read_manifest(path: Option<PathBuf>, name: &str) -> Result<PluginMetadata, Pl
|
|||||||
};
|
};
|
||||||
|
|
||||||
log::debug!("[plugin:{}] Parsing manifest content", name);
|
log::debug!("[plugin:{}] Parsing manifest content", name);
|
||||||
let mut metadata: PluginMetadata = serde_yaml::from_str(manifest_content.as_str()).map_err(|e| PluginError::PluginUnavailable {
|
let mut metadata: PluginMetadata =
|
||||||
|
serde_yaml::from_str(manifest_content.as_str()).map_err(|e| {
|
||||||
|
PluginError::PluginUnavailable {
|
||||||
name: name.to_string(),
|
name: name.to_string(),
|
||||||
reason: format!("Failed to parse plugin manifest: {}", e),
|
reason: format!("Failed to parse plugin manifest: {}", e),
|
||||||
|
}
|
||||||
})?;
|
})?;
|
||||||
metadata.fspath = path;
|
metadata.fspath = path;
|
||||||
Ok(metadata)
|
Ok(metadata)
|
||||||
@@ -117,16 +120,22 @@ fn read_manifest(path: Option<PathBuf>, name: &str) -> Result<PluginMetadata, Pl
|
|||||||
|
|
||||||
fn load_manifest(path: &Path, name: &str, suffix: &str) -> Result<String, PluginError> {
|
fn load_manifest(path: &Path, name: &str, suffix: &str) -> Result<String, PluginError> {
|
||||||
let manifest_path = path.join(format!("{}{}", FINALIZE_PLUGIN_MANIFEST, suffix));
|
let manifest_path = path.join(format!("{}{}", FINALIZE_PLUGIN_MANIFEST, suffix));
|
||||||
log::debug!("[plugin:{}] Reading manifest from {}", name, manifest_path.display());
|
log::debug!(
|
||||||
|
"[plugin:{}] Reading manifest from {}",
|
||||||
|
name,
|
||||||
|
manifest_path.display()
|
||||||
|
);
|
||||||
if !manifest_path.exists() {
|
if !manifest_path.exists() {
|
||||||
return Err(PluginError::ManifestNotFound(manifest_path));
|
return Err(PluginError::ManifestNotFound(manifest_path));
|
||||||
}
|
}
|
||||||
|
|
||||||
std::fs::read_to_string(&manifest_path).map_err(|e| {
|
std::fs::read_to_string(&manifest_path).map_err(|e| PluginError::PluginUnavailable {
|
||||||
PluginError::PluginUnavailable {
|
|
||||||
name: name.to_string(),
|
name: name.to_string(),
|
||||||
reason: format!("Failed to read plugin manifest from {}: {}", manifest_path.display(), e.to_string()),
|
reason: format!(
|
||||||
}
|
"Failed to read plugin manifest from {}: {}",
|
||||||
|
manifest_path.display(),
|
||||||
|
e.to_string()
|
||||||
|
),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,17 +211,38 @@ fn get_entry(plugin_type: PluginType, name: &str) -> Result<Box<dyn AsyncPluginF
|
|||||||
///
|
///
|
||||||
/// Returns a [`PluginMap`] containing all registered plugins, or a [`PluginError`] if
|
/// Returns a [`PluginMap`] containing all registered plugins, or a [`PluginError`] if
|
||||||
/// a plugin manifest cannot be read or parsed.
|
/// a plugin manifest cannot be read or parsed.
|
||||||
pub fn register(external_plugins: RawPluginMap) -> Result<PluginMap, PluginError> {
|
pub fn register(
|
||||||
|
external_plugins: RawPluginMap,
|
||||||
|
list: Option<&HashSet<String>>,
|
||||||
|
) -> Result<PluginMap, PluginError> {
|
||||||
let mut plugins: PluginMap = HashMap::new();
|
let mut plugins: PluginMap = HashMap::new();
|
||||||
|
let mut count = 0;
|
||||||
|
|
||||||
// Register internal plugin
|
// Register internal plugin
|
||||||
for i in internal::get_internal_plugin() {
|
for i in internal::get_internal_plugin() {
|
||||||
|
let name = i.name.to_string();
|
||||||
|
match list {
|
||||||
|
Some(list) if !list.contains(&name) => {
|
||||||
|
log::debug!("Plugin {} not in list, skipping", name);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
log::debug!("Registering internal plugin {}", i.name);
|
log::debug!("Registering internal plugin {}", i.name);
|
||||||
plugins.insert(i.name.to_string(), FinalizePlugin::new(i.entry));
|
plugins.insert(i.name.to_string(), FinalizePlugin::new(i.entry));
|
||||||
|
count += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register external plugin
|
// Register external plugin
|
||||||
for (name, data) in external_plugins.into_iter() {
|
for (name, data) in external_plugins.into_iter() {
|
||||||
|
match list {
|
||||||
|
Some(list) if !list.contains(&name) => {
|
||||||
|
log::debug!("Plugin {} not in list, skipping", name);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
log::debug!("Registering external plugin {}", name);
|
log::debug!("Registering external plugin {}", name);
|
||||||
let (_source, config) = data.into_iter().next().unwrap_or_else(|| {
|
let (_source, config) = data.into_iter().next().unwrap_or_else(|| {
|
||||||
panic!(
|
panic!(
|
||||||
@@ -234,8 +264,11 @@ pub fn register(external_plugins: RawPluginMap) -> Result<PluginMap, PluginError
|
|||||||
.with_metadata(metadata)
|
.with_metadata(metadata)
|
||||||
.with_argument(config.config),
|
.with_argument(config.config),
|
||||||
);
|
);
|
||||||
|
count += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log::info!("Registered {} plugins", count);
|
||||||
|
|
||||||
Ok(plugins)
|
Ok(plugins)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
// Code in this module PARTIALLY OR FULLY utilized AI Coding Agent.
|
// Code in this module PARTIALLY OR FULLY utilized AI Coding Agent.
|
||||||
use crate::finalize::plugin::PluginError;
|
use std::collections::HashSet;
|
||||||
|
use crate::finalize::fetch;
|
||||||
|
use crate::finalize::{
|
||||||
|
PluginConfig,
|
||||||
|
plugin::{PluginError, RawPluginMap},
|
||||||
|
};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
mod types;
|
|
||||||
mod checksum;
|
mod checksum;
|
||||||
mod extract;
|
mod extract;
|
||||||
mod http;
|
|
||||||
mod git;
|
mod git;
|
||||||
|
mod http;
|
||||||
|
mod types;
|
||||||
|
|
||||||
pub use types::FetchArgument;
|
pub use types::FetchArgument;
|
||||||
|
|
||||||
@@ -72,3 +77,35 @@ pub async fn fetch_plugin(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn fetch_plugins(
|
||||||
|
plugin_path: &Path,
|
||||||
|
plugin_config: &mut RawPluginMap,
|
||||||
|
list: Option<&HashSet<String>>,
|
||||||
|
) -> Result<(), PluginError> {
|
||||||
|
log::info!("Preparing to download {} plugins", plugin_config.len());
|
||||||
|
for (name, data) in plugin_config.iter_mut() {
|
||||||
|
match list {
|
||||||
|
Some(list) if !list.contains(name) => {
|
||||||
|
log::debug!("Plugin {} not in list, skipping", name);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
log::debug!("Downloading plugin {}", name);
|
||||||
|
let (source, config) = data.iter_mut().next().ok_or_else(|| {
|
||||||
|
PluginError::PluginUnavailable {
|
||||||
|
name: name.clone(),
|
||||||
|
reason: "no source configured".to_string(),
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
let fetch_argument = FetchArgument {
|
||||||
|
checksum: config.checksum.clone(),
|
||||||
|
shallow: config.shallow,
|
||||||
|
};
|
||||||
|
fetch::fetch_plugin(&source, &name, &plugin_path, fetch_argument).await?;
|
||||||
|
log::debug!("Storing plugin {} to {}", name, plugin_path.display());
|
||||||
|
config.runtime.fspath = Some(plugin_path.join(name));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ pub mod bake;
|
|||||||
pub mod socket;
|
pub mod socket;
|
||||||
pub mod types;
|
pub mod types;
|
||||||
pub mod constant;
|
pub mod constant;
|
||||||
|
pub mod notify;
|
||||||
|
|
||||||
// External crates re-exports
|
// External crates re-exports
|
||||||
pub use config;
|
pub use config;
|
||||||
|
|||||||
@@ -0,0 +1,203 @@
|
|||||||
|
use crate::constant::FINALIZE_STANDALONE_PLUGIN_PATH;
|
||||||
|
use crate::finalize::FinalizeConfig;
|
||||||
|
use crate::finalize::NotificationConfig;
|
||||||
|
use crate::finalize::parse;
|
||||||
|
use crate::finalize::plugin;
|
||||||
|
use crate::finalize::plugin::FinalizePlugin;
|
||||||
|
use crate::finalize::plugin::fetch;
|
||||||
|
use crate::types::buildstatus::StagePhase;
|
||||||
|
use crate::types::buildstatus::StageResult;
|
||||||
|
use crate::{EventSender, ExecutionContext};
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use serde::Serialize;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use std::hash::Hash;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
mod error;
|
||||||
|
use error::NotifyError;
|
||||||
|
|
||||||
|
fn validate(finalize: &FinalizeConfig) -> anyhow::Result<()> {
|
||||||
|
let validate_result = finalize.validate();
|
||||||
|
if let Err(errors) = validate_result {
|
||||||
|
for error in &errors {
|
||||||
|
log::error!("Error: {}", error);
|
||||||
|
}
|
||||||
|
anyhow::bail!(NotifyError::FinalizeValidationError(errors))
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type NotifyPluginMap = HashMap<String, FinalizePlugin>;
|
||||||
|
|
||||||
|
fn extract_plugin(finalize: &FinalizeConfig) -> HashSet<String> {
|
||||||
|
finalize
|
||||||
|
.notification
|
||||||
|
.groups
|
||||||
|
.iter()
|
||||||
|
.flat_map(|(_, methods)| methods.keys())
|
||||||
|
.cloned()
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
pub struct NotifyEvent {
|
||||||
|
id: Uuid,
|
||||||
|
stage: StagePhase,
|
||||||
|
substage: String,
|
||||||
|
result: StageResult,
|
||||||
|
not_before: DateTime<Utc>,
|
||||||
|
priority: u32,
|
||||||
|
effective_priority: u32,
|
||||||
|
lives: u32,
|
||||||
|
max_lives: u32,
|
||||||
|
target: HashSet<String>, // Target audience
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Hash for NotifyEvent {
|
||||||
|
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||||
|
self.id.hash(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for NotifyEvent {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.id == other.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for NotifyEvent {}
|
||||||
|
|
||||||
|
impl PartialOrd for NotifyEvent {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||||
|
Some(
|
||||||
|
self.effective_priority
|
||||||
|
.cmp(&other.effective_priority)
|
||||||
|
.then(self.not_before.cmp(&other.not_before))
|
||||||
|
.reverse(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ord for NotifyEvent {
|
||||||
|
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||||
|
self.partial_cmp(other).unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type BundledNotifyEvent = Vec<NotifyEvent>;
|
||||||
|
type NotifyEventReceiver = tokio::sync::mpsc::Receiver<BundledNotifyEvent>;
|
||||||
|
pub type NotifyEventSender = tokio::sync::mpsc::Sender<NotifyEvent>;
|
||||||
|
|
||||||
|
const NOTIFY_NOTREADY_DELAY: std::time::Duration = std::time::Duration::from_secs(5);
|
||||||
|
const NOTIFY_NOTREADY_PRIORITY: u32 = 99;
|
||||||
|
const NOTIFY_TEMPORARY_DELAY_FACTOR: f64 = 2.0;
|
||||||
|
const NOTIFY_TEMPORARY_DELAY_TIME: f64 = 5.0;
|
||||||
|
const NOTIFY_TEMPORARY_MAX_TIME: f64 = f64::MAX;
|
||||||
|
|
||||||
|
pub async fn notify(
|
||||||
|
finalize_path: &Path,
|
||||||
|
ctx: &mut ExecutionContext,
|
||||||
|
event_tx: EventSender,
|
||||||
|
notifyevent_rx: &mut NotifyEventReceiver,
|
||||||
|
notifyevent_tx: NotifyEventSender,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let mut finalize = parse(finalize_path)?;
|
||||||
|
validate(&finalize)?;
|
||||||
|
let plugin_list = extract_plugin(&finalize);
|
||||||
|
|
||||||
|
if plugin_list.is_empty() {
|
||||||
|
log::warn!("No notification method provided, skipping notification");
|
||||||
|
// TODO: send event to event_tx
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctx.standalone {
|
||||||
|
fetch::fetch_plugins(
|
||||||
|
&PathBuf::from(FINALIZE_STANDALONE_PLUGIN_PATH),
|
||||||
|
&mut finalize.plugin,
|
||||||
|
Some(&plugin_list),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
let notify_plugins = plugin::register(finalize.plugin, Some(&plugin_list))?;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
Some(events) = notifyevent_rx.recv() => {
|
||||||
|
match notify_handler(¬ify_plugins, &finalize.notification, &events){
|
||||||
|
Ok(_) => {}, // Notification succeeded / failed with fallible
|
||||||
|
Err(e) => {
|
||||||
|
log::debug!("Failed to send notification: {:?}", e);
|
||||||
|
match e {
|
||||||
|
NotifyError::PluginNotAvailable(_) => {
|
||||||
|
// Should keep lives, add static delay and give less priority
|
||||||
|
for mut event in events {
|
||||||
|
event.not_before = Utc::now() + NOTIFY_NOTREADY_DELAY;
|
||||||
|
event.effective_priority = event.effective_priority.saturating_add(1);
|
||||||
|
notifyevent_tx.send(event).await?;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
NotifyError::TemporaryError(_) => {
|
||||||
|
// Should cost 1 life, add dynamic (exponential) delay
|
||||||
|
for mut event in events {
|
||||||
|
event.lives -= 1;
|
||||||
|
event.not_before = Utc::now() + notify_delay(&event);
|
||||||
|
notifyevent_tx.send(event).await?;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
NotifyError::PermanentError(_) => {
|
||||||
|
// Unexpected!
|
||||||
|
// Should be dropped
|
||||||
|
log::error!("None of the configured notification plugins works.");
|
||||||
|
log::error!("Message will be dropped.");
|
||||||
|
// TODO: Send audit/metric information
|
||||||
|
}
|
||||||
|
error => {
|
||||||
|
// Unexpected!
|
||||||
|
log::error!("Internal error: notify_handler returned unexpected error: {:?}", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn notify_handler(
|
||||||
|
plugins: &NotifyPluginMap,
|
||||||
|
config: &NotificationConfig,
|
||||||
|
event: &Vec<NotifyEvent>,
|
||||||
|
) -> Result<(), NotifyError> {
|
||||||
|
todo!();
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculate min(1, k/x)*t as notification grouping period
|
||||||
|
// to avoid excessive notification frequency
|
||||||
|
fn wait_time(group_period: u32, group_watermark: u32, count: u32) -> f32 {
|
||||||
|
if count == 0 {
|
||||||
|
log::warn!("Unexpected notification count: {}", count);
|
||||||
|
return group_period as f32;
|
||||||
|
}
|
||||||
|
f32::min(1.0, group_watermark as f32 / count as f32) * group_period as f32
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculate p^(m-l)*t as notification delay
|
||||||
|
fn notify_delay(event: &NotifyEvent) -> std::time::Duration {
|
||||||
|
let exponent = (event.max_lives - event.lives) as i32;
|
||||||
|
if exponent < 0 {
|
||||||
|
log::warn!("Unexpected lives in notification: lives({}) > max_lives({})", event.lives, event.max_lives);
|
||||||
|
return std::time::Duration::from_secs_f64(NOTIFY_TEMPORARY_MAX_TIME);
|
||||||
|
}
|
||||||
|
let multiplier = NOTIFY_TEMPORARY_DELAY_FACTOR.powi(exponent);
|
||||||
|
let delay = multiplier * NOTIFY_TEMPORARY_DELAY_TIME;
|
||||||
|
if !delay.is_finite() || delay < 0.0 {
|
||||||
|
log::warn!("Unexpected calculated delay: {}, treating as infinite", delay);
|
||||||
|
return std::time::Duration::from_secs_f64(NOTIFY_TEMPORARY_MAX_TIME);
|
||||||
|
}
|
||||||
|
std::time::Duration::from_secs_f64(delay)
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum NotifyError{
|
||||||
|
#[error("Finalize config validation failed: {0:?}")]
|
||||||
|
FinalizeValidationError(Vec<String>),
|
||||||
|
|
||||||
|
#[error("Notification plugin not available {0}")]
|
||||||
|
PluginNotAvailable(String),
|
||||||
|
|
||||||
|
#[error("Temporary failure: {0}")]
|
||||||
|
TemporaryError(String),
|
||||||
|
|
||||||
|
#[error("Permanent error: {0}")]
|
||||||
|
PermanentError(String),
|
||||||
|
|
||||||
|
#[error("Notification failed after {0} attempt(s)")]
|
||||||
|
Exhausted(u32),
|
||||||
|
}
|
||||||
@@ -32,7 +32,7 @@ use crate::{
|
|||||||
error::PrebakeError,
|
error::PrebakeError,
|
||||||
ExecutionContext,
|
ExecutionContext,
|
||||||
prebake::{security::get_drop_after, stage::PrebakeStage},
|
prebake::{security::get_drop_after, stage::PrebakeStage},
|
||||||
types::build_status::{write_stage_status, StageInfo, StagePhase, StageResult},
|
types::buildstatus::{write_stage_status, StageInfo, StagePhase, StageResult},
|
||||||
constant::STANDALONE_STATUS_DIR,
|
constant::STANDALONE_STATUS_DIR,
|
||||||
};
|
};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
@@ -137,7 +137,7 @@ mut ctx: &mut ExecutionContext,
|
|||||||
StageInfo {
|
StageInfo {
|
||||||
name: "bootstrap".to_string(),
|
name: "bootstrap".to_string(),
|
||||||
phase: StagePhase::Prebake,
|
phase: StagePhase::Prebake,
|
||||||
sub_stage: "bootstrap".to_string(),
|
substage: "bootstrap".to_string(),
|
||||||
result: stage_result,
|
result: stage_result,
|
||||||
duration_ms: Utc::now().signed_duration_since(stage_start).num_milliseconds() as u64,
|
duration_ms: Utc::now().signed_duration_since(stage_start).num_milliseconds() as u64,
|
||||||
started_at: stage_start,
|
started_at: stage_start,
|
||||||
@@ -165,7 +165,7 @@ mut ctx: &mut ExecutionContext,
|
|||||||
StageInfo {
|
StageInfo {
|
||||||
name: "earlyhook".to_string(),
|
name: "earlyhook".to_string(),
|
||||||
phase: StagePhase::Prebake,
|
phase: StagePhase::Prebake,
|
||||||
sub_stage: "earlyhook".to_string(),
|
substage: "earlyhook".to_string(),
|
||||||
result: stage_result,
|
result: stage_result,
|
||||||
duration_ms: Utc::now().signed_duration_since(stage_start).num_milliseconds() as u64,
|
duration_ms: Utc::now().signed_duration_since(stage_start).num_milliseconds() as u64,
|
||||||
started_at: stage_start,
|
started_at: stage_start,
|
||||||
@@ -247,7 +247,7 @@ mut ctx: &mut ExecutionContext,
|
|||||||
StageInfo {
|
StageInfo {
|
||||||
name: "depssystem".to_string(),
|
name: "depssystem".to_string(),
|
||||||
phase: StagePhase::Prebake,
|
phase: StagePhase::Prebake,
|
||||||
sub_stage: "depssystem".to_string(),
|
substage: "depssystem".to_string(),
|
||||||
result: stage_result,
|
result: stage_result,
|
||||||
duration_ms: Utc::now().signed_duration_since(stage_start).num_milliseconds() as u64,
|
duration_ms: Utc::now().signed_duration_since(stage_start).num_milliseconds() as u64,
|
||||||
started_at: stage_start,
|
started_at: stage_start,
|
||||||
@@ -272,7 +272,7 @@ mut ctx: &mut ExecutionContext,
|
|||||||
StageInfo {
|
StageInfo {
|
||||||
name: "depsuser".to_string(),
|
name: "depsuser".to_string(),
|
||||||
phase: StagePhase::Prebake,
|
phase: StagePhase::Prebake,
|
||||||
sub_stage: "depsuser".to_string(),
|
substage: "depsuser".to_string(),
|
||||||
result: stage_result,
|
result: stage_result,
|
||||||
duration_ms: Utc::now().signed_duration_since(stage_start).num_milliseconds() as u64,
|
duration_ms: Utc::now().signed_duration_since(stage_start).num_milliseconds() as u64,
|
||||||
started_at: stage_start,
|
started_at: stage_start,
|
||||||
@@ -301,7 +301,7 @@ mut ctx: &mut ExecutionContext,
|
|||||||
StageInfo {
|
StageInfo {
|
||||||
name: "latehook".to_string(),
|
name: "latehook".to_string(),
|
||||||
phase: StagePhase::Prebake,
|
phase: StagePhase::Prebake,
|
||||||
sub_stage: "latehook".to_string(),
|
substage: "latehook".to_string(),
|
||||||
result: stage_result,
|
result: stage_result,
|
||||||
duration_ms: Utc::now().signed_duration_since(stage_start).num_milliseconds() as u64,
|
duration_ms: Utc::now().signed_duration_since(stage_start).num_milliseconds() as u64,
|
||||||
started_at: stage_start,
|
started_at: stage_start,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
pub mod architecture;
|
pub mod architecture;
|
||||||
pub mod build_status;
|
pub mod buildstatus;
|
||||||
pub mod builderconfig;
|
pub mod builderconfig;
|
||||||
pub mod cache;
|
pub mod cache;
|
||||||
pub mod command;
|
pub mod command;
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ impl std::fmt::Display for StageResult {
|
|||||||
pub struct StageInfo {
|
pub struct StageInfo {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub phase: StagePhase,
|
pub phase: StagePhase,
|
||||||
pub sub_stage: String,
|
pub substage: String,
|
||||||
pub result: StageResult,
|
pub result: StageResult,
|
||||||
pub duration_ms: u64,
|
pub duration_ms: u64,
|
||||||
pub started_at: DateTime<Utc>,
|
pub started_at: DateTime<Utc>,
|
||||||
@@ -51,7 +51,7 @@ pub struct StageInfo {
|
|||||||
|
|
||||||
impl StageInfo {
|
impl StageInfo {
|
||||||
pub fn full_name(&self) -> String {
|
pub fn full_name(&self) -> String {
|
||||||
format!("{}.{}", self.phase, self.sub_stage)
|
format!("{}.{}", self.phase, self.substage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,7 +143,7 @@ mod tests {
|
|||||||
StageInfo {
|
StageInfo {
|
||||||
name: name.to_string(),
|
name: name.to_string(),
|
||||||
phase,
|
phase,
|
||||||
sub_stage: name.to_string(),
|
substage: name.to_string(),
|
||||||
result,
|
result,
|
||||||
duration_ms: 1000,
|
duration_ms: 1000,
|
||||||
started_at: now,
|
started_at: now,
|
||||||
@@ -161,7 +161,7 @@ mod tests {
|
|||||||
|
|
||||||
let status = read_build_status(&dir).unwrap();
|
let status = read_build_status(&dir).unwrap();
|
||||||
assert_eq!(status.stages.len(), 1);
|
assert_eq!(status.stages.len(), 1);
|
||||||
assert_eq!(status.stages[0].sub_stage, "bootstrap");
|
assert_eq!(status.stages[0].substage, "bootstrap");
|
||||||
assert_eq!(status.stages[0].result, StageResult::Success);
|
assert_eq!(status.stages[0].result, StageResult::Success);
|
||||||
}
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user