Skip to content

test(txn): fix txn_kv_wiring port-collision TOCTOU flake#276

Merged
pilotspacex-byte merged 1 commit into
mainfrom
fix/txn-kv-wiring-port-toctou
Jul 10, 2026
Merged

test(txn): fix txn_kv_wiring port-collision TOCTOU flake#276
pilotspacex-byte merged 1 commit into
mainfrom
fix/txn-kv-wiring-port-toctou

Conversation

@pilotspacex-byte

@pilotspacex-byte pilotspacex-byte commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes an intermittent failure in the txn_kv_wiring integration suite caused by a port-selection TOCTOU race.

start_txn_server bound a throwaway TcpListener::bind("127.0.0.1:0") probe to discover a free port, dropped it, then let run_sharded rebind that port. Between the drop and the rebind, a parallel test — in this binary or another test binary — could win the just-freed ephemeral port. The caller then blind-slept 200ms and handed back a dead/wrong port, so tests flaked whenever the suite wasn't pinned to --test-threads=1 (which the module doc-comment recommends but nothing enforces).

Fix

Two independent defenses, no production code touched:

  1. reserve_unique_port — a process-global LazyLock<Mutex<HashSet<u16>>> rejects any port the OS recycled from a just-finished sibling test, so no two tests in this binary are ever handed the same port. This is the half a readiness check alone cannot solve: a sibling's live server would happily answer PING on the very port we lost, masking the collision.
  2. await_server_ready — an active connect+PING poll (up to 3s) replaces the fixed 200ms sleep (itself a slow-CI-startup flake source) and lets start_txn_server retry the whole start on a fresh port when a bind is genuinely lost to another process.

The background thread-spawn body was extracted verbatim into spawn_txn_server_thread so the retry loop can re-invoke it per attempt.

Verification

  • Compiles under --no-default-features --features runtime-tokio,jemalloc.
  • Ran the suite fully parallel (default thread count, not --test-threads=1) 13× — all 12 tests pass every run, 0 failures, with the readiness poll absorbing slow/contended starts (one run 3.0s vs typical 0.1s) instead of flaking.

Summary by CodeRabbit

  • Bug Fixes

    • Improved transaction integration test reliability by preventing ephemeral port conflicts.
    • Added active server readiness checks and automatic retries when startup fails.
  • Documentation

    • Updated the unreleased changelog to reflect the transaction test stability improvements.

start_txn_server bound a throwaway :0 probe to discover a port, dropped it,
then let run_sharded rebind that port — a classic TOCTOU. Between the drop and
the rebind a parallel test (same or another test binary) could win the freed
ephemeral port; the caller then blind-slept 200ms and handed back a dead/wrong
port, intermittently failing the suite when not pinned to --test-threads=1.

Two defenses:
- `reserve_unique_port`: a process-global `LazyLock<Mutex<HashSet<u16>>>`
  rejects any port the OS recycled from a just-finished sibling test, so no two
  tests in THIS binary are ever handed the same port. This is the half no
  readiness check can disambiguate — a sibling's live server would answer PING
  on the very port we lost.
- `await_server_ready`: an active connect+PING poll (up to 3s) replaces the
  fixed 200ms sleep (itself a slow-CI-startup flake source) and lets
  start_txn_server retry the whole start on a fresh port when a bind is lost to
  another process.

Thread-spawn body extracted to `spawn_txn_server_thread`; no production code
touched. Verified flake-free across 13 fully-parallel runs of all 12 tests.

author: Tin Dang <tindang.ht97@gmail.com>
@qodo-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The transaction KV wiring server harness now reserves process-unique ports, verifies readiness with Redis PING, and retries failed startup attempts. The changelog records this test flakiness fix and removes the prior SORT_RO ... STORE entry.

Changes

Transaction server startup reliability

Layer / File(s) Summary
Unique port reservation
tests/txn_kv_wiring.rs
A process-global mutex-protected set prevents reuse of ephemeral ports within the test binary.
Startup readiness and retry flow
tests/txn_kv_wiring.rs, CHANGELOG.md
Server startup uses readiness polling, cancellation, and bounded retries with fresh ports; the unreleased changelog entry reflects the change.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related issues

Possibly related PRs

  • pilotspace/moon#96 — Also changes transaction server readiness retry and Redis handshake logic in tests/txn_kv_wiring.rs.

Sequence Diagram(s)

sequenceDiagram
  participant TestHarness
  participant MoonServer
  participant RedisClient
  TestHarness->>MoonServer: Start on reserved port
  TestHarness->>RedisClient: Connect and send PING
  RedisClient-->>TestHarness: Return readiness result
  TestHarness->>MoonServer: Cancel and retry on failure
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description covers summary and verification, but it omits the template's Checklist, Performance Impact, and Notes sections. Add the missing Checklist, Performance Impact, and Notes sections, and state any unmet items or that performance impact is None.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the txn_kv_wiring port-collision TOCTOU fix and matches the change set.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/txn-kv-wiring-port-toctou

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.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/txn_kv_wiring.rs`:
- Around line 297-307: Update reserve_unique_port to use parking_lot::Mutex for
the HANDED_OUT static instead of std::sync::Mutex, and remove the .unwrap() from
its lock call because parking_lot::Mutex::lock() returns the guard directly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 23e0e14b-c2ee-4149-9214-bbc70470829e

📥 Commits

Reviewing files that changed from the base of the PR and between 284e674 and 394eb9b.

📒 Files selected for processing (2)
  • CHANGELOG.md
  • tests/txn_kv_wiring.rs

Comment thread tests/txn_kv_wiring.rs
Comment on lines +297 to +307
async fn reserve_unique_port() -> u16 {
static HANDED_OUT: std::sync::LazyLock<std::sync::Mutex<std::collections::HashSet<u16>>> =
std::sync::LazyLock::new(|| std::sync::Mutex::new(std::collections::HashSet::new()));
loop {
let probe = TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = probe.local_addr().unwrap().port();
drop(probe);
if HANDED_OUT.lock().unwrap().insert(port) {
return port;
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Use parking_lot::Mutex and eliminate the poisoned-lock unwrap pattern.

reserve_unique_port uses std::sync::Mutex and .lock().unwrap(), violating two coding guidelines: (1) "Use parking_lot::RwLock and parking_lot::Mutex instead of std::sync locks" and (2) "do not use poisoned-lock unwrap patterns." parking_lot is already a dependency in this file (e.g., lines 174, 192), so the fix is straightforward — parking_lot::Mutex::lock() returns a guard directly without Result.

As per coding guidelines: "Use parking_lot::RwLock and parking_lot::Mutex instead of std::sync locks; do not use poisoned-lock unwrap patterns."

🔒 Proposed fix
 async fn reserve_unique_port() -> u16 {
-    static HANDED_OUT: std::sync::LazyLock<std::sync::Mutex<std::collections::HashSet<u16>>> =
-        std::sync::LazyLock::new(|| std::sync::Mutex::new(std::collections::HashSet::new()));
+    static HANDED_OUT: std::sync::LazyLock<parking_lot::Mutex<std::collections::HashSet<u16>>> =
+        std::sync::LazyLock::new(|| parking_lot::Mutex::new(std::collections::HashSet::new()));
     loop {
         let probe = TcpListener::bind("127.0.0.1:0").await.unwrap();
         let port = probe.local_addr().unwrap().port();
         drop(probe);
-        if HANDED_OUT.lock().unwrap().insert(port) {
+        if HANDED_OUT.lock().insert(port) {
             return port;
         }
     }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async fn reserve_unique_port() -> u16 {
static HANDED_OUT: std::sync::LazyLock<std::sync::Mutex<std::collections::HashSet<u16>>> =
std::sync::LazyLock::new(|| std::sync::Mutex::new(std::collections::HashSet::new()));
loop {
let probe = TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = probe.local_addr().unwrap().port();
drop(probe);
if HANDED_OUT.lock().unwrap().insert(port) {
return port;
}
}
async fn reserve_unique_port() -> u16 {
static HANDED_OUT: std::sync::LazyLock<parking_lot::Mutex<std::collections::HashSet<u16>>> =
std::sync::LazyLock::new(|| parking_lot::Mutex::new(std::collections::HashSet::new()));
loop {
let probe = TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = probe.local_addr().unwrap().port();
drop(probe);
if HANDED_OUT.lock().insert(port) {
return port;
}
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/txn_kv_wiring.rs` around lines 297 - 307, Update reserve_unique_port to
use parking_lot::Mutex for the HANDED_OUT static instead of std::sync::Mutex,
and remove the .unwrap() from its lock call because parking_lot::Mutex::lock()
returns the guard directly.

Source: Coding guidelines

@pilotspacex-byte pilotspacex-byte merged commit 3c51694 into main Jul 10, 2026
18 of 19 checks passed
@pilotspacex-byte pilotspacex-byte deleted the fix/txn-kv-wiring-port-toctou branch July 10, 2026 15:01
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.

2 participants