Skip to content

Commit dcac0e5

Browse files
committed
fix(transaction): use non-panicking elapsed checks in the reaper
The reaper captures `now` once per pass, then snapshots and rechecks the live registry. A transaction created, claimed, or rolled back after that capture can have a created_at/claimed_at/dormant_since later than `now`. std::time::Instant::duration_since panics when its argument is later than self (the current toolchain returns 0 instead, but that is undocumented behavior), which would permanently terminate the one-shot reaper task and let abandoned objects exhaust the live transaction quota. Use saturating_duration_since so a timestamp newer than now is treated as zero elapsed: a freshly created/claimed/rolled-back transaction is never expired on the current pass. Same fix in claim_expired. Adds regression coverage for timestamps newer than now. All 37 unit tests pass in debug and release.
1 parent 1ca412b commit dcac0e5

2 files changed

Lines changed: 58 additions & 2 deletions

File tree

src/transaction/live.rs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -303,8 +303,15 @@ pub(crate) async fn remove_for_destroy(
303303
/// 一次回滚的时刻)为基准,未记录时退回 `created_at`。回滚会重置
304304
/// dormant_since,保证"授权/入队失败后回到休眠"的对象获得完整的重试
305305
/// 窗口,而不是在下一个清扫周期被立即回收。
306+
///
307+
/// 显式用 `saturating_duration_since` 而非 `duration_since`:reaper 捕获
308+
/// `now` 之后、快照/复查之前并发创建/回滚会写入晚于 `now` 的
309+
/// `created_at`/`dormant_since`。标准库文档承诺 `duration_since` 在参数
310+
/// 晚于 self 时 panic(当前工具链实测返回 0,但那是未文档化的实现细节,
311+
/// 不应依赖);显式 saturating 语义明确(晚于 `now` → Duration::ZERO →
312+
/// 刚创建/回滚的对象不过期、不回收)且跨 Rust 版本稳定。
306313
pub(crate) fn dormant_expired(dormant_since: Option<Instant>, created_at: Instant, now: Instant) -> bool {
307-
now.duration_since(dormant_since.unwrap_or(created_at)) >= DORMANT_TIMEOUT
314+
now.saturating_duration_since(dormant_since.unwrap_or(created_at)) >= DORMANT_TIMEOUT
308315
}
309316

310317
/// 周期清扫超时的事务对象,释放配额槽位。覆盖创建者断开或放弃对象
@@ -483,7 +490,13 @@ pub(crate) async fn claim_expired(
483490
// 已入队/运行中:不回收。
484491
return false;
485492
}
486-
if now.duration_since(claimed_at) >= CLAIM_TIMEOUT {
493+
// 显式用 saturating 而非 duration_since:reaper 捕获 now 之后、快照前
494+
// 并发 begin 声明会写入晚于 now 的 claimed_at。标准库文档承诺
495+
// duration_since 在参数晚于 self 时 panic(当前工具链实测返回 0,但
496+
// 那是未文档化的实现细节,不应依赖);显式 saturating 语义明确
497+
// (晚于 now → Duration::ZERO → 刚 claim 的不算超时、不回收)且跨
498+
// Rust 版本稳定。
499+
if now.saturating_duration_since(claimed_at) >= CLAIM_TIMEOUT {
487500
// claim 超时:无论创建者是否还在线,都视为放弃。
488501
return true;
489502
}

src/transaction/tests/live.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,18 @@ fn dormant_expired_uses_reset_baseline() {
459459
// 重置后再次超过 DORMANT_TIMEOUT:判 stale。
460460
let stale_reset = now - DORMANT_TIMEOUT - Duration::from_secs(1);
461461
assert!(dormant_expired(Some(stale_reset), created, now));
462+
463+
// 回归:时间戳晚于 now(reaper 捕获 now 后并发创建/回滚)不得 panic,
464+
// 且按"刚创建/回滚"处理——不过期、不回收。
465+
let future = now + Duration::from_secs(1);
466+
assert!(
467+
!dormant_expired(Some(future), created, now),
468+
"dormant_since newer than now must not be expired"
469+
);
470+
assert!(
471+
!dormant_expired(None, future, now),
472+
"created_at newer than now must not be expired"
473+
);
462474
}
463475

464476
/// 代际校验:Cancel 回滚旧 claim 后用户 re-trigger(新 claim、新代际),
@@ -631,6 +643,37 @@ async fn claim_expired_requires_dead_sender_or_timeout() {
631643
.await,
632644
"enqueued-but-not-cleaned-up transaction must not be reclaimed"
633645
);
646+
647+
// 回归:claimed_at 晚于 now(reaper 捕获 now 后并发 begin 声明)不得
648+
// panic。saturating 把时间差视为 0,不走 CLAIM_TIMEOUT 分支;死 sender
649+
// 仍走 sender 检查被回收(合理:claim 无法完成),活 sender + 刚 claim
650+
// 则两个条件都不满足、不回收。
651+
assert!(
652+
claim_expired(
653+
&mgr,
654+
&dbus,
655+
false,
656+
8,
657+
":1.999999999",
658+
now + Duration::from_secs(1),
659+
now,
660+
)
661+
.await,
662+
"dead sender must be reclaimed even with future claimed_at (and must not panic)"
663+
);
664+
assert!(
665+
!claim_expired(
666+
&mgr,
667+
&dbus,
668+
false,
669+
8,
670+
self_name.as_str(),
671+
now + Duration::from_secs(1),
672+
now,
673+
)
674+
.await,
675+
"live sender + future claimed_at must not be reclaimed (and must not panic)"
676+
);
634677
let _ = release_tx.send(());
635678
}
636679

0 commit comments

Comments
 (0)