Skip to content

Flaky: single_instance drop_releases_so_next_acquire_succeeds fails ci-success on main (blocks releases) #324

Description

@charliek

roost-engine's single_instance::tests::drop_releases_so_next_acquire_succeeds fails intermittently, and when it does it fails rust-buildci-success on main.

This gates releases. release.yml's ci-gate polls ci-success on the tagged commit and refuses to release unless it is green. A flake on that particular run stops a release until CI is re-run.

Observed failures

Same test, three separate main runs, both platforms — so it is not macOS-specific:

Date Runner Run
2026-08-07 macos-latest 31135056075
2026-08-07 ubuntu-latest 31138315777
2026-08-09 macos-latest on the 98f0987 merge (plan 023 PR #316)

Each time: 152–157 passed; 1 failed, and the same assertion.

The failure

thread 'single_instance::tests::drop_releases_so_next_acquire_succeeds' (50817)
panicked at crates/roost-engine/src/single_instance.rs:181:
called `Result::unwrap()` on an `Err` value: AlreadyHeld(18263)

The test is:

let dir = tempdir().unwrap();
let first = acquire(dir.path().join("roost.lock")).unwrap();
drop(first);
let second = acquire(dir.path().join("roost.lock")).unwrap();  // <- panics here

Note the number in parentheses after the thread name is Rust's thread id, not a pid. The reported holder — AlreadyHeld(18263) — is the test process's own pid, read back out of the lock file it just wrote. So at the moment of the second acquire, flock(LOCK_EX | LOCK_NB) returned WouldBlock against a lock file in a freshly created, uniquely named tempdir(), whose only writer was this same process a few microseconds earlier.

In other words: drop(first) did not reliably release the flock before the next acquire in the same process.

What is not the cause

  • Not a stale file from another run — tempdir() is unique per test.
  • Not another process — the pid in the file is this process's own.
  • Not platform-specific — reproduced on both ubuntu and macOS runners.
  • Not caused by plan 023's changes — that PR touched chrome colors, a pixel test, and docs, and the same commit passed on its own PR run and on the next merge to main.

Where to look

crates/roost-engine/src/single_instance.rs. Two candidates worth checking first:

  1. Descriptor duplication. read_pid does file.try_clone(). A dup'd descriptor shares the same open file description, and the flock is only released when every copy is closed. That path only runs on the error branch, but cargo test runs these tests concurrently in one process, so it is worth confirming no clone can outlive its InstanceLock.
  2. Drop ordering. InstanceLock has a manual Drop impl whose body deliberately does nothing; the _file field is dropped after the body returns. That should be synchronous, but it is the load-bearing assumption the test is asserting, and it is the assumption that appears to fail.

If the release genuinely is asynchronous under some condition, that matters beyond the test: the same "drop then re-acquire" sequence is what a restart-in-place would do.

Suggested handling

Root-cause it rather than retry it — a single-instance lock that does not reliably release is a real property to know about. But if a quick fix is not available, note that this is the sort of flake that will eventually land on a release commit at an inconvenient moment.

Related: #319 (the split XDG_RUNTIME_DIR lock issue) is in the same subsystem, though a different defect.

Refs

  • crates/roost-engine/src/single_instance.rsacquire, read_pid, impl Drop for InstanceLock, and the test at ~:172-183
  • .github/workflows/release.ymlci-gate, which makes this release-blocking

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions