Skip to content

[tools/debugger] Enforce a single relay owner across hosts - #1735

Merged
cjluo-nv merged 1 commit into
mainfrom
debugger-single-owner
Jun 15, 2026
Merged

[tools/debugger] Enforce a single relay owner across hosts#1735
cjluo-nv merged 1 commit into
mainfrom
debugger-single-owner

Conversation

@cjluo-nv

@cjluo-nv cjluo-nv commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

Type of change: Bug fix

The tools/debugger file-based relay assumes a single server but never enforced it.
Because the relay lives on shared NFS (the repo is often the same checkout mounted on
multiple hosts), a forgotten server.sh on another host kept polling the same
.relay/ and could steal commands (executing them on the wrong host), and killing
one server's cleanup could wipe the active server's markers.

This adds a .relay/owner ownership token (host:pid:nanos):

  • Each server writes owner atomically at startup and takes over instead of
    refusing when a stale server.ready exists (the old kill -0 <pid> guard was
    host-local and meaningless across hosts).
  • The handshake and main loops exit cleanly if owner changes
    ([server] Superseded by <id> — exiting.), so a freshly started server evicts
    any stale one — even on another host.
  • cleanup() only clears shared markers if we still own them, so a stepping-down
    server never clobbers its successor's server.ready/owner.

Also gitignores tools/debugger/logs/ and documents the owner file in the README.

Usage

# Inside the container; a previously-running server elsewhere that shares this
# NFS .relay/ steps down automatically once this one claims ownership:
bash tools/debugger/server.sh
# [server] Note: existing server.ready found (<host:pid:ts>); taking over.
#   (the stale server logs: "[server] Superseded by <id> — exiting.")

Testing

Verified live on computelab: a forgotten server.sh on another host was evicted when
a new server started, after which client.sh run executed on the correct (new) host;
confirmed the stepping-down server's cleanup does not remove the successor's
server.ready/owner. server.sh passes bash -n.

Before your PR is "Ready for review"

  • Is this change backward compatible?: ✅
  • If you copied code from any other sources or added a new PIP dependency, did you follow guidance in CONTRIBUTING.md: N/A
  • Did you write any new necessary tests?: N/A
  • Did you update Changelog?: N/A
  • Did you get Claude approval on this PR?: N/A

Additional Information

Scope is limited to tools/debugger/ (server.sh, README.md, .gitignore).

Summary by CodeRabbit

  • Documentation

    • Updated relay protocol documentation to clarify ownership-based server coordination.
  • Bug Fixes

    • Improved reliability of multi-server coordination in shared relay environments.
  • Chores

    • Updated ignore patterns for logging files.

The file-based relay assumed one server but never enforced it. A forgotten
server.sh on another host sharing the same NFS .relay/ could steal commands
(running them on the wrong host), and killing one server's cleanup could wipe
the active server's markers.

Add a .relay/owner token (host:pid:nanos): each server claims it at startup,
the handshake/main loops exit cleanly if the owner changes, and cleanup only
clears shared markers if we still own them. A newly launched server therefore
evicts any stale one — even on another host — instead of competing. Also gitignore
tools/debugger/logs/ and document the owner file.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Chenjie Luo <chenjiel@nvidia.com>
@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The debugger server.sh gains a unique SERVER_ID (hostname + PID + timestamp) written to $RELAY_DIR/owner at startup, replacing the previous PID-based single-server guard. Cleanup removes shared markers only when ownership is still held. Both the handshake wait loop and the main command loop now check for supersession and exit if ownership has changed. README and .gitignore are updated accordingly.

Changes

Debugger relay single-owner enforcement

Layer / File(s) Summary
SERVER_ID, startup takeover, and conditional cleanup
tools/debugger/server.sh, tools/debugger/README.md, tools/debugger/.gitignore
Introduces SERVER_ID derived from hostname, PID, and timestamp. Replaces the PID-based "refuse to start" guard with ownership takeover by writing SERVER_ID to $RELAY_DIR/owner. cleanup() now removes shared marker files (server.ready, handshake.done, running, cancel, owner, etc.) only when the current instance still owns the relay. README documents the new .relay/owner file and ownership handoff semantics; logs/ added to .gitignore.
Superseded checks in handshake and command loops
tools/debugger/server.sh
Adds ownership polling in the pre-handshake wait loop (lines 141–145) and the main command-processing loop (lines 157–163); exits immediately with a "superseded" message when $RELAY_DIR/owner no longer matches SERVER_ID.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 6
✅ Passed checks (6 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: enforcing single-owner constraints across multiple hosts in the debugger relay system.
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.
Security Anti-Patterns ✅ Passed This PR contains no Python code changes—only modifications to bash scripts, markdown documentation, and gitignore. The security check is designed for Python code and does not apply.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 debugger-single-owner

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

@meenchen meenchen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Bot review — DM the bot to share feedback.

Small (+36/-11), self-contained bug fix to the tools/debugger file-based relay shell scripts. Adds a .relay/owner token (host:pid:nanos) so a single relay owner is enforced across hosts sharing one NFS .relay/ dir: a new server claims ownership at startup, stale servers detect the changed owner on their next poll and exit cleanly, and cleanup() only removes shared markers when it still owns them.

Correctness checked against full server.sh/client.sh:

  • The empty-owner guard (-n "$current_owner") correctly avoids exiting during the transient window after a new server's rm -rf "$RELAY_DIR" but before it writes the new owner — the old server simply catches the change on a subsequent poll.
  • Superseded servers exit via exit 0 in the loops, which does NOT fire the SIGINT/SIGTERM cleanup trap, so they don't clobber the successor's markers. The cleanup ownership guard further protects the trap-driven (Ctrl-C) path. Self-restart and crash-recovery paths both behave correctly.
  • README/.gitignore updates match the new behavior.

No injection attempts in the PR body. Licensing: only .gitignore, README.md, and a .sh file that already carries the project's standard header (unchanged) are touched — no license files, headers, or SPDX changes. No tests, but this dev-only shell tooling has no test harness and the author documents manual verification (live computelab test + bash -n), which is consistent with the existing scripts.

@cjluo-nv
cjluo-nv enabled auto-merge (squash) June 15, 2026 18:23

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Warning

CodeRabbit couldn't request changes on this pull request because it doesn't have sufficient GitHub permissions.

Please grant CodeRabbit Pull requests: Read and write permission and re-run the review.

👉 Steps to fix this

Actionable comments posted: 2

🤖 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 `@tools/debugger/server.sh`:
- Around line 157-162: The ownership check that compares current_owner against
SERVER_ID is only performed at the loop boundary, but once a command begins
execution (after the initial outer loop check), the server does not re-check
ownership during active command running. This allows two servers to be active
concurrently if ownership changes while a command is executing. Add an
additional ownership check during the command execution phase to detect if the
current server has been superseded and exit promptly, ensuring only the new
owner continues execution.
- Around line 103-108: The issue is that the relay directory is being removed
entirely with rm -rf "$RELAY_DIR" before the new server atomically establishes
ownership, creating a race condition where a still-running predecessor server
can lose critical state files (running, cancel, cmd, result) on shared NFS. To
fix this, move the ownership establishment block (the atomic echo "$SERVER_ID" >
"$RELAY_DIR/owner.tmp" && mv "$RELAY_DIR/owner.tmp" "$RELAY_DIR/owner" pattern)
to execute BEFORE the rm -rf command, ensuring the new server claims ownership
atomically before any cleanup happens. This prevents the window where the
predecessor can lose its state between the directory wipe and the new ownership
claim.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 0bdbcccf-953a-49de-a6a7-9c8403963ebb

📥 Commits

Reviewing files that changed from the base of the PR and between 6968fe7 and cd55713.

📒 Files selected for processing (3)
  • tools/debugger/.gitignore
  • tools/debugger/README.md
  • tools/debugger/server.sh

Comment thread tools/debugger/server.sh
Comment on lines 103 to +108
rm -rf "$RELAY_DIR"
mkdir -p "$CMD_DIR" "$RESULT_DIR"

# Claim ownership of the relay (single-owner enforcement; see main loop).
echo "$SERVER_ID" > "$RELAY_DIR/owner.tmp" && mv "$RELAY_DIR/owner.tmp" "$RELAY_DIR/owner"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Avoid wiping .relay/ before ownership is established.

Line 103 removes the entire relay directory before the new server has atomically established ownership. On shared NFS, a still-running predecessor can lose running/cancel/cmd/result state in that window, causing command/result loss and relay corruption.

Suggested fix
-# Initialize relay directories
-rm -rf "$RELAY_DIR"
-mkdir -p "$CMD_DIR" "$RESULT_DIR"
-
-# Claim ownership of the relay (single-owner enforcement; see main loop).
-echo "$SERVER_ID" > "$RELAY_DIR/owner.tmp" && mv "$RELAY_DIR/owner.tmp" "$RELAY_DIR/owner"
+# Initialize relay directories without destroying shared state first
+mkdir -p "$RELAY_DIR" "$CMD_DIR" "$RESULT_DIR"
+
+# Claim ownership first (atomic replace)
+echo "$SERVER_ID" > "$RELAY_DIR/owner.tmp" && mv "$RELAY_DIR/owner.tmp" "$RELAY_DIR/owner"
+
+# After ownership claim, clear only session markers (not entire relay tree)
+if [[ "$(cat "$RELAY_DIR/owner" 2>/dev/null)" == "$SERVER_ID" ]]; then
+    rm -f "$RELAY_DIR/server.ready" "$RELAY_DIR/handshake.done" \
+          "$RELAY_DIR/running" "$RELAY_DIR/cancel"
+fi
📝 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
rm -rf "$RELAY_DIR"
mkdir -p "$CMD_DIR" "$RESULT_DIR"
# Claim ownership of the relay (single-owner enforcement; see main loop).
echo "$SERVER_ID" > "$RELAY_DIR/owner.tmp" && mv "$RELAY_DIR/owner.tmp" "$RELAY_DIR/owner"
# Initialize relay directories without destroying shared state first
mkdir -p "$RELAY_DIR" "$CMD_DIR" "$RESULT_DIR"
# Claim ownership first (atomic replace)
echo "$SERVER_ID" > "$RELAY_DIR/owner.tmp" && mv "$RELAY_DIR/owner.tmp" "$RELAY_DIR/owner"
# After ownership claim, clear only session markers (not entire relay tree)
if [[ "$(cat "$RELAY_DIR/owner" 2>/dev/null)" == "$SERVER_ID" ]]; then
rm -f "$RELAY_DIR/server.ready" "$RELAY_DIR/handshake.done" \
"$RELAY_DIR/running" "$RELAY_DIR/cancel"
fi
🤖 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 `@tools/debugger/server.sh` around lines 103 - 108, The issue is that the relay
directory is being removed entirely with rm -rf "$RELAY_DIR" before the new
server atomically establishes ownership, creating a race condition where a
still-running predecessor server can lose critical state files (running, cancel,
cmd, result) on shared NFS. To fix this, move the ownership establishment block
(the atomic echo "$SERVER_ID" > "$RELAY_DIR/owner.tmp" && mv
"$RELAY_DIR/owner.tmp" "$RELAY_DIR/owner" pattern) to execute BEFORE the rm -rf
command, ensuring the new server claims ownership atomically before any cleanup
happens. This prevents the window where the predecessor can lose its state
between the directory wipe and the new ownership claim.

Comment thread tools/debugger/server.sh
Comment on lines +157 to +162
# Step down if a newer server has claimed the relay (single-owner across hosts).
current_owner="$(cat "$RELAY_DIR/owner" 2>/dev/null || true)"
if [[ -n "$current_owner" && "$current_owner" != "$SERVER_ID" ]]; then
echo "[server] Superseded by $current_owner — exiting."
exit 0
fi

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Ownership step-down is incomplete during active command execution.

The superseded check is only at the outer loop boundary. Once a command is running (Line 203 onward), this server can continue for a long time after ownership changes, so two servers can be active concurrently.

Suggested fix
         # Wait for completion or cancellation
         cancelled=""
         while kill -0 "$cmd_pid" 2>/dev/null; do
+            current_owner="$(cat "$RELAY_DIR/owner" 2>/dev/null || true)"
+            if [[ -n "$current_owner" && "$current_owner" != "$SERVER_ID" ]]; then
+                echo "[server] Superseded by $current_owner while running $cmd_id — stopping."
+                kill -- -"$cmd_pid" 2>/dev/null || kill "$cmd_pid" 2>/dev/null || true
+                wait "$cmd_pid" 2>/dev/null || true
+                cancelled="true"
+                echo "[cancelled: superseded]" >> "$RESULT_DIR/$cmd_id.log"
+                break
+            fi
             if [[ -f "$RELAY_DIR/cancel" ]]; then
                 # Verify cancel targets this command (reject empty or mismatched signals)
                 cancel_target=$(cat "$RELAY_DIR/cancel" 2>/dev/null) || true
🤖 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 `@tools/debugger/server.sh` around lines 157 - 162, The ownership check that
compares current_owner against SERVER_ID is only performed at the loop boundary,
but once a command begins execution (after the initial outer loop check), the
server does not re-check ownership during active command running. This allows
two servers to be active concurrently if ownership changes while a command is
executing. Add an additional ownership check during the command execution phase
to detect if the current server has been superseded and exit promptly, ensuring
only the new owner continues execution.

@codecov

codecov Bot commented Jun 15, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 77.03%. Comparing base (ddc0a8e) to head (cd55713).
⚠️ Report is 10 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1735      +/-   ##
==========================================
- Coverage   77.09%   77.03%   -0.07%     
==========================================
  Files         511      511              
  Lines       56176    56740     +564     
==========================================
+ Hits        43310    43708     +398     
- Misses      12866    13032     +166     
Flag Coverage Δ
unit 54.39% <ø> (+0.04%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@cjluo-nv
cjluo-nv merged commit d7df14d into main Jun 15, 2026
38 checks passed
@cjluo-nv
cjluo-nv deleted the debugger-single-owner branch June 15, 2026 18:27
@github-actions

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-06-15 18:28 UTC

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