mirror of
https://github.com/rzuasti/oott.git
synced 2026-07-08 19:21:54 +02:00
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>
115 lines
4.0 KiB
Rust
115 lines
4.0 KiB
Rust
pub mod device_events;
|
|
pub mod devices;
|
|
pub mod error;
|
|
pub mod notifications;
|
|
|
|
use include_dir::{Dir, include_dir};
|
|
use lazy_static::lazy_static;
|
|
use log::{debug, error};
|
|
use r2d2::PooledConnection;
|
|
use r2d2_sqlite::SqliteConnectionManager;
|
|
use rusqlite_migration::Migrations;
|
|
use tokio::sync::Mutex;
|
|
|
|
use crate::{db::error::DbError, settings::get_settings};
|
|
|
|
static MIGRATIONS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/database_migrations");
|
|
static INITIALISED: Mutex<bool> = Mutex::const_new(false);
|
|
|
|
lazy_static! {
|
|
// Define migrations. These are applied atomically.
|
|
static ref MIGRATIONS: Migrations<'static> =
|
|
Migrations::from_directory(&MIGRATIONS_DIR).unwrap();
|
|
|
|
// TODO : Move pool size to configuration file
|
|
static ref POOL: r2d2::Pool<SqliteConnectionManager> = r2d2::Pool::builder().
|
|
max_size(10).
|
|
build(
|
|
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();
|
|
}
|
|
|
|
pub fn get_db_connection() -> Result<PooledConnection<SqliteConnectionManager>, DbError> {
|
|
POOL.get().map_err(|error| {
|
|
error!("Error obtaining database connection from the pool: {error}");
|
|
DbError::from(error)
|
|
})
|
|
}
|
|
|
|
// Runs a blocking database operation on tokio's blocking thread pool so it never stalls an async
|
|
// worker thread. The DB layer uses synchronous `rusqlite`, so axum handlers must wrap their DB
|
|
// work in this rather than calling `db::*` functions inline.
|
|
pub async fn run_blocking<F, T>(f: F) -> T
|
|
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
|
|
// when both an offset and a limit are supplied. Used by the list endpoints (devices, notifications,
|
|
// device_events) so they page identically.
|
|
pub fn apply_paging(
|
|
sql: &mut String,
|
|
params: &mut Vec<rusqlite::types::Value>,
|
|
page_offset: Option<i64>,
|
|
page_limit: Option<i64>,
|
|
) {
|
|
if let (Some(page_offset), Some(page_limit)) = (page_offset, page_limit) {
|
|
debug!(
|
|
"Adding paging to list with offset={} and limit={}",
|
|
page_offset, page_limit
|
|
);
|
|
sql.push_str(" LIMIT ? OFFSET ?");
|
|
params.push(page_limit.into());
|
|
params.push(page_offset.into());
|
|
}
|
|
}
|
|
|
|
pub async fn init_db() -> Result<(), DbError> {
|
|
let mut initialised = INITIALISED.lock().await;
|
|
if *initialised {
|
|
return Ok(());
|
|
}
|
|
|
|
debug!("Getting database connection");
|
|
|
|
let mut conn = get_db_connection()?;
|
|
|
|
debug!("Executing database migrations if needed.");
|
|
|
|
let result = match MIGRATIONS.to_latest(&mut conn) {
|
|
Ok(_) => {
|
|
debug!("Database up to date.");
|
|
Ok(())
|
|
}
|
|
Err(error) => {
|
|
error!("Error updating database: {error}");
|
|
Err(DbError::from(error))
|
|
}
|
|
};
|
|
|
|
*initialised = true;
|
|
|
|
result
|
|
}
|