Release v1.0.0 candidate
This commit is contained in:
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user