Worker abandons work items on retryable ack failures, causing redundant activity re-execution
维护者通常 1 天内回复
评估
- 难度
- 4/5
- 预计耗时
- 3-5 天
- 新手友好度
- 68/100
- Issue 类型
- 缺陷
- 描述清晰度
- 描述清楚
- 活跃度
- 冷清
- 技术栈
- rust, sqlite
- 领域
- backend, databases, testing-qa
调研方向
从 src/runtime/dispatchers/worker.rs 中的 handle_activity_outcome 开始,将其 ack 错误处理与 fetch_work_item 轮询循环进行比较。然后检查 tests/session_e2e_tests.rs,并运行指定的 multi-worker 测试,包括提供的重复执行命令。当可重试的 ack 失败在放弃前进行退避、永久性失败仍然会放弃,并且测试符合文档记录的 at-least-once 行为时,即表示完成。
由索引模型根据 Issue 内容生成。
描述
Summary
A retryable ack_work_item failure causes the worker dispatcher to abandon the work item, which redelivers the activity and runs the handler again. The dispatcher poll loop already retries retryable provider errors on fetch_work_item; the ack path does not, and abandons immediately regardless of the error's retry classification.
This surfaces as a flaky test — session_e2e_tests::test_multi_worker_heterogeneous_config — but the flake is only the symptom. There are two distinct defects.
Defect 1 — worker abandons on retryable ack failures
src/runtime/dispatchers/worker.rs handle_activity_outcome abandons on any ack error:
Err(e) => {
warn!(..., "worker: atomic ack failed, abandoning work item");
let _ = rt.history_store
.abandon_work_item(&ctx.lock_token, Some(Duration::from_millis(100)), false)
.await;
rt.record_activity_infra_error();
}
Compare the poll loop in the same file, which does branch on e.is_retryable() and backs off:
Err(e) if e.is_retryable() => {
consecutive_retryable_errors += 1;
let backoff_ms = (100 * 2_u64.pow(consecutive_retryable_errors)).min(3000);
warn!("Error fetching work item (retryable, attempt {}): {:?}, backing off {}ms", ...);
tokio::time::sleep(Duration::from_millis(backoff_ms)).await;
continue;
}
Activities are documented as at-least-once (docs/durable-futures-internals.md, "Activity Guarantees: At-Least-Once Execution"), so re-execution is not a correctness violation. But re-running a completed handler because the database was contended for a few milliseconds is wasteful and avoidable — the ack transaction rolled back, so retrying in place is safe.
Defect 2 — test asserts exactly-once on an at-least-once runtime
assert_eq!(a + b, 3, "Total should be 3, got A={a} B={b}");
This asserts each of the 3 scheduled activities executed exactly once. Any ack failure or lock expiry redelivers, so the assertion is wrong independent of the storage layer. Observed failure: Total should be 3, got A=1 B=3.
Why the ack fails here: shared-cache SQLITE_LOCKED
SqliteProvider::new_in_memory connects with sqlite::memory:?cache=shared and max_connections(5).
Shared-cache mode replaces file-level locking with table-level read/write locks. A connection holding a read lock that tries to upgrade to a write lock while another connection still reads that table gets SQLITE_LOCKED (code 6) — reported by sqlx as database is deadlocked.
Two things follow from the SQLite shared-cache docs:
PRAGMA busy_timeout = 60000provides zero protection here. "If a required table lock cannot be obtained, the query fails andSQLITE_LOCKEDis returned to the caller." No busy handler is invoked; recovering requiressqlite3_unlock_notify, which sqlx does not wire up.- Writer starvation amplifies it. "After any attempt to obtain a write-lock on a table fails ... all attempts to open new transactions on the shared-cache fail until the current writer concludes." This is why
fetch_work_itemandfetch_orchestration_itemalso reportSQLITE_LOCKEDin the same window.
SQLite also documents shared-cache as obsolete: "Shared-cache mode is an obsolete feature. The use of shared-cache mode is discouraged."
Blast radius: shared-cache is used only by new_in_memory. File-backed deployments use WAL and get SQLITE_BUSY, which is covered by busy_timeout. So the specific SQLITE_LOCKED trigger is test-infrastructure-only — but Defect 1 applies to any provider that returns a retryable error from ack_work_item.
Reproduction
for i in $(seq 8); do cargo nt --test session_e2e_tests; done
Measured 2/8 failures in-suite. Also 1/5 in isolation:
for i in $(seq 5); do cargo nt -E 'test(test_multi_worker_heterogeneous_config)'; done
Isolation failing too is worth noting: the contention is intra-test (2 runtimes x (2 worker + 2 orchestration) dispatchers = 8 concurrent pollers against one shared cache with a 5-connection pool), not cross-test.
Failure log:
WARN duroxide::runtime::dispatchers::worker: worker: atomic ack failed,
abandoning work item instance=hetero-test execution_id=1 activity_id=3
worker_id=work-0-unconstrained-node
error=ack_work_item: error returned from database: (code: 6) database is deadlocked
Confirmed pre-existing: the assertion and the worker ack path are both unchanged at b6e0255 (origin/main).
Proposed fix
- Retry retryable ack failures in place with bounded exponential backoff before falling back to abandon, mirroring the existing
fetch_work_itemhandling. Permanent errors keep abandoning immediately. - Relax the test assertion to
a + b >= 3, matching the documented at-least-once guarantee.
- 主要语言
- Rust
- 星标
- 231
- 派生
- 63
- 平均合并
- 20 小时 46 分钟
- 30 天内合并 PR
- 6
环境准备
- 没有 Dockerfile 或 Docker Compose 文件
- 有 Pull Request 模板
- 阅读贡献指南
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
microsoft/duroxide 的其他 Issue
-
难度 4/5 3-5 天 新手友好度 38/100
维护者通常 1 天内回复
-
bug
难度 3/5 1-2 天 新手友好度 74/100
维护者通常 1 天内回复
-
One failed session lock renewal loses the session: no retry, no log, no signal to running activities未关闭bug
难度 4/5 3-5 天 新手友好度 30/100
维护者通常 1 天内回复
-
bug
难度 3/5 1-2 天 新手友好度 72/100
维护者通常 1 天内回复
-
bug
难度 5/5 一周以上 新手友好度 38/100
维护者通常 1 天内回复
查看 microsoft/duroxide 的全部 Issue
相似的 Issue
-
area: library data-loss type: bug
难度 2/5 1-3 小时 新手友好度 72/100
alplabai/signex#1740 · 1 条评论 ·
维护者通常 1 天内回复
-
test(fslc): the unreadable mutate catalog test pins the Unix I/O error text and fails on Windows可能已有人在做 @rizumita 今天认领。 未关闭
难度 2/5 1-3 小时 新手友好度 82/100
维护者通常 1 天内回复
-
难度 2/5 1-3 小时 新手友好度 69/100
维护者通常 1 天内回复
-
ServerProperties defaults white-list to true, so a fresh --dedicated server silently locks itself未关闭
难度 2/5 1-3 小时 新手友好度 78/100
horizon-rust/horizon#3888 ·
维护者通常 1 天内回复
-
refactor
难度 2/5 1-3 小时 新手友好度 66/100
维护者通常 1 天内回复