mirror of
https://github.com/rzuasti/oott.git
synced 2026-07-08 19:21:54 +02:00
Improve backend DB concurrency and async safety
Tune SQLite and remove blocking calls from async/scan paths: - Enable WAL + synchronous=NORMAL + busy_timeout + foreign_keys on each pooled connection, so the five scanners, web server, and retention no longer contend on the default rollback journal / FULL fsync. - Run DB work in axum handlers via spawn_blocking (db::run_blocking) so synchronous rusqlite calls no longer block tokio worker threads. - Deliver notifications on a dedicated task fed by a bounded channel; the blocking Pushover HTTP call runs in spawn_blocking, so a slow or unreachable Pushover can never stall device discovery. - Make get_db_connection() return Result instead of panicking, so pool exhaustion surfaces as a 500 rather than crashing the process. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
8749ff33c1
commit
15bbb3bda9
@@ -2,5 +2,7 @@
|
|||||||
/backend/target
|
/backend/target
|
||||||
/result
|
/result
|
||||||
oott.db
|
oott.db
|
||||||
|
oott.db-wal
|
||||||
|
oott.db-shm
|
||||||
oott.toml
|
oott.toml
|
||||||
/.claude
|
/.claude
|
||||||
|
|||||||
+34
-10
@@ -26,19 +26,43 @@ lazy_static! {
|
|||||||
max_size(10).
|
max_size(10).
|
||||||
build(
|
build(
|
||||||
r2d2_sqlite::SqliteConnectionManager::file(get_settings().database.path.as_str())
|
r2d2_sqlite::SqliteConnectionManager::file(get_settings().database.path.as_str())
|
||||||
|
.with_init(|conn| {
|
||||||
|
// Tune every pooled connection for the concurrent access pattern of this
|
||||||
|
// app (five scanners + web server + retention sharing the pool):
|
||||||
|
// - WAL lets readers and a writer proceed concurrently instead of blocking.
|
||||||
|
// - synchronous=NORMAL is the safe, recommended pairing with WAL and avoids
|
||||||
|
// an fsync on every commit (the default FULL fsyncs on each write).
|
||||||
|
// - busy_timeout makes a connection wait for a lock rather than failing
|
||||||
|
// immediately with "database is locked".
|
||||||
|
// - foreign_keys are off by default in SQLite and must be set per connection.
|
||||||
|
conn.execute_batch(
|
||||||
|
"PRAGMA journal_mode = WAL;
|
||||||
|
PRAGMA synchronous = NORMAL;
|
||||||
|
PRAGMA busy_timeout = 5000;
|
||||||
|
PRAGMA foreign_keys = ON;",
|
||||||
|
)
|
||||||
|
})
|
||||||
).unwrap();
|
).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_db_connection() -> PooledConnection<SqliteConnectionManager> {
|
pub fn get_db_connection() -> Result<PooledConnection<SqliteConnectionManager>, DbError> {
|
||||||
let result = POOL.get();
|
POOL.get().map_err(|error| {
|
||||||
|
error!("Error obtaining database connection from the pool: {error}");
|
||||||
|
DbError::from(error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
match result {
|
// Runs a blocking database operation on tokio's blocking thread pool so it never stalls an async
|
||||||
Ok(value) => value,
|
// worker thread. The DB layer uses synchronous `rusqlite`, so axum handlers must wrap their DB
|
||||||
Err(error) => {
|
// work in this rather than calling `db::*` functions inline.
|
||||||
error!("Error obtaining database connection from the pool: {error}");
|
pub async fn run_blocking<F, T>(f: F) -> T
|
||||||
panic!("Error obtaining database connection from the pool: {error}");
|
where
|
||||||
}
|
F: FnOnce() -> T + Send + 'static,
|
||||||
}
|
T: Send + 'static,
|
||||||
|
{
|
||||||
|
tokio::task::spawn_blocking(f)
|
||||||
|
.await
|
||||||
|
.expect("blocking database task panicked")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Appends the shared `LIMIT ? OFFSET ?` paging clause (and its bound parameters) to a list query
|
// Appends the shared `LIMIT ? OFFSET ?` paging clause (and its bound parameters) to a list query
|
||||||
@@ -69,7 +93,7 @@ pub async fn init_db() -> Result<(), DbError> {
|
|||||||
|
|
||||||
debug!("Getting database connection");
|
debug!("Getting database connection");
|
||||||
|
|
||||||
let mut conn = get_db_connection();
|
let mut conn = get_db_connection()?;
|
||||||
|
|
||||||
debug!("Executing database migrations if needed.");
|
debug!("Executing database migrations if needed.");
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use crate::model::device_events::{DeviceEvent, DeviceEventScanner};
|
|||||||
use crate::utils::network::normalize_mac;
|
use crate::utils::network::normalize_mac;
|
||||||
|
|
||||||
pub fn insert(event: DeviceEvent) -> Result<i64, DbError> {
|
pub fn insert(event: DeviceEvent) -> Result<i64, DbError> {
|
||||||
let conn = db::get_db_connection();
|
let conn = db::get_db_connection()?;
|
||||||
let mac_address = normalize_mac(&event.mac_address);
|
let mac_address = normalize_mac(&event.mac_address);
|
||||||
|
|
||||||
match conn.execute(
|
match conn.execute(
|
||||||
@@ -42,7 +42,7 @@ pub fn recent_duplicate_exists(
|
|||||||
scanner: &DeviceEventScanner,
|
scanner: &DeviceEventScanner,
|
||||||
since: DateTime<Utc>,
|
since: DateTime<Utc>,
|
||||||
) -> Result<bool, DbError> {
|
) -> Result<bool, DbError> {
|
||||||
let conn = db::get_db_connection();
|
let conn = db::get_db_connection()?;
|
||||||
let mac_address = normalize_mac(mac_address);
|
let mac_address = normalize_mac(mac_address);
|
||||||
|
|
||||||
let count: i64 = conn.query_row(
|
let count: i64 = conn.query_row(
|
||||||
@@ -66,7 +66,7 @@ pub fn list(
|
|||||||
page_limit: Option<i64>,
|
page_limit: Option<i64>,
|
||||||
) -> Result<Vec<DeviceEvent>, DbError> {
|
) -> Result<Vec<DeviceEvent>, DbError> {
|
||||||
debug!("Listing device events");
|
debug!("Listing device events");
|
||||||
let conn = db::get_db_connection();
|
let conn = db::get_db_connection()?;
|
||||||
|
|
||||||
let mut sql_statement =
|
let mut sql_statement =
|
||||||
"SELECT id, mac_address, created_on, event_type, ipv4_address, vendor, scanner FROM device_events WHERE 1=1"
|
"SELECT id, mac_address, created_on, event_type, ipv4_address, vendor, scanner FROM device_events WHERE 1=1"
|
||||||
@@ -111,7 +111,7 @@ pub fn list(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn purge_older_than(cutoff: DateTime<Utc>) -> Result<usize, DbError> {
|
pub fn purge_older_than(cutoff: DateTime<Utc>) -> Result<usize, DbError> {
|
||||||
let conn = db::get_db_connection();
|
let conn = db::get_db_connection()?;
|
||||||
|
|
||||||
match conn.execute(
|
match conn.execute(
|
||||||
"DELETE FROM device_events WHERE created_on < ?1",
|
"DELETE FROM device_events WHERE created_on < ?1",
|
||||||
@@ -130,7 +130,13 @@ pub fn purge_older_than(cutoff: DateTime<Utc>) -> Result<usize, DbError> {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
fn read(id: i64) -> Option<DeviceEvent> {
|
fn read(id: i64) -> Option<DeviceEvent> {
|
||||||
let conn = db::get_db_connection();
|
let conn = match db::get_db_connection() {
|
||||||
|
Ok(conn) => conn,
|
||||||
|
Err(error) => {
|
||||||
|
error!("Error obtaining database connection: {error}");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let result: Result<DeviceEvent, rusqlite::Error> = conn.query_one(
|
let result: Result<DeviceEvent, rusqlite::Error> = conn.query_one(
|
||||||
"SELECT id, mac_address, created_on, event_type, ipv4_address, vendor, scanner FROM device_events WHERE id=?1",
|
"SELECT id, mac_address, created_on, event_type, ipv4_address, vendor, scanner FROM device_events WHERE id=?1",
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ pub fn list_devices(
|
|||||||
page_limit: Option<i64>,
|
page_limit: Option<i64>,
|
||||||
) -> Result<Vec<Device>, DbError> {
|
) -> Result<Vec<Device>, DbError> {
|
||||||
debug!("Listing devices");
|
debug!("Listing devices");
|
||||||
let conn = db::get_db_connection();
|
let conn = db::get_db_connection()?;
|
||||||
|
|
||||||
// Prepare SQL and parameters
|
// Prepare SQL and parameters
|
||||||
let (filters, mut params) = build_device_filters(
|
let (filters, mut params) = build_device_filters(
|
||||||
@@ -151,7 +151,7 @@ pub fn count_devices(
|
|||||||
vendor: Option<String>,
|
vendor: Option<String>,
|
||||||
) -> Result<i64, DbError> {
|
) -> Result<i64, DbError> {
|
||||||
debug!("Counting devices");
|
debug!("Counting devices");
|
||||||
let conn = db::get_db_connection();
|
let conn = db::get_db_connection()?;
|
||||||
|
|
||||||
let (filters, params) = build_device_filters(
|
let (filters, params) = build_device_filters(
|
||||||
is_registered,
|
is_registered,
|
||||||
@@ -174,7 +174,13 @@ pub fn count_devices(
|
|||||||
|
|
||||||
// Read device from its MAC address
|
// Read device from its MAC address
|
||||||
pub fn read(mac_address: String) -> Option<Device> {
|
pub fn read(mac_address: String) -> Option<Device> {
|
||||||
let conn = db::get_db_connection();
|
let conn = match db::get_db_connection() {
|
||||||
|
Ok(conn) => conn,
|
||||||
|
Err(error) => {
|
||||||
|
error!("Error obtaining database connection: {error}");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
let mac_address = normalize_mac(&mac_address);
|
let mac_address = normalize_mac(&mac_address);
|
||||||
|
|
||||||
let result: Result<Device, rusqlite::Error> = conn.query_one(
|
let result: Result<Device, rusqlite::Error> = conn.query_one(
|
||||||
@@ -209,7 +215,7 @@ pub fn read(mac_address: String) -> Option<Device> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert(device: Device) -> Result<(), DbError> {
|
pub fn insert(device: Device) -> Result<(), DbError> {
|
||||||
let conn = db::get_db_connection();
|
let conn = db::get_db_connection()?;
|
||||||
let mac_address = normalize_mac(&device.mac_address);
|
let mac_address = normalize_mac(&device.mac_address);
|
||||||
|
|
||||||
match conn.execute(
|
match conn.execute(
|
||||||
@@ -228,7 +234,7 @@ pub fn insert(device: Device) -> Result<(), DbError> {
|
|||||||
|
|
||||||
pub fn get_summary() -> Result<DeviceSummary, DbError> {
|
pub fn get_summary() -> Result<DeviceSummary, DbError> {
|
||||||
debug!("Getting device summary");
|
debug!("Getting device summary");
|
||||||
let conn = db::get_db_connection();
|
let conn = db::get_db_connection()?;
|
||||||
let one_day_ago = (Utc::now() - Duration::days(1)).to_rfc3339();
|
let one_day_ago = (Utc::now() - Duration::days(1)).to_rfc3339();
|
||||||
let one_week_ago = (Utc::now() - Duration::weeks(1)).to_rfc3339();
|
let one_week_ago = (Utc::now() - Duration::weeks(1)).to_rfc3339();
|
||||||
|
|
||||||
@@ -277,7 +283,7 @@ pub fn seen(
|
|||||||
device_type: String,
|
device_type: String,
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
) -> Result<(), DbError> {
|
) -> Result<(), DbError> {
|
||||||
let conn = db::get_db_connection();
|
let conn = db::get_db_connection()?;
|
||||||
let mac_address = normalize_mac(&mac_address);
|
let mac_address = normalize_mac(&mac_address);
|
||||||
|
|
||||||
let mut sql = "UPDATE devices SET ipv4_address=?, last_seen=?".to_string();
|
let mut sql = "UPDATE devices SET ipv4_address=?, last_seen=?".to_string();
|
||||||
@@ -331,7 +337,7 @@ pub fn update(
|
|||||||
vendor: String,
|
vendor: String,
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
) -> Result<(), DbError> {
|
) -> Result<(), DbError> {
|
||||||
let conn = db::get_db_connection();
|
let conn = db::get_db_connection()?;
|
||||||
let mac_address = normalize_mac(&mac_address);
|
let mac_address = normalize_mac(&mac_address);
|
||||||
|
|
||||||
match conn.execute(
|
match conn.execute(
|
||||||
@@ -355,7 +361,7 @@ pub fn register(
|
|||||||
device_type: String,
|
device_type: String,
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
) -> Result<(), DbError> {
|
) -> Result<(), DbError> {
|
||||||
let conn = db::get_db_connection();
|
let conn = db::get_db_connection()?;
|
||||||
let mac_address = normalize_mac(&mac_address);
|
let mac_address = normalize_mac(&mac_address);
|
||||||
|
|
||||||
// Only write the name column when supplied, so a user registering without typing a name
|
// Only write the name column when supplied, so a user registering without typing a name
|
||||||
@@ -382,7 +388,7 @@ pub fn register(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn unregister(mac_address: String) -> Result<(), DbError> {
|
pub fn unregister(mac_address: String) -> Result<(), DbError> {
|
||||||
let conn = db::get_db_connection();
|
let conn = db::get_db_connection()?;
|
||||||
let mac_address = normalize_mac(&mac_address);
|
let mac_address = normalize_mac(&mac_address);
|
||||||
|
|
||||||
match conn.execute(
|
match conn.execute(
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use std::{error, fmt};
|
|||||||
pub enum DbError {
|
pub enum DbError {
|
||||||
ParseRusqlite(rusqlite::Error),
|
ParseRusqlite(rusqlite::Error),
|
||||||
ParseRusqliteMigration(rusqlite_migration::Error),
|
ParseRusqliteMigration(rusqlite_migration::Error),
|
||||||
|
Pool(r2d2::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for DbError {
|
impl fmt::Display for DbError {
|
||||||
@@ -11,6 +12,7 @@ impl fmt::Display for DbError {
|
|||||||
match *self {
|
match *self {
|
||||||
DbError::ParseRusqlite(..) => write!(f, "Database access error"),
|
DbError::ParseRusqlite(..) => write!(f, "Database access error"),
|
||||||
DbError::ParseRusqliteMigration(..) => write!(f, "Database migration error"),
|
DbError::ParseRusqliteMigration(..) => write!(f, "Database migration error"),
|
||||||
|
DbError::Pool(..) => write!(f, "Database connection pool error"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -20,6 +22,7 @@ impl error::Error for DbError {
|
|||||||
match *self {
|
match *self {
|
||||||
DbError::ParseRusqlite(ref e) => Some(e),
|
DbError::ParseRusqlite(ref e) => Some(e),
|
||||||
DbError::ParseRusqliteMigration(ref e) => Some(e),
|
DbError::ParseRusqliteMigration(ref e) => Some(e),
|
||||||
|
DbError::Pool(ref e) => Some(e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -29,6 +32,11 @@ impl From<rusqlite::Error> for DbError {
|
|||||||
DbError::ParseRusqlite(err)
|
DbError::ParseRusqlite(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
impl From<r2d2::Error> for DbError {
|
||||||
|
fn from(err: r2d2::Error) -> DbError {
|
||||||
|
DbError::Pool(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
impl From<rusqlite_migration::Error> for DbError {
|
impl From<rusqlite_migration::Error> for DbError {
|
||||||
fn from(err: rusqlite_migration::Error) -> DbError {
|
fn from(err: rusqlite_migration::Error) -> DbError {
|
||||||
DbError::ParseRusqliteMigration(err)
|
DbError::ParseRusqliteMigration(err)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ pub fn list(
|
|||||||
page_limit: Option<i64>,
|
page_limit: Option<i64>,
|
||||||
) -> Result<Vec<Notification>, DbError> {
|
) -> Result<Vec<Notification>, DbError> {
|
||||||
debug!("Listing notifications");
|
debug!("Listing notifications");
|
||||||
let conn = db::get_db_connection();
|
let conn = db::get_db_connection()?;
|
||||||
|
|
||||||
let mut sql_statement =
|
let mut sql_statement =
|
||||||
"SELECT id, created_on, notification_type, title, body, is_new, mac_address FROM notifications WHERE 1=1"
|
"SELECT id, created_on, notification_type, title, body, is_new, mac_address FROM notifications WHERE 1=1"
|
||||||
@@ -57,7 +57,7 @@ pub fn list(
|
|||||||
// report the total number of pages alongside a `list` page.
|
// report the total number of pages alongside a `list` page.
|
||||||
pub fn count(is_new: Option<bool>) -> Result<i64, DbError> {
|
pub fn count(is_new: Option<bool>) -> Result<i64, DbError> {
|
||||||
debug!("Counting notifications");
|
debug!("Counting notifications");
|
||||||
let conn = db::get_db_connection();
|
let conn = db::get_db_connection()?;
|
||||||
|
|
||||||
let mut sql_statement = "SELECT COUNT(*) FROM notifications WHERE 1=1".to_string();
|
let mut sql_statement = "SELECT COUNT(*) FROM notifications WHERE 1=1".to_string();
|
||||||
let mut params: Vec<rusqlite::types::Value> = Vec::new();
|
let mut params: Vec<rusqlite::types::Value> = Vec::new();
|
||||||
@@ -78,7 +78,7 @@ pub fn count(is_new: Option<bool>) -> Result<i64, DbError> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert(notification: Notification) -> Result<i64, DbError> {
|
pub fn insert(notification: Notification) -> Result<i64, DbError> {
|
||||||
let conn = db::get_db_connection();
|
let conn = db::get_db_connection()?;
|
||||||
let mac_address = notification.mac_address.as_deref().map(normalize_mac);
|
let mac_address = notification.mac_address.as_deref().map(normalize_mac);
|
||||||
|
|
||||||
match conn.execute(
|
match conn.execute(
|
||||||
@@ -96,7 +96,7 @@ pub fn insert(notification: Notification) -> Result<i64, DbError> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn mark_as_old(id: i64) -> Result<(), DbError> {
|
pub fn mark_as_old(id: i64) -> Result<(), DbError> {
|
||||||
let conn = db::get_db_connection();
|
let conn = db::get_db_connection()?;
|
||||||
|
|
||||||
match conn.execute("UPDATE notifications SET is_new=0 WHERE id=?1", params![id]) {
|
match conn.execute("UPDATE notifications SET is_new=0 WHERE id=?1", params![id]) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
@@ -111,7 +111,7 @@ pub fn mark_as_old(id: i64) -> Result<(), DbError> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn mark_as_new(id: i64) -> Result<(), DbError> {
|
pub fn mark_as_new(id: i64) -> Result<(), DbError> {
|
||||||
let conn = db::get_db_connection();
|
let conn = db::get_db_connection()?;
|
||||||
|
|
||||||
match conn.execute("UPDATE notifications SET is_new=1 WHERE id=?1", params![id]) {
|
match conn.execute("UPDATE notifications SET is_new=1 WHERE id=?1", params![id]) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
@@ -126,7 +126,7 @@ pub fn mark_as_new(id: i64) -> Result<(), DbError> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn mark_all_as_old() -> Result<(), DbError> {
|
pub fn mark_all_as_old() -> Result<(), DbError> {
|
||||||
let conn = db::get_db_connection();
|
let conn = db::get_db_connection()?;
|
||||||
|
|
||||||
match conn.execute("UPDATE notifications SET is_new=0 WHERE is_new=1", []) {
|
match conn.execute("UPDATE notifications SET is_new=0 WHERE is_new=1", []) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
@@ -141,7 +141,7 @@ pub fn mark_all_as_old() -> Result<(), DbError> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn purge_older_than(cutoff: DateTime<Utc>) -> Result<usize, DbError> {
|
pub fn purge_older_than(cutoff: DateTime<Utc>) -> Result<usize, DbError> {
|
||||||
let conn = db::get_db_connection();
|
let conn = db::get_db_connection()?;
|
||||||
|
|
||||||
match conn.execute(
|
match conn.execute(
|
||||||
"DELETE FROM notifications WHERE created_on < ?1",
|
"DELETE FROM notifications WHERE created_on < ?1",
|
||||||
@@ -159,7 +159,13 @@ pub fn purge_older_than(cutoff: DateTime<Utc>) -> Result<usize, DbError> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn read(id: i64) -> Option<Notification> {
|
pub fn read(id: i64) -> Option<Notification> {
|
||||||
let conn = db::get_db_connection();
|
let conn = match db::get_db_connection() {
|
||||||
|
Ok(conn) => conn,
|
||||||
|
Err(error) => {
|
||||||
|
error!("Error obtaining database connection: {error}");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let result: Result<Notification, rusqlite::Error> = conn.query_one(
|
let result: Result<Notification, rusqlite::Error> = conn.query_one(
|
||||||
"SELECT id, created_on, notification_type, title, body, is_new, mac_address FROM notifications WHERE id=?1",
|
"SELECT id, created_on, notification_type, title, body, is_new, mac_address FROM notifications WHERE id=?1",
|
||||||
|
|||||||
+105
-20
@@ -14,6 +14,82 @@ use crate::settings::get_settings;
|
|||||||
use chrono::{Local, Utc};
|
use chrono::{Local, Utc};
|
||||||
use duration_string::DurationString;
|
use duration_string::DurationString;
|
||||||
use log::{debug, error, info, warn};
|
use log::{debug, error, info, warn};
|
||||||
|
use once_cell::sync::OnceCell;
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
|
// A notification handed to the delivery loop. Delivery (a blocking Pushover HTTP call) runs on a
|
||||||
|
// dedicated task so a slow or unreachable Pushover can never stall the scan loops.
|
||||||
|
struct DeliveryRequest {
|
||||||
|
title: String,
|
||||||
|
body: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bounded so a stuck delivery loop cannot grow memory without limit; on overflow we drop and warn
|
||||||
|
// (delivery is best-effort, matching the "never stop the loop" policy in the scanner pipeline).
|
||||||
|
const DELIVERY_QUEUE_CAPACITY: usize = 100;
|
||||||
|
|
||||||
|
static DELIVERY_TX: OnceCell<mpsc::Sender<DeliveryRequest>> = OnceCell::new();
|
||||||
|
|
||||||
|
/// Owns the receiving end of the notification-delivery channel and delivers notifications off the
|
||||||
|
/// scan loop. Run this as its own task (see `main`); it returns only if the channel is closed.
|
||||||
|
pub async fn run_delivery() {
|
||||||
|
let (tx, mut rx) = mpsc::channel::<DeliveryRequest>(DELIVERY_QUEUE_CAPACITY);
|
||||||
|
if DELIVERY_TX.set(tx).is_err() {
|
||||||
|
error!("Notification delivery loop started more than once; ignoring");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
while let Some(request) = rx.recv().await {
|
||||||
|
deliver(request).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deliver a single notification according to the configured method. The Pushover call is blocking,
|
||||||
|
// so it runs on the blocking thread pool rather than the delivery task's async thread.
|
||||||
|
async fn deliver(request: DeliveryRequest) {
|
||||||
|
match get_settings().notifications.method.as_str() {
|
||||||
|
"pushover" => match &get_settings().notifications.pushover {
|
||||||
|
Some(config) => {
|
||||||
|
let config = config.clone();
|
||||||
|
let result = tokio::task::spawn_blocking(move || {
|
||||||
|
pushover::send_message(&config, request.title, request.body)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
match result {
|
||||||
|
Ok(Ok(())) => {}
|
||||||
|
Ok(Err(err)) => error!("Failed to deliver notification via Pushover: {err}"),
|
||||||
|
Err(err) => error!("Notification delivery task panicked: {err}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
error!(
|
||||||
|
"Notification method is 'pushover' but no [notifications.pushover] section is \
|
||||||
|
configured; cannot deliver notification."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
other => {
|
||||||
|
warn!("Notification method set to '{other}'. Set logs to 'info' to see notifications.");
|
||||||
|
info!("Notification: {}", request.body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hand a notification to the delivery loop. Never blocks: if the loop is not running (e.g. in
|
||||||
|
// tests) or its queue is full, the notification is logged and dropped rather than stalling the
|
||||||
|
// caller (a scan loop).
|
||||||
|
fn enqueue_delivery(title: String, body: String) {
|
||||||
|
match DELIVERY_TX.get() {
|
||||||
|
Some(tx) => {
|
||||||
|
if let Err(err) = tx.try_send(DeliveryRequest { title, body }) {
|
||||||
|
warn!("Notification delivery queue unavailable; dropping notification: {err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
info!("Notification (delivery loop not running): {body}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Device name for display in messages; falls back to "(unknown)" for devices with no
|
// Device name for display in messages; falls back to "(unknown)" for devices with no
|
||||||
// mDNS-discovered hostname (e.g. those found only via ARP).
|
// mDNS-discovered hostname (e.g. those found only via ARP).
|
||||||
@@ -177,30 +253,18 @@ fn render_device_changed(
|
|||||||
(title, body)
|
(title, body)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Private helper function to deliver messages
|
// Private helper function to record a notification and hand it off for delivery. Delivery happens
|
||||||
|
// on a separate task (see `run_delivery`), so this returns as soon as the notification is persisted
|
||||||
|
// and never blocks the caller on the (potentially slow) Pushover HTTP call.
|
||||||
fn send_notification(notification: Notification) -> Result<(), Box<dyn Error>> {
|
fn send_notification(notification: Notification) -> Result<(), Box<dyn Error>> {
|
||||||
debug!("About to record notification in database");
|
debug!("About to record notification in database");
|
||||||
db::notifications::insert(notification.clone())?;
|
|
||||||
|
|
||||||
debug!("About to send notification ({notification}).");
|
let title = notification.title.clone();
|
||||||
|
let body = notification.body.clone();
|
||||||
|
db::notifications::insert(notification)?;
|
||||||
|
|
||||||
match get_settings().notifications.method.as_str() {
|
debug!("Queued notification for delivery: {title}");
|
||||||
"pushover" => match &get_settings().notifications.pushover {
|
enqueue_delivery(title, body);
|
||||||
Some(pushover) => {
|
|
||||||
pushover::send_message(pushover, notification.title, notification.body)?;
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
error!(
|
|
||||||
"Notification method is 'pushover' but no [notifications.pushover] section is \
|
|
||||||
configured; cannot deliver notification."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
other => {
|
|
||||||
warn!("Notification method set to '{other}'. Set logs to 'info' to see notifications.");
|
|
||||||
info!("Notification: {}", notification.body);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -506,4 +570,25 @@ mod tests {
|
|||||||
"A sighting from a different scanner should be recorded"
|
"A sighting from a different scanner should be recorded"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn triggering_a_new_device_records_a_notification() {
|
||||||
|
crate::tests_common::setup().await;
|
||||||
|
|
||||||
|
let mac = "fa:ce:fa:ce:00:02".to_string();
|
||||||
|
let mut device = sample_device(Some("printer"));
|
||||||
|
device.mac_address = mac.clone();
|
||||||
|
|
||||||
|
// The delivery loop is not running in tests, so delivery is a no-op; the notification must
|
||||||
|
// still be persisted regardless of whether it is ever delivered.
|
||||||
|
trigger_new_device(device, DeviceEventScanner::Arp).unwrap();
|
||||||
|
|
||||||
|
let notifications = db::notifications::list(None, None, None).unwrap();
|
||||||
|
assert!(
|
||||||
|
notifications
|
||||||
|
.iter()
|
||||||
|
.any(|n| n.mac_address.as_deref() == Some(mac.as_str())),
|
||||||
|
"A new-device sighting should persist a notification"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-2
@@ -63,7 +63,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
scanners::dhcp::status::STATUS.init();
|
scanners::dhcp::status::STATUS.init();
|
||||||
scanners::snmp::status::STATUS.init();
|
scanners::snmp::status::STATUS.init();
|
||||||
|
|
||||||
// Start the device scanners, web server, and retention cleaner in parallel
|
// Start the device scanners, web server, retention cleaner, and notification delivery loop in
|
||||||
|
// parallel. Notification delivery runs on its own task so a slow Pushover never stalls a scan.
|
||||||
tokio::join!(
|
tokio::join!(
|
||||||
scanners::arp::scanner::scan(),
|
scanners::arp::scanner::scan(),
|
||||||
scanners::mdns::scanner::listen(),
|
scanners::mdns::scanner::listen(),
|
||||||
@@ -71,7 +72,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
scanners::dhcp::scanner::listen(),
|
scanners::dhcp::scanner::listen(),
|
||||||
scanners::snmp::scanner::scan(),
|
scanners::snmp::scanner::scan(),
|
||||||
web_server::serve(),
|
web_server::serve(),
|
||||||
retention::run()
|
retention::run(),
|
||||||
|
events::run_delivery()
|
||||||
)
|
)
|
||||||
.0
|
.0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ pub async fn setup() {
|
|||||||
// Initialize database
|
// Initialize database
|
||||||
db::init_db().await.unwrap();
|
db::init_db().await.unwrap();
|
||||||
info!("Initializing database for testing.");
|
info!("Initializing database for testing.");
|
||||||
let conn = db::get_db_connection();
|
let conn = db::get_db_connection().expect("failed to get a database connection for tests");
|
||||||
debug!("Running database setup scripts for testing.");
|
debug!("Running database setup scripts for testing.");
|
||||||
|
|
||||||
for entry in TEST_MIGRATIONS_DIR.entries() {
|
for entry in TEST_MIGRATIONS_DIR.entries() {
|
||||||
|
|||||||
@@ -33,11 +33,14 @@ pub async fn list(
|
|||||||
let page_offset: Option<i64> = utils::parse_parameter(¶ms, "page_offset");
|
let page_offset: Option<i64> = utils::parse_parameter(¶ms, "page_offset");
|
||||||
let page_limit: Option<i64> = utils::parse_parameter(¶ms, "page_limit");
|
let page_limit: Option<i64> = utils::parse_parameter(¶ms, "page_limit");
|
||||||
|
|
||||||
match db::device_events::list(Some(mac_address), created_from, page_offset, page_limit) {
|
db::run_blocking(move || {
|
||||||
Ok(value) => Ok(Json(value)),
|
match db::device_events::list(Some(mac_address), created_from, page_offset, page_limit) {
|
||||||
Err(err) => {
|
Ok(value) => Ok(Json(value)),
|
||||||
error!("Error listing device events: {}", err);
|
Err(err) => {
|
||||||
Err(StatusCode::INTERNAL_SERVER_ERROR)
|
error!("Error listing device events: {}", err);
|
||||||
|
Err(StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|||||||
+128
-114
@@ -52,41 +52,44 @@ pub async fn list(
|
|||||||
let page_offset: Option<i64> = utils::parse_parameter(¶ms, "page_offset");
|
let page_offset: Option<i64> = utils::parse_parameter(¶ms, "page_offset");
|
||||||
let page_limit: Option<i64> = utils::parse_parameter(¶ms, "page_limit");
|
let page_limit: Option<i64> = utils::parse_parameter(¶ms, "page_limit");
|
||||||
|
|
||||||
let items = match db::devices::list_devices(
|
db::run_blocking(move || {
|
||||||
is_registered,
|
let items = match db::devices::list_devices(
|
||||||
last_seen_from,
|
is_registered,
|
||||||
last_seen_to,
|
last_seen_from,
|
||||||
owner.clone(),
|
last_seen_to,
|
||||||
device_type.clone(),
|
owner.clone(),
|
||||||
vendor.clone(),
|
device_type.clone(),
|
||||||
sort_by,
|
vendor.clone(),
|
||||||
sort_order,
|
sort_by,
|
||||||
page_offset,
|
sort_order,
|
||||||
page_limit,
|
page_offset,
|
||||||
) {
|
page_limit,
|
||||||
Ok(value) => value,
|
) {
|
||||||
Err(err) => {
|
Ok(value) => value,
|
||||||
error!("Error listing devices: {}", err);
|
Err(err) => {
|
||||||
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
error!("Error listing devices: {}", err);
|
||||||
}
|
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
||||||
};
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let total_count = match db::devices::count_devices(
|
let total_count = match db::devices::count_devices(
|
||||||
is_registered,
|
is_registered,
|
||||||
last_seen_from,
|
last_seen_from,
|
||||||
last_seen_to,
|
last_seen_to,
|
||||||
owner,
|
owner,
|
||||||
device_type,
|
device_type,
|
||||||
vendor,
|
vendor,
|
||||||
) {
|
) {
|
||||||
Ok(value) => value,
|
Ok(value) => value,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("Error counting devices: {}", err);
|
error!("Error counting devices: {}", err);
|
||||||
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Json(DeviceListResponse { items, total_count }))
|
Ok(Json(DeviceListResponse { items, total_count }))
|
||||||
|
})
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
@@ -103,10 +106,11 @@ pub async fn list(
|
|||||||
security(("bearer_auth" = []))
|
security(("bearer_auth" = []))
|
||||||
)]
|
)]
|
||||||
pub async fn read(Path(mac_address): Path<String>) -> Result<Json<Device>, StatusCode> {
|
pub async fn read(Path(mac_address): Path<String>) -> Result<Json<Device>, StatusCode> {
|
||||||
match db::devices::read(mac_address) {
|
db::run_blocking(move || match db::devices::read(mac_address) {
|
||||||
Some(value) => Ok(Json(value)),
|
Some(value) => Ok(Json(value)),
|
||||||
None => Err(StatusCode::NOT_FOUND),
|
None => Err(StatusCode::NOT_FOUND),
|
||||||
}
|
})
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
@@ -128,38 +132,41 @@ pub async fn register(Json(payload): Json<RegisterDevicePayload>) -> impl IntoRe
|
|||||||
payload.mac_address, payload.owner, payload.device_type
|
payload.mac_address, payload.owner, payload.device_type
|
||||||
);
|
);
|
||||||
|
|
||||||
let device = match db::devices::read(payload.mac_address.clone()) {
|
db::run_blocking(move || {
|
||||||
Some(value) => value,
|
let device = match db::devices::read(payload.mac_address.clone()) {
|
||||||
None => {
|
Some(value) => value,
|
||||||
|
None => {
|
||||||
|
return (
|
||||||
|
axum::http::StatusCode::NOT_FOUND,
|
||||||
|
"Device not found or could not be read",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if device.is_registered {
|
||||||
return (
|
return (
|
||||||
axum::http::StatusCode::NOT_FOUND,
|
axum::http::StatusCode::CONFLICT,
|
||||||
"Device not found or could not be read",
|
"Device already registered",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
if device.is_registered {
|
match db::devices::register(
|
||||||
return (
|
payload.mac_address,
|
||||||
axum::http::StatusCode::CONFLICT,
|
payload.owner,
|
||||||
"Device already registered",
|
payload.device_type,
|
||||||
);
|
payload.name,
|
||||||
}
|
) {
|
||||||
|
Ok(_) => (axum::http::StatusCode::CREATED, "Device registered"),
|
||||||
match db::devices::register(
|
Err(err) => {
|
||||||
payload.mac_address,
|
error!("Error registering device in the database: {}", err);
|
||||||
payload.owner,
|
(
|
||||||
payload.device_type,
|
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
payload.name,
|
"Error registering device in the server, check your logs",
|
||||||
) {
|
)
|
||||||
Ok(_) => (axum::http::StatusCode::CREATED, "Device registered"),
|
}
|
||||||
Err(err) => {
|
|
||||||
error!("Error registering device in the database: {}", err);
|
|
||||||
(
|
|
||||||
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
|
|
||||||
"Error registering device in the server, check your logs",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
@@ -187,39 +194,42 @@ pub async fn update(
|
|||||||
mac_address, payload.owner, payload.device_type, payload.vendor, payload.name
|
mac_address, payload.owner, payload.device_type, payload.vendor, payload.name
|
||||||
);
|
);
|
||||||
|
|
||||||
let device = match db::devices::read(mac_address.clone()) {
|
db::run_blocking(move || {
|
||||||
Some(value) => value,
|
let device = match db::devices::read(mac_address.clone()) {
|
||||||
None => {
|
Some(value) => value,
|
||||||
|
None => {
|
||||||
|
return (
|
||||||
|
axum::http::StatusCode::NOT_FOUND,
|
||||||
|
"Device not found or could not be read",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if !device.is_registered {
|
||||||
return (
|
return (
|
||||||
axum::http::StatusCode::NOT_FOUND,
|
axum::http::StatusCode::CONFLICT,
|
||||||
"Device not found or could not be read",
|
"Device is not registered, register it before modifying",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
if !device.is_registered {
|
match db::devices::update(
|
||||||
return (
|
mac_address,
|
||||||
axum::http::StatusCode::CONFLICT,
|
payload.owner,
|
||||||
"Device is not registered, register it before modifying",
|
payload.device_type,
|
||||||
);
|
payload.vendor,
|
||||||
}
|
payload.name,
|
||||||
|
) {
|
||||||
match db::devices::update(
|
Ok(_) => (axum::http::StatusCode::OK, "Device updated"),
|
||||||
mac_address,
|
Err(err) => {
|
||||||
payload.owner,
|
error!("Error updating device in the database: {}", err);
|
||||||
payload.device_type,
|
(
|
||||||
payload.vendor,
|
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
payload.name,
|
"Error updating device in the server, check your logs",
|
||||||
) {
|
)
|
||||||
Ok(_) => (axum::http::StatusCode::OK, "Device updated"),
|
}
|
||||||
Err(err) => {
|
|
||||||
error!("Error updating device in the database: {}", err);
|
|
||||||
(
|
|
||||||
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
|
|
||||||
"Error updating device in the server, check your logs",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
@@ -238,33 +248,36 @@ pub async fn update(
|
|||||||
security(("bearer_auth" = []))
|
security(("bearer_auth" = []))
|
||||||
)]
|
)]
|
||||||
pub async fn unregister(Path(mac_address): Path<String>) -> impl IntoResponse {
|
pub async fn unregister(Path(mac_address): Path<String>) -> impl IntoResponse {
|
||||||
let device = match db::devices::read(mac_address.clone()) {
|
db::run_blocking(move || {
|
||||||
Some(value) => value,
|
let device = match db::devices::read(mac_address.clone()) {
|
||||||
None => {
|
Some(value) => value,
|
||||||
|
None => {
|
||||||
|
return (
|
||||||
|
axum::http::StatusCode::NOT_FOUND,
|
||||||
|
"Device not found or could not be read",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if !device.is_registered {
|
||||||
return (
|
return (
|
||||||
axum::http::StatusCode::NOT_FOUND,
|
axum::http::StatusCode::CONFLICT,
|
||||||
"Device not found or could not be read",
|
"Device not registered, you cannot un-register it again",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
if !device.is_registered {
|
match db::devices::unregister(mac_address) {
|
||||||
return (
|
Ok(_) => (axum::http::StatusCode::OK, "Device un-registered"),
|
||||||
axum::http::StatusCode::CONFLICT,
|
Err(err) => {
|
||||||
"Device not registered, you cannot un-register it again",
|
error!("Error updating device in the database: {}", err);
|
||||||
);
|
(
|
||||||
}
|
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
"Error updating device in the server, check your logs",
|
||||||
match db::devices::unregister(mac_address) {
|
)
|
||||||
Ok(_) => (axum::http::StatusCode::OK, "Device un-registered"),
|
}
|
||||||
Err(err) => {
|
|
||||||
error!("Error updating device in the database: {}", err);
|
|
||||||
(
|
|
||||||
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
|
|
||||||
"Error updating device in the server, check your logs",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
@@ -278,13 +291,14 @@ pub async fn unregister(Path(mac_address): Path<String>) -> impl IntoResponse {
|
|||||||
security(("bearer_auth" = []))
|
security(("bearer_auth" = []))
|
||||||
)]
|
)]
|
||||||
pub async fn summary() -> Result<Json<DeviceSummary>, StatusCode> {
|
pub async fn summary() -> Result<Json<DeviceSummary>, StatusCode> {
|
||||||
match db::devices::get_summary() {
|
db::run_blocking(move || match db::devices::get_summary() {
|
||||||
Ok(value) => Ok(Json(value)),
|
Ok(value) => Ok(Json(value)),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("Error getting device summary: {}", err);
|
error!("Error getting device summary: {}", err);
|
||||||
Err(StatusCode::INTERNAL_SERVER_ERROR)
|
Err(StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
// Payload structs
|
// Payload structs
|
||||||
|
|||||||
@@ -29,18 +29,21 @@ use crate::{
|
|||||||
security(("bearer_auth" = []))
|
security(("bearer_auth" = []))
|
||||||
)]
|
)]
|
||||||
pub async fn read(Path(id): Path<i64>) -> Result<Json<Notification>, StatusCode> {
|
pub async fn read(Path(id): Path<i64>) -> Result<Json<Notification>, StatusCode> {
|
||||||
match db::notifications::mark_as_old(id) {
|
db::run_blocking(move || {
|
||||||
Ok(_) => {}
|
match db::notifications::mark_as_old(id) {
|
||||||
Err(err) => {
|
Ok(_) => {}
|
||||||
error!("Error marking notification (id={id}) as old: {}", err);
|
Err(err) => {
|
||||||
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
error!("Error marking notification (id={id}) as old: {}", err);
|
||||||
}
|
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
||||||
};
|
}
|
||||||
|
};
|
||||||
|
|
||||||
match db::notifications::read(id) {
|
match db::notifications::read(id) {
|
||||||
Some(value) => Ok(Json(value)),
|
Some(value) => Ok(Json(value)),
|
||||||
None => Err(StatusCode::NOT_FOUND),
|
None => Err(StatusCode::NOT_FOUND),
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
@@ -57,10 +60,11 @@ pub async fn read(Path(id): Path<i64>) -> Result<Json<Notification>, StatusCode>
|
|||||||
security(("bearer_auth" = []))
|
security(("bearer_auth" = []))
|
||||||
)]
|
)]
|
||||||
pub async fn read_without_flagging(Path(id): Path<i64>) -> Result<Json<Notification>, StatusCode> {
|
pub async fn read_without_flagging(Path(id): Path<i64>) -> Result<Json<Notification>, StatusCode> {
|
||||||
match db::notifications::read(id) {
|
db::run_blocking(move || match db::notifications::read(id) {
|
||||||
Some(value) => Ok(Json(value)),
|
Some(value) => Ok(Json(value)),
|
||||||
None => Err(StatusCode::NOT_FOUND),
|
None => Err(StatusCode::NOT_FOUND),
|
||||||
}
|
})
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
@@ -77,7 +81,7 @@ pub async fn read_without_flagging(Path(id): Path<i64>) -> Result<Json<Notificat
|
|||||||
security(("bearer_auth" = []))
|
security(("bearer_auth" = []))
|
||||||
)]
|
)]
|
||||||
pub async fn mark_as_new(Path(id): Path<i64>) -> impl IntoResponse {
|
pub async fn mark_as_new(Path(id): Path<i64>) -> impl IntoResponse {
|
||||||
match db::notifications::mark_as_new(id) {
|
db::run_blocking(move || match db::notifications::mark_as_new(id) {
|
||||||
Ok(_) => (StatusCode::OK, "Notification marked as new"),
|
Ok(_) => (StatusCode::OK, "Notification marked as new"),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("Error marking notification (id={id}) as new: {}", err);
|
error!("Error marking notification (id={id}) as new: {}", err);
|
||||||
@@ -86,7 +90,8 @@ pub async fn mark_as_new(Path(id): Path<i64>) -> impl IntoResponse {
|
|||||||
"Error updating notification in the server, check your logs",
|
"Error updating notification in the server, check your logs",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
@@ -100,7 +105,7 @@ pub async fn mark_as_new(Path(id): Path<i64>) -> impl IntoResponse {
|
|||||||
security(("bearer_auth" = []))
|
security(("bearer_auth" = []))
|
||||||
)]
|
)]
|
||||||
pub async fn mark_all_as_old() -> impl IntoResponse {
|
pub async fn mark_all_as_old() -> impl IntoResponse {
|
||||||
match db::notifications::mark_all_as_old() {
|
db::run_blocking(move || match db::notifications::mark_all_as_old() {
|
||||||
Ok(_) => (StatusCode::OK, "All notifications marked as old"),
|
Ok(_) => (StatusCode::OK, "All notifications marked as old"),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("Error marking all notifications as old: {}", err);
|
error!("Error marking all notifications as old: {}", err);
|
||||||
@@ -109,7 +114,8 @@ pub async fn mark_all_as_old() -> impl IntoResponse {
|
|||||||
"Error updating notifications in the server, check your logs",
|
"Error updating notifications in the server, check your logs",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
@@ -134,21 +140,24 @@ pub async fn list(
|
|||||||
let page_offset: Option<i64> = utils::parse_parameter(¶ms, "page_offset");
|
let page_offset: Option<i64> = utils::parse_parameter(¶ms, "page_offset");
|
||||||
let page_limit: Option<i64> = utils::parse_parameter(¶ms, "page_limit");
|
let page_limit: Option<i64> = utils::parse_parameter(¶ms, "page_limit");
|
||||||
|
|
||||||
let items = match db::notifications::list(is_new, page_offset, page_limit) {
|
db::run_blocking(move || {
|
||||||
Ok(value) => value,
|
let items = match db::notifications::list(is_new, page_offset, page_limit) {
|
||||||
Err(err) => {
|
Ok(value) => value,
|
||||||
error!("Error listing notifications: {}", err);
|
Err(err) => {
|
||||||
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
error!("Error listing notifications: {}", err);
|
||||||
}
|
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
||||||
};
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let total_count = match db::notifications::count(is_new) {
|
let total_count = match db::notifications::count(is_new) {
|
||||||
Ok(value) => value,
|
Ok(value) => value,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("Error counting notifications: {}", err);
|
error!("Error counting notifications: {}", err);
|
||||||
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Json(NotificationListResponse { items, total_count }))
|
Ok(Json(NotificationListResponse { items, total_count }))
|
||||||
|
})
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user