Skip to content

refactor: one file lock for seven packages, not fourteen - #296

Merged
dinstein merged 2 commits into
mainfrom
refactor/platform-unix-flock
Aug 10, 2026
Merged

refactor: one file lock for seven packages, not fourteen#296
dinstein merged 2 commits into
mainfrom
refactor/platform-unix-flock

Conversation

@dinstein

Copy link
Copy Markdown
Owner

Seven packages hand-carried a flock_unix.go / flock_windows.go / flock_stub.go triplet. The
Windows halves were already one implementation in internal/platform and byte-for-byte identical;
the Unix halves were seven hand-written copies and had driftedinternal/secrets retried
EINTR around its non-blocking lock, internal/oauthflow's same-purpose function did not (so a
signal delivered mid-syscall reported the offline refresh path as broken rather than retryable), and
internal/ratelimit was a third variant. The comments still described the copies as structurally
identical.

  • 1. internal/platform gains the Unix half of the seam — filelock_unix.go answers
    LockFile / TryLockFile / UnlockFile / IsLockBusy with flock(2) and one EINTR
    policy (retry, below every entry point). filelock_other.go narrows to the platforms with
    neither implementation. syscall only: the package is depguard-locked to $gostd.
    New tests exercise exclusion, the blocking wait and the busy predicate — Unix is the half that
    can actually be executed, and the Windows branch has never had a machine to run on.
  • 2. The seven packages collapse to one flock.go each (tagged darwin || linux || windows,
    four lines over internal/platform) plus their existing flock_stub.go. Blocking-versus-
    polling preserved per caller: calllog and ratelimit wait in the kernel, the other five poll
    a non-blocking attempt so they can honour a context. Stale prose removed with it —
    oauthflow cited internal/integrity (retired) and promised Windows "in M2" (shipped), and
    docs/windows.md counted six packages where seven take a lock.
  • 3. test/buildrules/flockparity_test.go moves with the syscalls: it demanded a
    flock_windows.go beside every flock_unix.go; it now demands every flock.go reach
    internal/platform and fails any package outside internal/platform that calls
    syscall.Flock or LockFileEx itself. That second claim is the drift the old check could not
    see.

The poll ladder: read, not hoisted

registry/lock.go, httpbridge/fileio.go, skills/lock.go — and, not in the original finding,
secrets/vaultlock.go and oauthflow/refresh.go — carry five near-identical
open / non-blocking-attempt / poll / deadline ladders. They are not hoisted here, and the reason is
that hoisting the wait loop is not a smaller change than it looks:

  • The moment platform owns the loop, it also owns the non-blocking attempt and the busy predicate,
    so each package's flockExclusiveNB and isWouldBlock become unreachable and the whole per-package
    lock seam — flock.go, flock_stub.go, and the three packages' crossProcessLockSupported
    fail-closed prose — dissolves into internal/platform with it. That is 21 files and four
    documented failure directions, on top of this branch's 34.
  • What would not move is what actually differs: each package's own fileLock type and
    LockTimeoutError (no store may import another's error model, and test/buildrules pins each
    sentinel into the CLI's exit-7 parity table). Every call site would keep an error-mapping switch.

So it deserves its own branch, judged on its own. Two things found while reading them, recorded here
rather than fixed: oauthflow polls with a timeout but returns plain fmt.Errorf, so lock contention
there reaches the CLI as exit 1 while the identical contention in four other stores exits 7 —
and locktimeoutregistry_test.go's comment claimed oauthflow "tries once", which it does not. The
comment is corrected in this branch; the exit-code asymmetry is not.

Verification

make fmt, make ci, and make cross-windows — the last because this branch touches the
_windows.go files and cross-compilation is the only Windows gate that exists.

dinstein and others added 2 commits August 10, 2026 11:30
internal/platform owned LockFileEx and nothing else, so the four names it
exports existed only on Windows and each of the seven packages that lock a
file carried its own flock(2) shim. The copies drifted: internal/secrets
retries EINTR around the non-blocking lock and internal/oauthflow does not,
so one signal delivered mid-syscall costs secrets a retry and makes
oauthflow's offline refresh report a hard failure.

filelock_unix.go answers LockFile, TryLockFile, UnlockFile and IsLockBusy
with flock(2) and one EINTR policy — retry, below every entry point rather
than in some of them — leaving syscall the only import, as the zero-dependency
rule requires. filelock_other.go narrows to the platforms with neither
implementation and keeps the fail-closed stand-ins there.

Unix is the half that can actually be executed, so it gets the exclusion,
blocking-wait and unknown-error tests the Windows branch has never had a
machine to run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Each of the seven packages that keeps a single-writer file carried a
flock_unix.go and a flock_windows.go. The Windows halves already delegated to
internal/platform and were identical; the Unix halves were hand-written and no
longer agreed — internal/secrets retried EINTR, internal/oauthflow did not,
internal/ratelimit had a third variant — so the same interrupted syscall meant
different things in different packages, and the comments still called the
copies structurally identical.

Now that internal/platform answers both platforms, each package owns one
flock.go tagged darwin || linux || windows, four lines over
LockFile/TryLockFile/UnlockFile/IsLockBusy, and keeps flock_stub.go for
everything else. Blocking-versus-polling is preserved per caller: calllog and
ratelimit wait in the kernel, the other five poll a non-blocking attempt so
they can honour a context.

The parity test moves with the syscalls. It demanded a flock_windows.go beside
every flock_unix.go; it now demands that every flock.go reach
internal/platform and that nothing outside internal/platform call flock(2) or
LockFileEx itself, which is the drift it could not see before. Stale prose
goes with it: oauthflow's comment cited internal/integrity and promised
Windows "in M2", both years out of date, and docs/windows.md counted six
packages where seven take a lock.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@dinstein
dinstein force-pushed the refactor/platform-unix-flock branch from f82bb5d to a16dc0a Compare August 10, 2026 03:30
@dinstein
dinstein marked this pull request as ready for review August 10, 2026 03:30
@dinstein
dinstein merged commit a16dc0a into main Aug 10, 2026
3 checks passed
@dinstein
dinstein deleted the refactor/platform-unix-flock branch August 10, 2026 03:37
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