Skip to content

Retry a lock sibling whose deletion has begun - #368

Merged
btsouth merged 1 commit into
mainfrom
fix/windows-delete-pending-lock
Aug 22, 2026
Merged

Retry a lock sibling whose deletion has begun#368
btsouth merged 1 commit into
mainfrom
fix/windows-delete-pending-lock

Conversation

@btsouth

@btsouth btsouth commented Aug 22, 2026

Copy link
Copy Markdown
Owner

Why

main is red. Rust / shared on 8f4868e5 failed:

---- secure_file::tests::flock_unsupported_serializes_through_exclusive_create stdout ----
called `Result::unwrap()` on an `Err` value: Custom { kind: PermissionDenied,
  error: "flock is unsupported (unsupported) and exclusive-create failed:
          could not create the exclusive-create lock file: Access is denied. (os error 5)" }

It is a flake, not a regression from that commit#359 touched usage_index.rs only, and main was green on 380db89c immediately before. But the underlying bug is real and not test-only.

Windows reports a file that has been unlinked while a handle is still closing as ERROR_ACCESS_DENIED, not ERROR_FILE_EXISTS. A waiter calling create_new while the holder is releasing therefore sees a name that is neither takeable nor already-taken, and try_exclusive_create mapped that to a hard Failed. In production that means a release can hand the next writer an error instead of the lock.

What

  • try_exclusive_create retries a PermissionDenied create for up to one second — orders of magnitude longer than delete-pending takes to clear, and short enough that a genuine permission problem still reports its own error instead of waiting out the 10s acquire timeout.
  • The retry is gated on const RETRIES_DELETE_PENDING: bool = cfg!(windows) rather than #[cfg(windows)], so the branch is compiled and lint-checked on Linux too. I can't build the MSVC target here, so this keeps the Windows-only path from going unchecked.

Verified

Linux: 26 secure_file tests pass, clippy -D warnings clean. The new releasing_the_sibling_hands_the_lock_to_the_next_writer runs the handoff 25 times per invocation; 15/15 invocations clean.

The Windows path itself can only be confirmed by CI on this PR.


Note

Medium Risk
Touches the cross-process lock that serializes settings and credential writes. The retry is short and Windows-only, but a misclassified permission error could delay a real failure by up to a second.

Overview
On Windows, releasing the flock-fallback sibling lock can fail the next writer instead of handing the lock over. create_new racing an unlink that still has a handle closing returns ERROR_ACCESS_DENIED, which was treated as a hard failure.

try_exclusive_create now retries PermissionDenied for up to one second, gated by cfg!(windows) so the path still compiles on Unix. A 25-round handoff test covers the race, and the changelog notes that a waiter in that window gets the lock rather than os error 5.

Reviewed by Cursor Bugbot for commit c3323d3. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Retry try_exclusive_create on Windows when sibling is delete-pending

On Windows, deleting the exclusive-create sibling can leave a brief window where the next writer gets PermissionDenied. The lock helper now retries for up to 1 second (DELETE_PENDING_GRACE) before returning an error, using a small sleep between attempts.

  • Extracts the single-attempt path into try_exclusive_create_once and wraps it in a retry loop gated by RETRIES_DELETE_PENDING (cfg!(windows)).
  • Adds a regression test (releasing_the_sibling_hands_the_lock_to_the_next_writer) that runs 25 rounds of lock handoff to cover the delete-pending race.
  • Risk: on Windows, lock acquisition can now take up to 1s longer in the delete-pending case before failing; non-Windows behavior is unchanged.

Macroscope summarized c3323d3.

CI went red on main with a Windows-only failure in
flock_unsupported_serializes_through_exclusive_create:

  Access is denied. (os error 5)

Windows reports a file that is unlinked but still has a handle closing as
ERROR_ACCESS_DENIED, not ERROR_FILE_EXISTS. A waiter that calls create_new
while the holder is releasing therefore sees a name that is neither
takeable nor gone, and try_exclusive_create turned that into a hard
failure rather than one more retry. In production that means a release can
hand the next writer an error instead of the lock.

The create is now retried for up to a second, which is orders of magnitude
longer than the state takes to clear and short enough that a genuine
permission problem still reports its own error rather than waiting out the
ten-second acquire timeout. The retry is compiled on every platform and
enabled only where the state exists, so it is type- and lint-checked off
Windows too.

The regression test runs the release-to-acquire handoff 25 times to widen
the window the flake needed.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
ceiling c3323d3 Commit Preview URL

Branch Preview URL
Aug 22 2026, 06:09 PM

@btsouth
btsouth enabled auto-merge (squash) August 22, 2026 18:09
@coderabbitai

coderabbitai Bot commented Aug 22, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@tsouth89, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 4 minutes

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

Wait for the limit to reset, then comment @coderabbitai review or push new commits to the PR.

An organization admin can change what happens after included review limits in Billing.

How do review limits work?

CodeRabbit enforces per-developer PR review limits within each organization.

For paid Pro and Pro+ reviews, CodeRabbit uses a developer's included PR review attempts over the past 7 days to set the current hourly allowance. At typical activity levels, the full plan allowance applies. Higher sustained activity can lower the allowance until earlier attempts leave the 7-day window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ff6b717c-8271-424c-8d05-b04b6ce6c68e

📥 Commits

Reviewing files that changed from the base of the PR and between c0148f6 and c3323d3.

📒 Files selected for processing (2)
  • CHANGELOG.md
  • rust/src/secure_file.rs

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@btsouth
btsouth merged commit 5c4ca0b into main Aug 22, 2026
12 of 14 checks passed
@btsouth
btsouth deleted the fix/windows-delete-pending-lock branch August 22, 2026 18:12

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit c3323d3. Configure here.

Comment thread rust/src/secure_file.rs
btsouth added a commit that referenced this pull request Aug 22, 2026
Two unrelated flakes have each failed PRs today that did not touch them.

## 1. `TrayPanel > keeps a counted star ask visible when the tray is
empty`

Failed on #367 and #355 with:

```
TypeError: Cannot read properties of undefined (reading 'then')
  The above error occurred in the <TrayPanel> component
```

`afterEach` calls `vi.restoreAllMocks()`, which resets every mock to the
implementation it was **created** with — and for a bare `vi.fn()` that
is "return undefined". `LocaleProvider` does `listen(...).then(...)`, so
any call landing outside the `beforeEach`→test window throws.

It is always this test because it is the only one that sets
`starPromptMocks.reason`, so it is the only one that renders the prompt
and takes the slower path.

`eventMocks.listen` is now declared as `vi.fn(() => Promise.resolve(()
=> {}))`, so a restore leaves it harmless. `beforeEach` still installs
the listener-capturing version the tests drive.

**Proof** — same file, forcing `eventMocks.listen.mockReset()` before
the render:

| `listen` declared as | Result |
|---|---|
| `vi.fn()` (before) | `1 failed \| 24 passed` — the exact CI error |
| `vi.fn(() => Promise.resolve(…))` (after) | `25 passed` |

## 2.
`an_unenforceable_lock_fails_closed_instead_of_writing_unserialized`

Failed on #355. **This one is my regression from #368.** That PR added a
retry for Windows' delete-pending state, which reports access-denied. A
*directory* planted at the sibling path — which is exactly what this
test does — is also access-denied on Windows, but it never clears. The
retry sat on it for the full second and broke the test's sub-second
bound.

The retry now skips a directory, the grace drops from 1s to 250ms (still
orders of magnitude longer than delete-pending needs), and the test
asserts against `DELETE_PENDING_GRACE` rather than a bare literal so the
two cannot drift.

## Verified

Frontend: **88 files, 707 tests passing.** Rust: 26 `secure_file` tests,
clippy `-D warnings` clean.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Test-only mock change plus a narrower Windows lock retry that fails
faster on an unfixable directory sibling. Does not change the successful
lock or write path.
> 
> **Overview**
> Stops two unrelated CI flakes: a Vitest mock restore that made
`listen()` return `undefined`, and a Windows exclusive-create retry that
sat on a directory for the full grace period.
> 
> `eventMocks.listen` is now created as a function that resolves to an
unsubscribe, so `vi.restoreAllMocks()` in `afterEach` no longer breaks
`LocaleProvider`'s `listen(...).then(...)` outside the `beforeEach`
window.
> 
> On the lock path, `try_exclusive_create` no longer retries
`PermissionDenied` when the sibling is a directory (that never clears on
Windows). `DELETE_PENDING_GRACE` drops from 1s to 250ms, and the
fail-closed test asserts against that constant so it cannot drift.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
383d3b1. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

<!-- Macroscope's pull request summary starts here -->
<!-- Macroscope will only edit the content between these invisible
markers, and the markers themselves will not be visible in the GitHub
rendered markdown. -->
<!-- If you delete either of the start / end markers from your PR's
description, Macroscope will append its summary at the bottom of the
description. -->
> [!NOTE]
> ### Fix CI flakes in `TrayPanel` test mock and `secure_file` lock
retry logic
> - Changes the `eventMocks.listen` mock in
[TrayPanel.test.tsx](https://github.com/tsouth89/ceiling/pull/369/files#diff-d28e96cb56241c4ab16b385da508fb6832756fb3f5b99c0ef7c2098ee0386269)
to return a resolved Promise to prevent errors when mocks are restored.
> - Reduces `DELETE_PENDING_GRACE` from 1 second to 250 milliseconds in
[secure_file.rs](https://github.com/tsouth89/ceiling/pull/369/files#diff-ad931c0d6c98745631dc7a8420f5f246909a8c5b9f80d9987e7f76df43b3e157).
> - Updates `StateWriteLock.try_exclusive_create` to fail fast when the
lock path is a directory instead of retrying until the deadline.
> - Behavioral Change: `DELETE_PENDING_GRACE` reduction lowers the
maximum wait time for exclusive-create retries on Windows.
>
> <!-- Macroscope's review summary starts here -->
>
> <sup><a href="https://app.macroscope.com">Macroscope</a> summarized
383d3b1.</sup>
> <!-- Macroscope's review summary ends here -->
>
<!-- Macroscope's pull request summary ends here -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant