Replies: 3 comments
|
定位很干净( 锁文件里有 PID,而没人读它你原文写的:
这两句合起来就意味着:进程重启之后,那把锁里记的是一个已经不存在的 PID,而新进程仍然认为"这不是我的锁,我不能动" ——于是需要人工删文件。你正是这么描述的("until the leftover 所以除了重试释放之外,还有一条更根本的补法:
它的好处是同时治两种情况:
第二行是重试方案完全覆盖不到的,而它在 Windows 上并不罕见(任务管理器结束进程、蓝屏、Windows 更新重启)。 ("拒绝移除不属于自己的锁"这条设计是对的,它防的是并发写者互相踩;但它防不了、也不该防"持有者已经不存在了"。判据是PID 是否存活,不是"是不是我的"。这是文件锁的标准做法。) 实现上的注意:Windows 上 PID 会被复用,所以严格做法是在锁里除了 PID 再写一个启动时间戳或随机 token,接管前两者都对不上才判定为孤儿。这一点值得在提案里点一句,否则容易被评审以"PID 复用不安全"驳回。 顺带:这个 bug 的影响面可能比标题写的还大你说它"breaks all settings persistence",并举了 onboarding、模型设置、API key。补两条这个社区里同样表现为"设置存不下来"的报告,帮你把它和它们区分开——否则用户很容易把三件事混成一件:
这三者的区分判据很清楚,值得写进你的帖子帮后来人自查:
(DevTools 的 Network 面板一眼能分:有没有请求、有没有响应、响应说了什么。) 你这条难得地属于报错说人话的那一类(错误信息直接点名了锁文件的完整路径),这在这个社区里不多见——建议在帖子里把那句原始报错保留在显眼处,它是用户能自助的关键。 一个立刻能用的自救(给搜到这帖的人)(确认 dsh 没在运行,或者确认文件里那个 PID 不是当前 dsh 的。) 边界与利益相关我们不修 DSH 自家组件—— 利益相关:我维护 pi2dsh(Pi 生态兼容层)。这条不推销——这是 DSH 的原子写工具包在 Windows 上的行为,装什么插件都不改变它。 |
|
Reproduced and fixed locally. Your root cause is right, and there is a second defect hiding in the same three lines that a retry alone does not address. The retry, and why the release specifically cannot be best-effort
What makes it fatal rather than annoying is the interaction you identified: the contender refuses to remove a lock it does not own — correctly, since age cannot distinguish a crashed owner from a paused one — and nothing ages one out. So a sharing violation lasting milliseconds brands the file permanently, and the write that caused it returns success. A short retry covers the transient case: const MAX_LOCK_RELEASE_ATTEMPTS = 5
const LOCK_RELEASE_RETRY_MS = 20with exponential backoff — under 350 ms worst case, against a handle usually held for tens of milliseconds. The part I would argue for beyond a retry: when the release still cannot happen after a successful operation, that has to be reported rather than swallowed. Swallowing returns success while every future write to the file is already doomed, which is the failure you spent this investigation on. Failing loudly at least names the file to delete: The second bug in those three lines
The split that fixes both: strict release on the success path, quiet best-effort release on the failure path so it cannot speak over an error already in hand. try {
const result = await operation()
await releaseFileLock(lockPath, 'strict')
return result
} catch (error) {
await releaseFileLock(lockPath, 'best-effort')
throw error
}Regression coverageFour cases, all failing before the change, with the unlink refusal injected rather than waited for. The one worth copying is the reported symptom stated directly: two writes in a row, a transient refusal injected on each release, asserting the second one lands — because that is the user-visible bug, and a test that only checks the first write passes against the broken code. The other three: the lock is gone after a transient refusal (asserting the lock's absence, not merely that nothing threw — a swallowed failure also does not throw, and bricks the file); a permanent refusal surfaces and names the file; and a stuck release never masks the operation error it is unwinding. Worth noting for anyone hitting this now: deleting the stray |
|
A branch carrying a fix for this is available, based directly on https://github.com/nokkies/dsh-upstream-patches/tree/fix/atomic-write-lock-release It retries the lock release and never leaks the lock on failure. This branch carries the source change only, not its tests, and that is deliberate. Our test harness for this package has diverged: we replaced the boolean Offered as-is, no attribution wanted. Take, adapt, or ignore it freely. |
Uh oh!
There was an error while loading. Please reload this page.
Summary
On Windows,
@deepseek-ai/dsh-atomic-writeleaks the writer lock file after the first successful write. Every subsequentwithFileLockcall on the same target then fails withatomic-write: timed out waiting for the writer lock, which breaks all settings persistence indsh web(onboarding acknowledgement, model settings, API keys, etc.).Environment
@deepseek-ai/dsh@0.1.1-rc.2(npm dist,dsh web)@deepseek-ai/dsh-atomic-write0.1.1-rc.2(packages/util/atomic-writein this repo)Symptom
Fresh install, run
dsh web, open the Web UI athttp://127.0.0.1:3080:~/.dsh/settings.yamlnow containsui-onboarding.welcomeNoticeVersion), but~/.dsh/settings.yaml.lockis left on disk containing the dsh PID.returns:
and the Web UI shows "The acknowledgement could not be saved. Please try again."
So on Windows: first write works, all later writes fail until the leftover
.lockfile is deleted manually.Root cause
packages/util/atomic-write/src/index.ts,withFileLock():The lock release is a single fire-and-forget
rm. On Windows, right afterwriteFileAtomicwrites the target, antivirus software and/or file watchers (chokidar is watching these files in dsh) transiently hold an open handle to the freshly written.lockfile. Thermthen fails withEACCES/EPERM/EBUSY, the rejection is unhandled, and the lock file stays on disk forever.Since
withFileLockrefuses to remove a lock it does not own (correctly), every later writer waitsDEFAULT_LOCK_WAIT_MS(2 s) and fails. The design intentionally keeps lock-file age-based recovery out of band, so the leaked lock never heals by itself.This is much easier to hit on Windows than on Linux/macOS due to mandatory file sharing semantics, which likely explains why it survived CI.
Fix
Branch on my fork: https://github.com/MAPLEYOU/deepseek-harness/tree/fix/atomic-write-lock-release-retry
Make the release path resilient instead of fire-and-forget:
rma bounded number of times with exponential backoff (25 ms → 250 ms);rmSyncretries when the async path keeps failing;process.emitWarning(..., { code: 'ATOMIC_WRITE_LOCK_LEAKED' })instead of an unhandled rejection — the data write already succeeded, so the caller should not see an error, but the leak becomes observable.The acquisition path (
wxcreate + contention backoff) is untouched; orphan-lock ownership semantics are unchanged.Tests added in
packages/util/atomic-write/tests/atomic-write.spec.tscovering:EPERMon lock release → lock still removed, operation result preserved;ATOMIC_WRITE_LOCK_LEAKED, write still succeeds;Full suite (12 tests) passes locally on Windows.
Happy to open a PR if welcome — I saw CONTRIBUTING.md says external PRs are not accepted at this stage, so posting here instead. If the team prefers a different channel or wants a patch file instead, let me know.
Workaround for affected users
Delete the stale lock file and restart dsh:
All reactions