feat(all): 重炼 crates/common 核心组件、上线 Web 运维看板与 Docker 容器化部署
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
pub mod sqlite_queue;
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
use anyhow::{Context, Result};
|
||||
use common::models::TaskSpec;
|
||||
use r2d2::Pool;
|
||||
use r2d2_sqlite::SqliteConnectionManager;
|
||||
use rusqlite::params;
|
||||
use tracing::info;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct SqliteCustomizer;
|
||||
|
||||
impl r2d2::CustomizeConnection<rusqlite::Connection, rusqlite::Error> for SqliteCustomizer {
|
||||
fn on_acquire(&self, conn: &mut rusqlite::Connection) -> Result<(), rusqlite::Error> {
|
||||
conn.pragma_update(None, "busy_timeout", 5000)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SqliteTaskQueue {
|
||||
pool: Pool<SqliteConnectionManager>,
|
||||
}
|
||||
|
||||
impl SqliteTaskQueue {
|
||||
pub async fn new(db_path: &str) -> Result<Self> {
|
||||
let db_path_owned = db_path.to_string();
|
||||
let pool = tokio::task::spawn_blocking(move || -> Result<Pool<SqliteConnectionManager>> {
|
||||
if let Some(parent) = std::path::Path::new(&db_path_owned).parent() {
|
||||
let _ = std::fs::create_dir_all(parent);
|
||||
}
|
||||
let manager = SqliteConnectionManager::file(&db_path_owned);
|
||||
let pool = Pool::builder()
|
||||
.max_size(4)
|
||||
.connection_customizer(Box::new(SqliteCustomizer))
|
||||
.build(manager)
|
||||
.context("Failed to build SQLite queue connection pool")?;
|
||||
|
||||
let conn = pool.get()?;
|
||||
let _: String = conn.pragma_update_and_check(None, "journal_mode", "WAL", |r| r.get(0))?;
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS task_queue (
|
||||
task_id TEXT PRIMARY KEY,
|
||||
payload TEXT NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
created_at DATETIME NOT NULL,
|
||||
claimed_at DATETIME
|
||||
)",
|
||||
[],
|
||||
)?;
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_task_queue_status_created ON task_queue(status, created_at)",
|
||||
[],
|
||||
)?;
|
||||
Ok(pool)
|
||||
})
|
||||
.await??;
|
||||
|
||||
info!("成功初始化 SQLite 任务队列数据库连接池: {}", db_path);
|
||||
Ok(Self { pool })
|
||||
}
|
||||
|
||||
pub async fn push_task(&self, task: &TaskSpec) -> Result<()> {
|
||||
let payload = serde_json::to_string(task)?;
|
||||
let task_id_str = task.task_id.to_string();
|
||||
let pool = self.pool.clone();
|
||||
|
||||
tokio::task::spawn_blocking(move || -> Result<()> {
|
||||
let conn = pool.get().map_err(|e| anyhow::anyhow!("Queue DB pool error: {}", e))?;
|
||||
conn.execute(
|
||||
"INSERT OR REPLACE INTO task_queue (task_id, payload, status, created_at)
|
||||
VALUES (?1, ?2, 'pending', datetime('now'))",
|
||||
params![task_id_str, payload],
|
||||
)?;
|
||||
Ok(())
|
||||
})
|
||||
.await??;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn pop_task(&self) -> Result<Option<TaskSpec>> {
|
||||
let pool = self.pool.clone();
|
||||
|
||||
tokio::task::spawn_blocking(move || -> Result<Option<TaskSpec>> {
|
||||
let mut attempts = 0;
|
||||
loop {
|
||||
let mut conn = pool.get().map_err(|e| anyhow::anyhow!("Queue DB pool error: {}", e))?;
|
||||
let tx_res = conn.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate);
|
||||
match tx_res {
|
||||
Ok(tx) => {
|
||||
let mut stmt = tx.prepare(
|
||||
"SELECT task_id, payload FROM task_queue WHERE status = 'pending' ORDER BY created_at ASC LIMIT 1"
|
||||
)?;
|
||||
|
||||
let row = stmt.query_row([], |row| {
|
||||
let id: String = row.get(0)?;
|
||||
let payload: String = row.get(1)?;
|
||||
Ok((id, payload))
|
||||
});
|
||||
|
||||
drop(stmt);
|
||||
|
||||
let (task_id, payload) = match row {
|
||||
Ok(res) => res,
|
||||
Err(rusqlite::Error::QueryReturnedNoRows) => {
|
||||
return Ok(None);
|
||||
}
|
||||
Err(e) => return Err(e.into()),
|
||||
};
|
||||
|
||||
let task: TaskSpec = serde_json::from_str(&payload)?;
|
||||
|
||||
tx.execute(
|
||||
"UPDATE task_queue SET status = 'claimed', claimed_at = datetime('now') WHERE task_id = ?1",
|
||||
params![task_id],
|
||||
)?;
|
||||
|
||||
tx.commit()?;
|
||||
return Ok(Some(task));
|
||||
}
|
||||
Err(rusqlite::Error::SqliteFailure(err, _))
|
||||
if err.code == rusqlite::ErrorCode::DatabaseBusy
|
||||
|| err.code == rusqlite::ErrorCode::DatabaseLocked =>
|
||||
{
|
||||
attempts += 1;
|
||||
if attempts >= 5 {
|
||||
anyhow::bail!("Queue DB busy/locked after 5 retries: {}", err);
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(10 * (1 << attempts)));
|
||||
}
|
||||
Err(e) => return Err(e.into()),
|
||||
}
|
||||
}
|
||||
})
|
||||
.await?
|
||||
}
|
||||
|
||||
pub async fn remove_task(&self, task_id: &str) -> Result<()> {
|
||||
let pool = self.pool.clone();
|
||||
let id_owned = task_id.to_string();
|
||||
|
||||
tokio::task::spawn_blocking(move || -> Result<()> {
|
||||
let conn = pool.get().map_err(|e| anyhow::anyhow!("Queue DB pool error: {}", e))?;
|
||||
conn.execute("DELETE FROM task_queue WHERE task_id = ?1", params![id_owned])?;
|
||||
Ok(())
|
||||
})
|
||||
.await??;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn requeue_stale_tasks(&self, stale_sec: u64) -> Result<Vec<String>> {
|
||||
let pool = self.pool.clone();
|
||||
|
||||
let names = tokio::task::spawn_blocking(move || -> Result<Vec<String>> {
|
||||
let mut conn = pool.get().map_err(|e| anyhow::anyhow!("Queue DB pool error: {}", e))?;
|
||||
let tx = conn.transaction()?;
|
||||
let mut point_names = Vec::new();
|
||||
|
||||
{
|
||||
// 改写为单一原子更新带 RETURNING 返回语句,消弭 TOCTOU (Time-Of-Check-To-Time-Of-Use) 竞态问题
|
||||
let mut stmt = tx.prepare(
|
||||
"UPDATE task_queue SET status = 'pending', claimed_at = NULL
|
||||
WHERE status = 'claimed' AND strftime('%s', 'now') - strftime('%s', claimed_at) >= ?1
|
||||
RETURNING payload",
|
||||
)?;
|
||||
let rows = stmt.query_map(params![stale_sec as i64], |row| row.get::<_, String>(0))?;
|
||||
for r in rows {
|
||||
if let Ok(payload) = r {
|
||||
if let Ok(task) = serde_json::from_str::<TaskSpec>(&payload) {
|
||||
point_names.push(task.point_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tx.commit()?;
|
||||
Ok(point_names)
|
||||
})
|
||||
.await??;
|
||||
|
||||
Ok(names)
|
||||
}
|
||||
|
||||
pub async fn clear_queue(&self) -> Result<()> {
|
||||
let pool = self.pool.clone();
|
||||
tokio::task::spawn_blocking(move || -> Result<()> {
|
||||
let conn = pool.get().map_err(|e| anyhow::anyhow!("Queue DB pool error: {}", e))?;
|
||||
conn.execute("DELETE FROM task_queue", [])?;
|
||||
Ok(())
|
||||
})
|
||||
.await??;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use common::models::{GridPointParams, TaskType};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_sqlite_task_queue_operations() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let db_path = temp_dir.path().join("test_queue.db");
|
||||
let queue = SqliteTaskQueue::new(&db_path.to_string_lossy()).await.unwrap();
|
||||
|
||||
assert!(queue.pop_task().await.unwrap().is_none());
|
||||
|
||||
let task_id = Uuid::new_v4();
|
||||
let task = TaskSpec {
|
||||
task_id,
|
||||
point_name: "t35000_g5.5_he-1_c-2_n-2_o-2".to_string(),
|
||||
params: GridPointParams {
|
||||
teff: 35000.0,
|
||||
logg: 5.5,
|
||||
loghe: -1.0,
|
||||
logc: -2.0,
|
||||
logn: -2.0,
|
||||
logo: -2.0,
|
||||
},
|
||||
task_type: TaskType::ColdRun,
|
||||
seed_point_name: None,
|
||||
timeout_sec: 3600,
|
||||
};
|
||||
queue.push_task(&task).await.unwrap();
|
||||
|
||||
let popped = queue.pop_task().await.unwrap();
|
||||
assert!(popped.is_some());
|
||||
let popped_task = popped.unwrap();
|
||||
assert_eq!(popped_task.task_id, task_id);
|
||||
assert_eq!(popped_task.point_name, task.point_name);
|
||||
|
||||
assert!(queue.pop_task().await.unwrap().is_none());
|
||||
|
||||
let requeued = queue.requeue_stale_tasks(0).await.unwrap();
|
||||
assert_eq!(requeued.len(), 1);
|
||||
|
||||
let popped2 = queue.pop_task().await.unwrap();
|
||||
assert!(popped2.is_some());
|
||||
|
||||
queue.remove_task(&task_id.to_string()).await.unwrap();
|
||||
assert!(queue.pop_task().await.unwrap().is_none());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user