Compare commits

...
Author SHA1 Message Date
Brandon Thomas 80e428ee60 errors 2026-03-25 17:17:25 -04:00
Brandon Thomas a4e3525080 fix error types 2026-03-25 17:13:41 -04:00
Brandon Thomas 7e8ca70153 errors 2026-03-25 17:10:21 -04:00
Brandon Thomas 9152b61cca error refactor 2026-03-25 17:06:55 -04:00
Brandon Thomas 24ccda5a61 rename 2026-03-25 16:57:19 -04:00
Brandon Thomas cdd88ff6d0 cleanup 2026-03-25 16:53:29 -04:00
Brandon Thomas 930d705217 get rid of pesky type alias 2026-03-25 16:38:56 -04:00
Brandon Thomas d80c3fdfe7 improvement 2026-03-25 16:26:37 -04:00
Brandon Thomas 7d5c741bc0 improvements 2026-03-25 16:24:33 -04:00
Brandon Thomas 521c1ebf22 refactor 2026-03-25 15:11:18 -04:00
Brandon Thomas d50cb38178 refactor 2026-03-25 15:06:45 -04:00
Brandon Thomas 7ce0018e13 wip 2026-03-25 15:06:28 -04:00
Brandon Thomas b423e4d524 error 2026-03-25 14:51:54 -04:00
Brandon Thomas 89896611e7 pager 2026-03-25 14:41:44 -04:00
19 changed files with 883 additions and 5 deletions
Generated
+22 -3
View File
@@ -596,12 +596,13 @@ dependencies = [
[[package]]
name = "anstyle-wincon"
version = "3.0.2"
version = "3.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7"
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"windows-sys 0.52.0",
"once_cell_polyfill",
"windows-sys 0.60.2",
]
[[package]]
@@ -7737,6 +7738,12 @@ version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "once_cell_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "opaque-debug"
version = "0.3.0"
@@ -8023,6 +8030,18 @@ dependencies = [
"sha2 0.10.8",
]
[[package]]
name = "pager"
version = "0.0.1"
dependencies = [
"chrono",
"errors",
"log",
"rootly_client",
"tokio",
"workspace-build-acceleration",
]
[[package]]
name = "pango"
version = "0.18.3"
+2
View File
@@ -75,6 +75,7 @@ members = [
"crates/lib/jwt_signer",
"crates/lib/logging",
"crates/lib/markdown",
"crates/lib/pager",
"crates/lib/password",
"crates/lib/server_environment",
"crates/lib/sqlx_mysql_helpers",
@@ -252,6 +253,7 @@ midjourney_client = { path = "crates/api_clients/midjourney_client" }
mimetypes = { path = "crates/lib/files/mimetypes" }
muapi_client = { path = "crates/api_clients/muapi_client" }
mysql_queries = { path = "crates/schema/database/mysql_queries" }
pager = { path = "crates/lib/pager" }
openai_sora_client = { path = "crates/api_clients/openai_sora_client" }
primitives = { path = "crates/lib/data/primitives" }
seedance2pro_client = { path = "crates/api_clients/seedance2pro_client" }
+28
View File
@@ -0,0 +1,28 @@
[package]
name = "pager"
edition = "2024"
version = "0.0.1"
publish = false
[lib]
name = "pager"
path = "src/lib.rs"
[package.metadata.cargo-udeps.ignore]
normal = ["workspace-build-acceleration"]
[dependencies]
# Workspace hack (faster compile times)
workspace-build-acceleration.workspace = true
# Internal
errors.workspace = true
rootly_client.workspace = true
# External
chrono.workspace = true
log.workspace = true
tokio.workspace = true
[dev-dependencies]
tokio = { version = "1", features = ["full"] }
+3
View File
@@ -0,0 +1,3 @@
pub mod pager;
pub mod pager_builder;
pub mod pager_client;
+96
View File
@@ -0,0 +1,96 @@
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use log::info;
use crate::client::pager_client::{PageSentResult, PagerClient};
use crate::error::pager_error::PagerError;
use crate::error::pager_system_error::PagerSystemError;
use crate::notification::notification_details::NotificationDetails;
use crate::worker::pager_worker_message_queue::PagerWorkerMessageQueue;
/// The main programmer interface to the pager system.
///
/// Supports two modes:
/// - **Immediate**: `send_page_immediately()` sends inline (blocks until API responds).
/// - **Queued**: `enqueue_page()` pushes to a background worker (non-blocking).
///
/// Build an instance via `PagerBuilder`:
/// ```ignore
/// // Without worker:
/// let pager = PagerBuilder::new()
/// .application_name("my-service".to_string())
/// .rootly(api_key)
/// .build()?;
///
/// // With worker:
/// let (pager, worker) = PagerBuilder::new()
/// .application_name("my-service".to_string())
/// .rootly(api_key)
/// .build_with_worker()?;
/// ```
pub struct Pager {
client: PagerClient,
queue: Option<Arc<PagerWorkerMessageQueue>>,
worker_shutdown: Option<Arc<AtomicBool>>,
}
impl Pager {
/// Create a Pager without a worker (immediate-only mode).
pub(crate) fn new(client: PagerClient) -> Self {
Self { client, queue: None, worker_shutdown: None }
}
/// Create a Pager with a shared queue and shutdown handle (worker mode).
pub(crate) fn with_queue(
client: PagerClient,
queue: Arc<PagerWorkerMessageQueue>,
worker_shutdown: Arc<AtomicBool>,
) -> Self {
Self {
client,
queue: Some(queue),
worker_shutdown: Some(worker_shutdown),
}
}
/// Send a page immediately, blocking until the API responds.
pub async fn send_page_immediately(
&self,
notification: NotificationDetails,
) -> Result<PageSentResult, PagerError> {
self.client.send_page(&notification).await
}
/// Enqueue a page to be sent by the background worker thread.
///
/// Returns an error if the worker is not configured (use `build_with_worker()`).
/// If the queue is full, the oldest item is dropped.
pub fn enqueue_page(
&self,
notification: NotificationDetails,
) -> Result<(), PagerError> {
let queue = self.queue.as_ref()
.ok_or(PagerSystemError::QueueNotConfigured)?;
let dropped = queue.push(notification)?;
if let Some(dropped) = dropped {
log::warn!("Pager queue overflow: dropped '{}'", dropped.summary);
}
Ok(())
}
/// Signal the worker thread to shut down.
pub fn shutdown_worker(&self) {
if let Some(ref shutdown) = self.worker_shutdown {
info!("Pager: signaling worker shutdown.");
shutdown.store(true, std::sync::atomic::Ordering::Relaxed);
}
if let Some(ref queue) = self.queue {
info!("Pager: notifying threads blocking on queue of shutdown.");
queue.notify_all();
}
}
}
@@ -0,0 +1,157 @@
use std::sync::Arc;
use rootly_client::creds::rootly_api_key::RootlyApiKey;
use crate::client::pager::Pager;
use crate::client::pager_client::{PagerClient, PagerClientConfig};
use crate::error::pager_builder_error::PagerBuilderError;
use crate::error::pager_error::PagerError;
use crate::worker::pager_worker_message_queue::{new_shared_queue, new_shared_queue_with_capacity, PagerWorkerMessageQueue};
use crate::worker::pager_worker::PagerWorker;
/// Builder for constructing a `Pager` instance.
pub struct PagerBuilder {
client_config: Option<PagerClientConfig>,
application_name: Option<String>,
environment: Option<String>,
queue_capacity: Option<usize>,
}
impl PagerBuilder {
pub fn new() -> Self {
Self {
client_config: None,
application_name: None,
environment: None,
queue_capacity: None,
}
}
/// Set the application name (used as the "source" tag on alerts).
pub fn application_name(mut self, name: String) -> Self {
self.application_name = Some(name);
self
}
/// Set the environment label (e.g. "production", "staging").
pub fn environment(mut self, environment: String) -> Self {
self.environment = Some(environment);
self
}
/// Configure the Rootly backend. Returns a sub-builder for Rootly-specific options.
pub fn rootly(self, api_key: RootlyApiKey) -> RootlyConfigBuilder {
RootlyConfigBuilder {
parent: self,
api_key,
alert_urgency_id: None,
notification_target_type: None,
notification_target_id: None,
}
}
/// Set the backend config directly (for advanced use or future backends).
pub fn client_config(mut self, config: PagerClientConfig) -> Self {
self.client_config = Some(config);
self
}
/// Set the maximum capacity for the worker message queue.
/// Only relevant when using `build_with_worker()`.
pub fn queue_capacity(mut self, capacity: usize) -> Self {
self.queue_capacity = Some(capacity);
self
}
/// Build a `Pager` without a background worker.
///
/// Only `send_page_immediately()` will be available.
/// Calling `enqueue_page()` will return `PagerServiceError::WorkerNotAvailable`.
pub fn build(self) -> Result<Pager, PagerError> {
let client = self.make_client()?;
Ok(Pager::new(client))
}
/// Build a `Pager` with a background worker thread.
///
/// Returns both the `Pager` (for enqueuing) and the `PagerWorker` (to spawn).
/// The caller is responsible for running the worker on a dedicated thread:
/// ```ignore
/// let (pager, worker) = PagerBuilder::new()
/// .application_name("my-service".to_string())
/// .rootly(api_key)
/// .build_with_worker()?;
///
/// std::thread::spawn(move || {
/// let rt = tokio::runtime::Runtime::new().unwrap();
/// rt.block_on(worker.run());
/// });
/// ```
pub fn build_with_worker(self) -> Result<(Pager, PagerWorker), PagerError> {
let client = self.make_client()?;
let queue: Arc<PagerWorkerMessageQueue> = match self.queue_capacity {
Some(capacity) => new_shared_queue_with_capacity(capacity),
None => new_shared_queue(),
};
let worker = PagerWorker::new(queue.clone(), client.clone());
let shutdown = worker.shutdown_handle();
let pager = Pager::with_queue(client, queue, shutdown);
Ok((pager, worker))
}
fn make_client(&self) -> Result<PagerClient, PagerError> {
let client_config = self.client_config.clone()
.ok_or(PagerBuilderError::NoBackendConfigured)?;
Ok(PagerClient::new(client_config, self.application_name.clone(), self.environment.clone()))
}
}
/// Sub-builder for configuring the Rootly backend.
/// Returned by `PagerBuilder::rootly()`.
pub struct RootlyConfigBuilder {
parent: PagerBuilder,
api_key: RootlyApiKey,
alert_urgency_id: Option<String>,
notification_target_type: Option<String>,
notification_target_id: Option<String>,
}
impl RootlyConfigBuilder {
/// Set the alert urgency ID.
pub fn alert_urgency_id(mut self, id: String) -> Self {
self.alert_urgency_id = Some(id);
self
}
/// Set the notification target (who gets paged).
pub fn notification_target(mut self, target_type: String, target_id: String) -> Self {
self.notification_target_type = Some(target_type);
self.notification_target_id = Some(target_id);
self
}
/// Finish Rootly configuration and return to the parent builder.
pub fn done(mut self) -> PagerBuilder {
self.parent.client_config = Some(PagerClientConfig::Rootly {
api_key: self.api_key,
alert_urgency_id: self.alert_urgency_id,
notification_target_type: self.notification_target_type,
notification_target_id: self.notification_target_id,
});
self.parent
}
/// Shortcut: finish Rootly config and build without a worker.
pub fn build(self) -> Result<Pager, PagerError> {
self.done().build()
}
/// Shortcut: finish Rootly config and build with a worker.
pub fn build_with_worker(self) -> Result<(Pager, PagerWorker), PagerError> {
self.done().build_with_worker()
}
}
+126
View File
@@ -0,0 +1,126 @@
use log::{error, info};
use rootly_client::creds::rootly_api_key::RootlyApiKey;
use rootly_client::requests::create_alert::create_alert::{
create_alert, CreateAlertArgs,
};
use crate::error::pager_error::PagerError;
use crate::error::pager_service_error::PagerServiceError;
use crate::notification::notification_details::NotificationDetails;
/// The actual client that sends pages.
#[derive(Clone)]
pub struct PagerClient {
/// The backend-specific configuration.
pub client_config: PagerClientConfig,
/// Application name used as the "source" tag in alerts (e.g. "storyteller-web", "seedance2-pro-job").
pub application_name: Option<String>,
/// Environment label (e.g. "production", "staging"). Inserted as a label on alerts.
pub environment: Option<String>,
}
/// Configuration for the pager client backend.
#[derive(Clone)]
pub enum PagerClientConfig {
/// Use Rootly as the paging backend.
Rootly {
api_key: RootlyApiKey,
/// Alert urgency ID (e.g. "62fde143-..." maps to "High" in our org).
alert_urgency_id: Option<String>,
/// Notification target type (e.g. "User", "EscalationPolicy").
notification_target_type: Option<String>,
/// Notification target ID (e.g. a user ID or escalation policy ID).
notification_target_id: Option<String>,
},
}
/// Result of a successfully sent page.
#[derive(Debug, Clone)]
pub struct PageSentResult {
/// A backend-specific identifier for the sent notification.
pub id: String,
/// A short human-readable ID (if the backend provides one).
pub short_id: Option<String>,
}
impl PagerClient {
pub fn new(
client_config: PagerClientConfig,
application_name: Option<String>,
environment: Option<String>,
) -> Self {
Self { client_config, application_name, environment }
}
/// Send a page immediately.
pub async fn send_page(&self, notification: &NotificationDetails) -> Result<PageSentResult, PagerError> {
match &self.client_config {
PagerClientConfig::Rootly { .. } => self.send_page_via_rootly(notification).await,
}
}
async fn send_page_via_rootly(
&self,
notification: &NotificationDetails,
) -> Result<PageSentResult, PagerError> {
let PagerClientConfig::Rootly {
api_key,
alert_urgency_id,
notification_target_type,
notification_target_id,
} = &self.client_config;
let source = self.application_name
.clone()
.unwrap_or_else(|| "unknown".to_string());
info!("Sending page via Rootly (source={}): {}", source, notification.summary);
let mut labels: Vec<(String, String)> = Vec::new();
if let Some(env) = &self.environment {
labels.push(("environment".to_string(), env.clone()));
}
let labels = if labels.is_empty() { None } else { Some(labels) };
let result = create_alert(CreateAlertArgs {
api_key: api_key.clone(),
source,
summary: notification.summary.clone(),
description: notification.description.clone(),
status: Some("triggered".to_string()),
service_ids: None,
group_ids: None,
environment_ids: None,
external_id: None,
external_url: None,
alert_urgency_id: alert_urgency_id.clone(),
notification_target_type: notification_target_type.clone(),
notification_target_id: notification_target_id.clone(),
labels,
deduplication_key: None,
}).await;
match result {
Ok(success) => {
info!("Page sent successfully via Rootly: id={}, short_id={:?}", success.id, success.short_id);
Ok(PageSentResult {
id: success.id,
short_id: success.short_id,
})
}
Err(err) => {
error!("Failed to send page via Rootly: {}", err);
Err(PagerError::Service(PagerServiceError::RootlyError(err)))
}
}
}
}
+4
View File
@@ -0,0 +1,4 @@
pub mod pager_builder_error;
pub mod pager_error;
pub mod pager_service_error;
pub mod pager_system_error;
@@ -0,0 +1,19 @@
use std::error::Error;
use std::fmt::{Display, Formatter};
/// Errors from building/configuring a Pager instance.
#[derive(Debug)]
pub enum PagerBuilderError {
/// The pager was not configured with a backend.
NoBackendConfigured,
}
impl Error for PagerBuilderError {}
impl Display for PagerBuilderError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::NoBackendConfigured => write!(f, "No pager backend configured. Call .rootly() or .client_config() on PagerBuilder."),
}
}
}
+49
View File
@@ -0,0 +1,49 @@
use std::error::Error;
use std::fmt::{Display, Formatter};
use crate::error::pager_builder_error::PagerBuilderError;
use crate::error::pager_service_error::PagerServiceError;
use crate::error::pager_system_error::PagerSystemError;
/// Top-level error type for the pager library.
#[derive(Debug)]
pub enum PagerError {
/// An error during Pager construction/configuration.
Builder(PagerBuilderError),
/// A third-party paging service (Rootly, etc.) returned an error.
Service(PagerServiceError),
/// Our own pager system broke (mutex, queue, etc.).
System(PagerSystemError),
}
impl Error for PagerError {}
impl Display for PagerError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::Builder(err) => write!(f, "PagerBuilderError: {}", err),
Self::Service(err) => write!(f, "PagerServiceError: {}", err),
Self::System(err) => write!(f, "PagerSystemError: {}", err),
}
}
}
impl From<PagerBuilderError> for PagerError {
fn from(err: PagerBuilderError) -> Self {
Self::Builder(err)
}
}
impl From<PagerServiceError> for PagerError {
fn from(err: PagerServiceError) -> Self {
Self::Service(err)
}
}
impl From<PagerSystemError> for PagerError {
fn from(err: PagerSystemError) -> Self {
Self::System(err)
}
}
@@ -0,0 +1,27 @@
use std::error::Error;
use std::fmt::{Display, Formatter};
use rootly_client::error::rootly_error::RootlyError;
/// Errors from third-party paging services (Rootly, etc.).
#[derive(Debug)]
pub enum PagerServiceError {
/// The underlying Rootly API returned an error.
RootlyError(RootlyError),
}
impl Error for PagerServiceError {}
impl Display for PagerServiceError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::RootlyError(err) => write!(f, "Rootly API error: {}", err),
}
}
}
impl From<RootlyError> for PagerServiceError {
fn from(err: RootlyError) -> Self {
Self::RootlyError(err)
}
}
@@ -0,0 +1,23 @@
use std::error::Error;
use std::fmt::{Display, Formatter};
/// Errors from our own pager system at runtime (concurrency, queue, etc.).
#[derive(Debug)]
pub enum PagerSystemError {
/// The message queue was not configured. Use `build_with_worker()` to enable queuing.
QueueNotConfigured,
/// A mutex lock was poisoned (another thread panicked while holding it).
MutexPoisoned(String),
}
impl Error for PagerSystemError {}
impl Display for PagerSystemError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::QueueNotConfigured => write!(f, "Pager message queue not configured. Use build_with_worker() to enable queuing."),
Self::MutexPoisoned(msg) => write!(f, "Pager mutex poisoned: {}", msg),
}
}
}
+4
View File
@@ -0,0 +1,4 @@
pub mod client;
pub mod error;
pub mod notification;
pub mod worker;
+1
View File
@@ -0,0 +1 @@
pub mod notification_details;
@@ -0,0 +1,101 @@
use chrono::{DateTime, Utc};
use std::fmt::{Debug, Display};
/// Details for a pager notification.
#[derive(Clone, Debug)]
pub struct NotificationDetails {
/// Title or summary of the alert.
pub summary: String,
/// Full details for the alert.
pub description: Option<String>,
/// When the event occurred.
pub event_time: DateTime<Utc>,
}
impl NotificationDetails {
/// Create a notification with a summary and description.
pub fn with_summary_and_description(summary: String, description: String) -> Self {
Self {
summary,
description: Some(description),
event_time: Utc::now(),
}
}
/// Create a notification with just a summary.
pub fn with_summary(summary: String) -> Self {
Self {
summary,
description: None,
event_time: Utc::now(),
}
}
/// Create a notification from any error type.
///
/// Formats the error into a structured description that includes:
/// - The error message
/// - The error's source chain (if any)
/// - A backtrace (if available via `std::backtrace`)
/// - The timestamp of the event
pub fn from_error<E: Debug + Display>(error: &E) -> Self {
let summary = format!("{}", error);
// Truncate summary to a reasonable length for alert titles.
let summary = if summary.len() > 200 {
format!("{}...", &summary[..197])
} else {
summary
};
let event_time = Utc::now();
let mut description_parts: Vec<String> = Vec::new();
description_parts.push(format!("Event time: {}", event_time.format("%Y-%m-%d %H:%M:%S UTC")));
description_parts.push(String::new());
description_parts.push(format!("Error: {}", error));
// Include the Debug representation if it differs from Display (often has more detail).
let debug_repr = format!("{:?}", error);
let display_repr = format!("{}", error);
if debug_repr != display_repr {
description_parts.push(String::new());
description_parts.push(format!("Debug: {}", debug_repr));
}
// Attempt to walk the error source chain.
// NB: We use the Debug trait here since we can't require std::error::Error
// (that would require E: 'static + Error which is too restrictive for callers).
// Try to capture a backtrace from the current call site.
#[cfg(feature = "backtrace")]
{
let bt = std::backtrace::Backtrace::capture();
if bt.status() == std::backtrace::BacktraceStatus::Captured {
description_parts.push(String::new());
description_parts.push(format!("Backtrace:\n{}", bt));
}
}
let description = description_parts.join("\n");
Self {
summary,
description: Some(description),
event_time,
}
}
/// Create a notification from an error, with a custom summary prefix.
pub fn from_error_with_context<E: Debug + Display>(context: &str, error: &E) -> Self {
let mut notification = Self::from_error(error);
notification.summary = format!("{}: {}", context, notification.summary);
if notification.summary.len() > 200 {
notification.summary = format!("{}...", &notification.summary[..197]);
}
notification
}
}
+2
View File
@@ -0,0 +1,2 @@
pub mod pager_worker_message_queue;
pub mod pager_worker;
+112
View File
@@ -0,0 +1,112 @@
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
use log::{error, info, warn};
use crate::client::pager_client::PagerClient;
use crate::worker::pager_worker_message_queue::PagerWorkerMessageQueue;
/// A background worker thread that consumes a message queue and sends pages.
pub struct PagerWorker {
queue: Arc<PagerWorkerMessageQueue>,
client: PagerClient,
shutdown: Arc<AtomicBool>,
}
impl PagerWorker {
pub fn new(
queue: Arc<PagerWorkerMessageQueue>,
client: PagerClient,
) -> Self {
Self {
queue,
client,
shutdown: Arc::new(AtomicBool::new(false)),
}
}
/// Get a handle to the shutdown flag so external code can trigger shutdown.
pub fn shutdown_handle(&self) -> Arc<AtomicBool> {
self.shutdown.clone()
}
/// Signal the worker to stop.
pub fn shutdown(&self) {
info!("Pager worker thread shutdown requested.");
self.shutdown.store(true, Ordering::Relaxed);
// Wake the thread in case it's blocked on wait_and_drain().
self.queue.notify_all();
}
/// Run the main loop. This blocks the current thread until shutdown is signaled.
///
/// Intended to be called from a dedicated thread:
/// ```ignore
/// std::thread::spawn(move || {
/// let rt = tokio::runtime::Runtime::new().unwrap();
/// rt.block_on(worker.run());
/// });
/// ```
pub async fn run(&self) {
info!("Pager worker thread started.");
while !self.shutdown.load(Ordering::Relaxed) {
// Block until items are available (or we're woken up for shutdown).
let items = match self.queue.wait_and_drain() {
Ok(items) => items,
Err(err) => {
error!("Pager worker queue error: {}. Retrying in 5s.", err);
tokio::time::sleep(Duration::from_secs(5)).await;
continue;
}
};
if items.is_empty() {
// Woken up for shutdown with no items.
continue;
}
info!("Pager worker processing {} queued notification(s).", items.len());
for notification in &items {
if self.shutdown.load(Ordering::Relaxed) {
warn!("Pager worker shutting down, {} item(s) still in batch.", items.len());
break;
}
match self.client.send_page(notification).await {
Ok(success) => {
info!(
"Pager worker sent page: id={}, summary={}",
success.id, notification.summary
);
}
Err(err) => {
error!(
"Pager worker failed to send page for '{}': {}",
notification.summary, err
);
// Don't kill the thread on errors — keep processing.
}
}
}
}
// Drain any remaining items on shutdown.
match self.queue.drain_available() {
Ok(remaining) if !remaining.is_empty() => {
warn!(
"Pager worker shutting down with {} unsent notification(s) in queue.",
remaining.len()
);
}
Err(err) => {
error!("Pager worker could not drain queue on shutdown: {}", err);
}
_ => {}
}
info!("Pager worker thread stopped.");
}
}
@@ -0,0 +1,105 @@
use std::collections::VecDeque;
use std::sync::{Arc, Mutex, Condvar};
use log::warn;
use crate::error::pager_error::PagerError;
use crate::error::pager_system_error::PagerSystemError;
use crate::notification::notification_details::NotificationDetails;
const DEFAULT_MAX_SIZE: usize = 256;
/// A thread-safe bounded ring buffer for pager notifications.
///
/// When the queue is full, the oldest item is dropped to make room.
/// The worker thread blocks on `wait_and_drain()` until items are available.
pub struct PagerWorkerMessageQueue {
inner: Mutex<VecDeque<NotificationDetails>>,
condvar: Condvar,
max_size: usize,
}
impl PagerWorkerMessageQueue {
pub fn new() -> Self {
Self::with_capacity(DEFAULT_MAX_SIZE)
}
pub fn with_capacity(max_size: usize) -> Self {
Self {
inner: Mutex::new(VecDeque::with_capacity(max_size)),
condvar: Condvar::new(),
max_size,
}
}
/// Push a notification onto the queue.
///
/// If the queue is full, the oldest item is dropped and a warning is logged.
/// Returns `Ok(Some(summary))` if an old item was evicted, `Ok(None)` otherwise.
pub fn push(&self, notification: NotificationDetails) -> Result<Option<NotificationDetails>, PagerError> {
let mut queue = self.inner.lock()
.map_err(|e| PagerSystemError::MutexPoisoned(format!("push: {}", e)))?;
let dropped = if queue.len() >= self.max_size {
let old = queue.pop_front();
if let Some(ref n) = old {
warn!("Pager queue full (max={}). Dropped oldest: {}", self.max_size, n.summary);
}
old
} else {
None
};
queue.push_back(notification);
self.condvar.notify_one();
Ok(dropped)
}
/// Block until at least one item is available, then drain all items.
///
/// Returns an error if the mutex is poisoned.
pub fn wait_and_drain(&self) -> Result<Vec<NotificationDetails>, PagerError> {
let mut queue = self.inner.lock()
.map_err(|e| PagerSystemError::MutexPoisoned(format!("wait_and_drain lock: {}", e)))?;
while queue.is_empty() {
queue = self.condvar.wait(queue)
.map_err(|e| PagerSystemError::MutexPoisoned(format!("wait_and_drain wait: {}", e)))?;
}
let items: Vec<NotificationDetails> = queue.drain(..).collect();
Ok(items)
}
/// Non-blocking drain of all currently queued items.
pub fn drain_available(&self) -> Result<Vec<NotificationDetails>, PagerError> {
let mut queue = self.inner.lock()
.map_err(|e| PagerSystemError::MutexPoisoned(format!("drain_available: {}", e)))?;
Ok(queue.drain(..).collect())
}
/// Wake up any thread blocked on `wait_and_drain()`.
///
/// Used during shutdown to unblock the worker thread.
pub fn notify_all(&self) {
self.condvar.notify_all();
}
pub fn len(&self) -> Result<usize, PagerError> {
let queue = self.inner.lock()
.map_err(|e| PagerSystemError::MutexPoisoned(format!("len: {}", e)))?;
Ok(queue.len())
}
pub fn is_empty(&self) -> Result<bool, PagerError> {
Ok(self.len()? == 0)
}
}
pub fn new_shared_queue() -> Arc<PagerWorkerMessageQueue> {
Arc::new(PagerWorkerMessageQueue::new())
}
pub fn new_shared_queue_with_capacity(max_size: usize) -> Arc<PagerWorkerMessageQueue> {
Arc::new(PagerWorkerMessageQueue::with_capacity(max_size))
}
@@ -343,10 +343,10 @@
"markdownDescription": "Denies the fetch_send command without any pre-configured scope."
},
{
"description": "This permission set configures what kind of\r\nfetch operations are available from the http plugin.\r\n\r\nThis enables all fetch operations but does not\r\nallow explicitly any origins to be fetched. This needs to\r\nbe manually configured before usage.\r\n\r\n#### Granted Permissions\r\n\r\nAll fetch operations are enabled.\r\n\r\n\n#### This default permission set includes:\n\n- `allow-fetch`\n- `allow-fetch-cancel`\n- `allow-fetch-read-body`\n- `allow-fetch-send`",
"description": "This permission set configures what kind of\nfetch operations are available from the http plugin.\n\nThis enables all fetch operations but does not\nallow explicitly any origins to be fetched. This needs to\nbe manually configured before usage.\n\n#### Granted Permissions\n\nAll fetch operations are enabled.\n\n\n#### This default permission set includes:\n\n- `allow-fetch`\n- `allow-fetch-cancel`\n- `allow-fetch-read-body`\n- `allow-fetch-send`",
"type": "string",
"const": "default",
"markdownDescription": "This permission set configures what kind of\r\nfetch operations are available from the http plugin.\r\n\r\nThis enables all fetch operations but does not\r\nallow explicitly any origins to be fetched. This needs to\r\nbe manually configured before usage.\r\n\r\n#### Granted Permissions\r\n\r\nAll fetch operations are enabled.\r\n\r\n\n#### This default permission set includes:\n\n- `allow-fetch`\n- `allow-fetch-cancel`\n- `allow-fetch-read-body`\n- `allow-fetch-send`"
"markdownDescription": "This permission set configures what kind of\nfetch operations are available from the http plugin.\n\nThis enables all fetch operations but does not\nallow explicitly any origins to be fetched. This needs to\nbe manually configured before usage.\n\n#### Granted Permissions\n\nAll fetch operations are enabled.\n\n\n#### This default permission set includes:\n\n- `allow-fetch`\n- `allow-fetch-cancel`\n- `allow-fetch-read-body`\n- `allow-fetch-send`"
}
]
}