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:
rzuasti
2026-06-06 09:30:15 -04:00
co-authored by Claude Opus 4.8
parent 8749ff33c1
commit 15bbb3bda9
12 changed files with 372 additions and 207 deletions
+2
View File
@@ -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
View File
@@ -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.");
+11 -5
View File
@@ -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",
+15 -9
View File
@@ -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(
+8
View File
@@ -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)
+14 -8
View File
@@ -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
View File
@@ -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
View File
@@ -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
} }
+1 -1
View File
@@ -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() {
+9 -6
View File
@@ -33,11 +33,14 @@ pub async fn list(
let page_offset: Option<i64> = utils::parse_parameter(&params, "page_offset"); let page_offset: Option<i64> = utils::parse_parameter(&params, "page_offset");
let page_limit: Option<i64> = utils::parse_parameter(&params, "page_limit"); let page_limit: Option<i64> = utils::parse_parameter(&params, "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
View File
@@ -52,41 +52,44 @@ pub async fn list(
let page_offset: Option<i64> = utils::parse_parameter(&params, "page_offset"); let page_offset: Option<i64> = utils::parse_parameter(&params, "page_offset");
let page_limit: Option<i64> = utils::parse_parameter(&params, "page_limit"); let page_limit: Option<i64> = utils::parse_parameter(&params, "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
+41 -32
View File
@@ -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(&params, "page_offset"); let page_offset: Option<i64> = utils::parse_parameter(&params, "page_offset");
let page_limit: Option<i64> = utils::parse_parameter(&params, "page_limit"); let page_limit: Option<i64> = utils::parse_parameter(&params, "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
} }