minor bugs
This commit is contained in:
@@ -218,20 +218,27 @@ pub async fn push(
|
||||
AxPath(id): AxPath<i32>,
|
||||
Json(body): Json<Value>,
|
||||
) -> ApiResult<Value> {
|
||||
// The current holder (or an admin) may push. A null holder (auto-released, not
|
||||
// re-claimed) is tolerated; a different holder is rejected.
|
||||
let claimed_by: Option<Option<String>> =
|
||||
sqlx::query_scalar("SELECT claimed_by FROM videos WHERE id=$1")
|
||||
// A non-admin pusher must currently hold an ACTIVE claim on this video. This blocks
|
||||
// a stale/expired claim from overwriting newer authoritative server state: if the
|
||||
// lease lapsed (auto-released, or re-claimed by someone else) the worker must
|
||||
// re-claim before pushing — their local work is never lost, it just can't clobber a
|
||||
// claim they no longer hold. Admins may always push.
|
||||
let row: Option<(Option<String>, Option<chrono::DateTime<chrono::Utc>>)> =
|
||||
sqlx::query_as("SELECT claimed_by, lease_expires_at FROM videos WHERE id=$1")
|
||||
.bind(id)
|
||||
.fetch_optional(&s.db)
|
||||
.await
|
||||
.map_err(err)?;
|
||||
let claimed_by = claimed_by.ok_or((StatusCode::NOT_FOUND, "video not found".to_string()))?;
|
||||
let (claimed_by, lease_expires_at) =
|
||||
row.ok_or((StatusCode::NOT_FOUND, "video not found".to_string()))?;
|
||||
if !user.is_admin() {
|
||||
if let Some(holder) = claimed_by.as_deref() {
|
||||
if holder != user.username {
|
||||
return Err((StatusCode::CONFLICT, "claim is held by another worker".into()));
|
||||
}
|
||||
let holds_active_claim = claimed_by.as_deref() == Some(user.username.as_str())
|
||||
&& lease_expires_at.map(|l| l > chrono::Utc::now()).unwrap_or(false);
|
||||
if !holds_active_claim {
|
||||
return Err((
|
||||
StatusCode::CONFLICT,
|
||||
"your claim has expired or is held by someone else — re-claim the video before pushing".into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,9 +250,6 @@ pub async fn push(
|
||||
let doc: ExportDoc = serde_json::from_value(body.clone())
|
||||
.map_err(|e| (StatusCode::BAD_REQUEST, format!("invalid export doc: {e}")))?;
|
||||
|
||||
crate::ingest::replace_annotations(&s.db, id, &doc)
|
||||
.await
|
||||
.map_err(err)?;
|
||||
let ann_count = (doc.fixed_annotations.len() + doc.range_annotations.len()) as i32;
|
||||
|
||||
// Recompute the video-level annotator fields from the pushed doc (same idea as
|
||||
@@ -261,6 +265,15 @@ pub async fn push(
|
||||
let primary_annotator = freq.into_iter().max_by_key(|(_, c)| *c).map(|(u, _)| u).unwrap_or_default();
|
||||
let ann_time_ms = doc.video.as_ref().map(|v| v.annotation_time_ms).unwrap_or(0);
|
||||
|
||||
// Replace the annotation set AND flip the video to verified in ONE transaction:
|
||||
// either the pushed annotations + the verified status both commit, or neither does.
|
||||
// This makes a push all-or-nothing — a verifier can never end up "verified" with a
|
||||
// half-written annotation set, and a failed push leaves the prior pass intact.
|
||||
let mut tx = s.db.begin().await.map_err(err)?;
|
||||
crate::ingest::replace_annotations(&mut tx, id, &doc)
|
||||
.await
|
||||
.map_err(err)?;
|
||||
|
||||
// Accumulate verify_time_ms across passes (re-verification adds up). The list
|
||||
// of verifiers + the pass count are derived from video_events (each push logs one).
|
||||
sqlx::query(
|
||||
@@ -281,9 +294,10 @@ pub async fn push(
|
||||
.bind(&primary_annotator)
|
||||
.bind(ann_time_ms)
|
||||
.bind(imported_count)
|
||||
.execute(&s.db)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(err)?;
|
||||
tx.commit().await.map_err(err)?;
|
||||
|
||||
log_event(&s.db, id, &user.username, "push", json!({ "verify_time_ms": verify_time_ms })).await;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user