Skip to content

Testing: the port-lock directory works for every user of a shared box; both failures printed; the race row's count asserted (CD2 follow-up to #45) - #47

Merged
rafalmaciag merged 3 commits into
masterfrom
fix/testing-store-port-race
Oct 18, 2026
Merged

rafalmaciag merged 3 commits into
masterfrom
fix/testing-store-port-race

Conversation

@rafalmaciag

Copy link
Copy Markdown
Contributor

Follow-up to #45 (rev-reach CD2: 9de418b9d, fbe88ab06).

m1: the lock directory on a shared box

  • When this process creates the directory it is made 1777 (sticky), and each lock file 0666, whatever the umask. The modes are set after creation, which is umask-proof. A path another user owns cannot be chmodded; that is best effort.
  • An existing directory is used as it is, which is the same as the foreign-owner case. So a stale same-user 0755 directory from an older version gives other users the named refusal below rather than a silent repair (rev-reach agreed).
  • A lock file this user may not open (UnauthorizedAccessException: another user's, made before these modes) is a port to skip.
  • A directory in which nothing can be locked (20 denied in a row) is refused, named, with chmod 1777 '<dir>' and "stale lock files of another user" as the causes. It is never an unhandled throw and never an endless search.
  • Lock files are never unlinked. Dispose only closes the file, releasing the flock. A lock file removed while held lets the next opener create a new inode and hold it too, so "never tidy lock files while tests run" is written on the class. Under the sticky bit only the owner, the directory's owner or root can remove one.

Found by the row going red first: my own fix made the first 0555 stand-in vacuous. Acquire chmodded my own 0555 directory to 1777, and the refusal row saw no refusal. A foreign-owned directory cannot be chmodded, so the stand-in was not faithful. Hence "chmod only when this process creates it".

m2: both failures printed

A failed start whose cleanup also failed throws an AggregateException: the start failure first, the cleanup failure second. Both appear in ToString; Data, used before, is printed by neither xUnit nor ToString.

The note

The race row asserts HaveCount(2) before its only/unique claims, which hold vacuously over one result.

Rows and mutants (each mutant build-gated)

Row Mutant → result
PortLockTests: modes 1777/0666 no chmod → red
PortLockTests: an unopenable lock file is skipped the UnauthorizedAccessException catch removed → red
PortLockTests: a 0555 directory refused, named the same catch removed → red (an unhandled throw surfaces)
start failure + cleanup failure → both in ToString back to Data → red
the race row one child only → red; only HaveCount(2) catches it

Each PortLockTests row uses its own directory and port range; the default directory is shared with every parallel class's stores.

Counts

Full MicroPlumberd.Tests at 6954f7b (with --blame-hang-timeout 4m): 200/207 (17 m 43 s). No hang and no host crash; 0 containers left. The 7 failures:

  • The long-standing 6: GrpcApiTests ×3, ProcessManagerSourceGenerationTests.CommandTypes, SubscriptionRunnerStateTests ×2.
  • Cron.CronTests.AnEnabledIntervalJob_IsScheduledByTheEngine_AndRuns: not this diff (cron, not Testing). ScheduledTotal was 0 for the full 60 s budget: never scheduled, not slow. A separate finding, reported to the lead:

The head (cab1959) adds only comment and message text on top of 6954f7b; built, and PortLockTests 3/3.

🤖 Generated with Claude Code

rafalmaciag and others added 3 commits October 18, 2026 17:37
…d box — created 1777 (sticky) with 0666 lock files whatever the umask; a lock file this user may not open is a port to skip; a directory in which nothing can be locked is refused, named, with the chmod that fixes it — never an unhandled throw or an endless search; a failed start whose cleanup also failed throws an AggregateException (start failure first) so both are printed; the race row asserts two results before its only/unique claims (rev-reach 9de418b9d m1, m2, note)

The directory is chmodded only when THIS process creates it: an existing one is used as it is (another user's cannot be
chmodded — and an own 0555 stand-in, chmodded, made the refusal row pass vacuously: caught by the row going red first).
Rows: modes 1777/0666; an unopenable lock file skipped; a 0555 directory refused named; both failures in ToString.
Mutants red: the UnauthorizedAccessException catch removed (2 rows); no chmod; the cleanup failure back in Data; the race
row with one child (only HaveCount(2) catches it).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sts run (an unlink while held swaps the inode and the race returns), and the refusal names another user's stale lock files as a cause (rev-reach fbe88ab06) — text only

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@rafalmaciag
rafalmaciag merged commit dfc8143 into master Oct 18, 2026
1 check passed
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