Retry transient Windows atomic replacements
重试 Windows 上的瞬时原子替换失败
Windows can temporarily reject a rename that replaces an existing file with `EACCES`, `EBUSY`, or `EPERM` while another system component holds the target. The cross-process writer lock orders cooperating application writers but cannot release that external handle, so treating the first error as permanent makes an otherwise valid settings or credentials update fail nondeterministically.
English
Problem
Windows can temporarily reject a rename that replaces an existing file with EACCES, EBUSY, or EPERM while another system component holds the target. The cross-process writer lock orders cooperating application writers but cannot release that external handle, so treating the first error as permanent makes an otherwise valid settings or credentials update fail nondeterministically.
Decision
writeFileAtomic owns replacement retry because every file-backed store needs the same guarantee. On Windows only, it retries EACCES, EBUSY, and EPERM up to eight times with exponential delays from 20 to 200 milliseconds. The same fully written temporary sibling remains the rename source throughout, and a caller-held writer lock remains held until writeFileAtomic settles.
Other error codes and other operating systems fail immediately. Exhausting the retry budget rethrows the final filesystem error after removing the temporary sibling; the existing target remains unchanged because no attempt deletes or truncates it.
Alternatives considered
Retry the credentials mutation. A consumer-level retry would leave settings and future stores exposed, and replaying a read-modify-write operation can repeat work outside the atomic replacement. The shared primitive is the narrow owner of replacement-only retry.
Delete the target before rename. Removing the target can make readers observe an absent file and forfeits atomic replacement, so it cannot be a recovery step.
Retry indefinitely. A permanent permission error would then hang the writer and any lock contender. A bounded delay absorbs transient file use while preserving a predictable failure outcome.
Consequences
A transient Windows handle can delay one replacement by at most 1.1 seconds before the final attempt fails. During that interval readers continue to see the complete old target, and success still consists of one atomic rename. Regression tests inject every retried code, permanent and non-Windows failures, and retry exhaustion; they observe rename attempts and advance fake timers rather than depending on wall-clock sleeps.
中文
问题
当另一个系统组件持有目标文件时,Windows 可能以 EACCES、EBUSY 或 EPERM 暂时拒绝替换已有文件的 rename。跨进程写锁能够排序应用内互相协作的写入方,却无法释放该外部句柄,因此把第一次错误当作永久失败会让本来有效的设置或凭据更新随机失败。
决策
writeFileAtomic 负责替换重试,因为每个文件型存储都需要相同保证。它仅在 Windows 上重试 EACCES、EBUSY 与 EPERM,最多八次,延迟从 20 毫秒指数增长至 200 毫秒。整个过程中,同一份已经完整写入的临时兄弟文件始终作为 rename 来源;调用方持有的写锁也会保持到 writeFileAtomic 结束。
其他错误码和其他操作系统会立即失败。重试预算耗尽后,函数移除临时兄弟文件并重新抛出最后一个文件系统错误;由于任何尝试都不会删除或截断现有目标,目标内容保持不变。
考虑过的替代方案
重试凭据变更。 消费方级重试仍会让设置和未来存储暴露于同一问题,而且重放一次读-修改-写操作可能重复原子替换之外的工作。共享原语是只负责替换重试的最窄所有者。
在 rename 前删除目标。 删除目标会让读取方观察到文件缺失,并放弃原子替换,因此不能作为恢复步骤。
无限重试。 永久权限错误会由此挂住写入方与所有锁竞争者。有界延迟可以吸收瞬时文件占用,同时保留可预测的失败结果。
后果
一个瞬时 Windows 句柄最多会让单次替换多等待 1.1 秒,随后最终尝试失败。在此期间,读取方继续看到完整的旧目标;成功仍由一次原子 rename 完成。回归测试注入每种可重试错误、永久错误、非 Windows 错误与重试耗尽,并观察 rename 尝试和推进伪时钟,而不依赖真实时间 sleep。