minor bugs

This commit is contained in:
2026-06-23 20:17:32 +05:30
parent c81e503af0
commit bf019f86c3
6 changed files with 133 additions and 60 deletions

View File

@@ -72,15 +72,27 @@ pub async fn ingest_project(db: &PgPool, project_id: i32, dir: &Path) -> anyhow:
// Never clobber a verified push (Phase 3): a worker's pushed annotations +
// completer/verify-time are authoritative, so skip re-ingesting this video.
let existing_status: Option<String> =
sqlx::query_scalar("SELECT status FROM videos WHERE project_id=$1 AND rel_path=$2")
.bind(project_id)
.bind(&rel_path)
.fetch_optional(db)
.await?;
if existing_status.as_deref() == Some("verified") {
tracing::debug!("skip re-ingest of verified video: {rel_path}");
continue;
// Also never clobber a video that is *actively claimed* (a worker is mid-edit)
// — a stale NAS JSON must not overwrite in-progress work. Both checks read the
// current row's status + claim lease.
let existing: Option<(String, Option<String>, Option<DateTime<Utc>>)> = sqlx::query_as(
"SELECT status, claimed_by, lease_expires_at FROM videos WHERE project_id=$1 AND rel_path=$2",
)
.bind(project_id)
.bind(&rel_path)
.fetch_optional(db)
.await?;
if let Some((status, claimed_by, lease)) = existing.as_ref() {
if status == "verified" {
tracing::debug!("skip re-ingest of verified video: {rel_path}");
continue;
}
let active_claim = claimed_by.is_some()
&& lease.map(|l| l > Utc::now()).unwrap_or(false);
if active_claim {
tracing::debug!("skip re-ingest of actively-claimed video: {rel_path}");
continue;
}
}
let json_path = sibling_json(path);
@@ -138,6 +150,10 @@ pub async fn ingest_project(db: &PgPool, project_id: i32, dir: &Path) -> anyhow:
std::fs::read_to_string(jp).ok().and_then(|t| serde_json::from_str(&t).ok())
});
// Upsert the video + replace its annotation rows atomically: either the new
// metadata AND the new annotation set both land, or neither does. Prevents a
// failed insert mid-loop from leaving the video with partial/zero annotations.
let mut tx = db.begin().await?;
// Upsert the video. Keep 'verified' status sticky (Phase 3 pushes).
let video_id: i32 = sqlx::query_scalar(
r#"
@@ -179,18 +195,19 @@ pub async fn ingest_project(db: &PgPool, project_id: i32, dir: &Path) -> anyhow:
.bind(latest)
.bind(raw_json)
.bind(ann_count)
.fetch_one(db)
.fetch_one(&mut *tx)
.await?;
// Replace detailed annotation rows for this video.
// Replace detailed annotation rows for this video (same transaction).
if let Some(d) = doc.as_ref() {
replace_annotations(db, video_id, d).await?;
replace_annotations(&mut tx, video_id, d).await?;
} else {
sqlx::query("DELETE FROM annotations WHERE video_id=$1")
.bind(video_id)
.execute(db)
.execute(&mut *tx)
.await?;
}
tx.commit().await?;
count += 1;
}
Ok(count)
@@ -199,27 +216,33 @@ pub async fn ingest_project(db: &PgPool, project_id: i32, dir: &Path) -> anyhow:
/// Replace all annotation rows for a video with the contents of an export doc.
/// Shared by ingest (NAS JSON) and the Phase 3 worker push. Returns the row count
/// written (each range annotation contributes 12 rows: start, and end if present).
///
/// Runs on a caller-supplied transaction connection so the delete + all inserts are
/// **atomic** — either every annotation row lands or none do. Callers (`ingest`, worker
/// `push`) own the transaction so the surrounding metadata update commits together with
/// the annotation set. A failed insert mid-loop rolls back the delete, so annotations
/// can never be partially lost.
pub async fn replace_annotations(
db: &PgPool,
conn: &mut sqlx::PgConnection,
video_id: i32,
d: &ExportDoc,
) -> anyhow::Result<i32> {
sqlx::query("DELETE FROM annotations WHERE video_id=$1")
.bind(video_id)
.execute(db)
.execute(&mut *conn)
.await?;
let mut written = 0i32;
for a in &d.fixed_annotations {
insert_ann(db, video_id, a.frame_number, &a.label, &a.side, &a.shape_type,
insert_ann(&mut *conn, video_id, a.frame_number, &a.label, &a.side, &a.shape_type,
&a.vertices, &a.annotated_by, &a.review_status, &a.remark, &a.subclass, &a.created_at).await?;
written += 1;
}
for a in &d.range_annotations {
insert_ann(db, video_id, a.start_frame, &a.label, &a.side, &a.shape_type,
insert_ann(&mut *conn, video_id, a.start_frame, &a.label, &a.side, &a.shape_type,
&a.start_vertices, &a.annotated_by, &a.review_status, &a.remark, &a.subclass, &a.created_at).await?;
written += 1;
if let (Some(ef), Some(ev)) = (a.end_frame, a.end_vertices.as_ref()) {
insert_ann(db, video_id, ef, &a.label, &a.side, &a.shape_type,
insert_ann(&mut *conn, video_id, ef, &a.label, &a.side, &a.shape_type,
ev, &a.annotated_by, &a.review_status, &a.remark, &a.subclass, &a.created_at).await?;
written += 1;
}
@@ -229,7 +252,7 @@ pub async fn replace_annotations(
#[allow(clippy::too_many_arguments)]
async fn insert_ann(
db: &PgPool, video_id: i32, frame: i64, label: &str, side: &str, shape: &str,
conn: &mut sqlx::PgConnection, video_id: i32, frame: i64, label: &str, side: &str, shape: &str,
vertices: &serde_json::Value, by: &str, review: &str, remark: &str, subclass: &str, created_at: &str,
) -> anyhow::Result<()> {
sqlx::query(
@@ -248,7 +271,7 @@ async fn insert_ann(
.bind(remark)
.bind(subclass)
.bind(created_at)
.execute(db)
.execute(&mut *conn)
.await?;
Ok(())
}