Release v1.0.0 candidate

This commit is contained in:
Accusys
2026-05-08 00:48:15 +08:00
parent 26d9c33419
commit 573714788f
17 changed files with 5040 additions and 895 deletions

View File

@@ -1,10 +1,37 @@
use anyhow::{Context, Result};
use libc;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use tokio::sync::{mpsc, RwLock};
use tracing::{error, info, warn};
/// Guard that ensures processor pool cleanup runs even if the task panics.
struct ProcessorCleanupGuard {
job_id: i32,
running: Arc<RwLock<HashMap<i32, ProcessorHandle>>>,
running_count: Arc<RwLock<usize>>,
}
impl Drop for ProcessorCleanupGuard {
fn drop(&mut self) {
use tokio::sync::TryLockError;
// 嘗試同步清理;若 lock 被佔用則跳過(避免 deadlock
if let Ok(mut guard) = self.running.try_write() {
guard.remove(&self.job_id);
} else {
warn!("[ProcessorCleanupGuard] running lock contended, skipping cleanup");
}
if let Ok(mut guard) = self.running_count.try_write() {
if *guard > 0 {
*guard -= 1;
}
} else {
warn!("[ProcessorCleanupGuard] running_count lock contended, skipping cleanup");
}
}
}
use crate::core::config::{OUTPUT_DIR, PYTHON_PATH, SCRIPTS_DIR};
use crate::core::db::{
MonitorJob, PostgresDb, ProcessorJobStatus, ProcessorType, QdrantDb, RedisClient,
@@ -93,7 +120,7 @@ impl ProcessorPool {
if handle_count == 0 && count == 0 {
if let Err(e) = self
.db
.reset_stale_processor_results(ProcessorJobStatus::Failed, "Worker restarted")
.reset_stale_processor_results(ProcessorJobStatus::Pending, "Worker restarted")
.await
{
error!("Failed to reset stale processor results: {}", e);
@@ -101,7 +128,29 @@ impl ProcessorPool {
}
}
async fn kill_existing_processor(redis: &RedisClient, uuid: &str, processor: &str) {
let prefix = crate::core::config::REDIS_KEY_PREFIX.as_str();
let key = format!("{}worker:job:{}:processor:{}", prefix, uuid, processor);
if let Ok(mut conn) = redis.get_conn().await {
let old_pid: Option<i32> = redis::cmd("HGET")
.arg(&key)
.arg("pid")
.query_async(&mut conn)
.await
.ok()
.flatten();
if let Some(pid) = old_pid {
if pid > 0 {
warn!("[PID] Killing existing process {} for {}/{}", pid, uuid, processor);
unsafe { libc::kill(pid, libc::SIGKILL); }
}
}
}
}
pub async fn start_processor(&self, task: ProcessorTask) -> Result<()> {
Self::kill_existing_processor(&*self.redis, &task.job.uuid, task.processor_type.as_str()).await;
let (cancel_tx, cancel_rx) = mpsc::channel(1);
let job_id = task.job.id;
let processor_type = task.processor_type;
@@ -144,6 +193,13 @@ impl ProcessorPool {
}
tokio::spawn(async move {
// Guard 的 Drop 確保 panic 時也清理 pool state
let _guard = ProcessorCleanupGuard {
job_id,
running: running.clone(),
running_count: running_count.clone(),
};
info!("Starting processor {} for job {}", processor_name, job.uuid);
let _ = db
@@ -171,19 +227,6 @@ impl ProcessorPool {
let result = Self::run_processor(&db, &redis, &job, processor_type, cancel_rx).await;
// Store child PID for stability
{
let mut pid_lock = child_pid.write().await;
*pid_lock = Some(0);
}
{
let mut running_guard = running.write().await;
running_guard.remove(&job_id);
let mut count_guard = running_count.write().await;
*count_guard -= 1;
}
match result {
Ok(output) => {
info!(
@@ -747,6 +790,12 @@ impl ProcessorPool {
"_face"
);
// 確保 collection 存在dim=512 for FaceNet
if let Err(e) = qdrant.ensure_collection(&collection, 512).await {
tracing::error!("Failed to ensure Qdrant face collection: {}", e);
return Ok(());
}
let mut count = 0;
for frame in &face_result.frames {
for face in &frame.faces {
@@ -807,6 +856,12 @@ impl ProcessorPool {
"_voice"
);
// 確保 collection 存在dim=192 for ASRX voice
if let Err(e) = qdrant.ensure_collection(&collection, 192).await {
tracing::error!("Failed to ensure Qdrant voice collection: {}", e);
return Ok(());
}
let embeddings = match &asrx_result.embeddings {
Some(e) => e,
None => return Ok(()),
@@ -958,6 +1013,24 @@ impl ProcessorPool {
db.store_scene_pre_chunks_batch(uuid, &scenes).await?;
for (i, scene) in scene_result.scenes.iter().enumerate() {
let chk_id = format!("scene_{}", i + 1);
let meta = serde_json::json!({
"scene_type": scene.scene_type,
"scene_type_zh": scene.scene_type_zh,
"confidence": scene.confidence,
"top_5": scene.top_5,
});
let _ = sqlx::query(
"UPDATE dev.chunks SET metadata = metadata || $1::jsonb WHERE file_uuid=$2 AND chunk_id=$3"
)
.bind(&meta)
.bind(uuid)
.bind(&chk_id)
.execute(db.pool())
.await;
}
tracing::info!(
"Stored {} Scene pre-chunks for video {}",
scenes.len(),