fix(queue): gate post-send side effects on the row's lease

A handler whose row was re-leased while it sat between heartbeats (a
>=120 s stall) still ran the channel forward, opened the edit prompt and
released the keep-alive temp media the new holder was using; only the
terminal write was token-guarded, which left the side effects to run
twice. The lease is now exposed to the handler on a task-local
(Lease/lease_still_held) and the completion path re-reads it, since the
loss may have gone unnoticed by the heartbeat. The forward retry is gated
the same way.
This commit is contained in:
2026-10-14 22:10:36 +08:00
parent 632cbb7c89
commit 3b955cf321
4 changed files with 243 additions and 20 deletions
+38 -17
View File
@@ -480,28 +480,49 @@ pub(crate) async fn handle_task(
// whole sequence (every batch) completed, so the channel forward
// and the edit-before-forward prompt must not be lost just
// because the send needed a retry.
//
// Unless the row was re-leased while this attempt sat between
// heartbeats (a ≥120 s stall): the new holder runs the same
// effects, so running them here too would copy to the channel a
// second time and open a second prompt. The keep-alive release is
// skipped with them — the payload (and so its temp media) is the
// same one the new holder is using.
if !crate::queue::lease_still_held().await {
log::warn!(
"queue: row re-leased before post-send actions; leaving the forward and the edit prompt to the new holder"
);
return Ok(());
}
post_send_actions(ctx, &task, message_ids).await;
settle_task(ctx, &task, Settled::Sent).await;
Ok(())
}
Task::ForwardMessages { .. } => match forward_messages(ctx, &task).await {
Ok(()) => Ok(()),
Err(SendError::Retryable {
delay_seconds,
task,
}) => Err(QueueError::Retryable {
delay_seconds,
payload: serde_json::to_value(task).expect("task serializes"),
}),
Err(SendError::Permanent { message, task }) => {
// Settled by `dead_letter_notify`, which the queue invokes for
// this payload.
Err(QueueError::Permanent {
message,
payload: serde_json::to_value(task).expect("task serializes"),
})
Task::ForwardMessages { .. } => {
// The copy *is* the side effect here, so it is the thing to gate:
// a re-leased row is already being copied by its new holder.
if !crate::queue::lease_still_held().await {
log::warn!("queue: row re-leased before the forward; leaving it to the new holder");
return Ok(());
}
},
match forward_messages(ctx, &task).await {
Ok(()) => Ok(()),
Err(SendError::Retryable {
delay_seconds,
task,
}) => Err(QueueError::Retryable {
delay_seconds,
payload: serde_json::to_value(task).expect("task serializes"),
}),
Err(SendError::Permanent { message, task }) => {
// Settled by `dead_letter_notify`, which the queue invokes
// for this payload.
Err(QueueError::Permanent {
message,
payload: serde_json::to_value(task).expect("task serializes"),
})
}
}
}
}
}