Skip to content

Destroy picofuzz socket after a timeout#10

Merged
tomusdrw merged 4 commits intomainfrom
debug/jampy-picofuzz-repeat-1
Mar 16, 2026
Merged

Destroy picofuzz socket after a timeout#10
tomusdrw merged 4 commits intomainfrom
debug/jampy-picofuzz-repeat-1

Conversation

@tomusdrw
Copy link
Copy Markdown
Member

@tomusdrw tomusdrw commented Mar 16, 2026

Summary

  • Adds a picofuzz_repeat input parameter to the reusable picofuzz workflow (default: 10, preserving existing behavior)
  • Sets picofuzz_repeat: 1 for the jampy workflow to speed up debugging
  • Test harness reads PICOFUZZ_REPEAT env var to override the default repeat count

Test plan

  • Verify jampy picofuzz job runs with repeat=1
  • Verify other clients still use the default repeat=10

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved socket closing behavior to ensure graceful shutdown before exit.
    • Added automatic timeout protection to force-close unresponsive connections after 5 seconds, preventing resource leaks.

Add picofuzz_repeat parameter to the reusable workflow and set it to 1
for jampy to speed up debugging.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 16, 2026

📝 Walkthrough

Walkthrough

The socket closing mechanism is converted from synchronous to asynchronous. The close() method now returns a Promise, implements a 5-second timeout for graceful shutdown, waits for the socket 'close' event, and force-destroys the socket if closure takes too long.

Changes

Cohort / File(s) Summary
Socket Closure Async Refactoring
picofuzz/index.ts, picofuzz/socket.ts
Converts close() from synchronous void method to async Promise<void>. Adds timeout-based force-destroy mechanism, event listener for socket close completion, and updates caller to await the closure operation.

Sequence Diagram

sequenceDiagram
    participant Caller as Caller
    participant Socket as Socket Instance
    participant EventListener as Event Listener
    participant Timeout as Timeout Handler

    Caller->>Socket: await close()
    activate Socket
    Socket->>Socket: Create completion Promise
    Socket->>Timeout: Set 5-second timeout
    activate Timeout
    Socket->>EventListener: Register 'close' event listener
    activate EventListener
    Socket->>Socket: Call end() on socket
    
    alt Socket closes within 5 seconds
        Socket-->>EventListener: 'close' event emitted
        EventListener->>Socket: Resolve completion Promise
        Socket->>Timeout: Clear timeout
        deactivate Timeout
    else Timeout expires
        Timeout->>Socket: Force destroy socket
        Socket->>Socket: Resolve completion Promise
    end
    
    deactivate EventListener
    deactivate Socket
    Caller->>Caller: Continue after close completes
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 The socket once closed with a snap and a crack,
Now waits for a promise to bring closure back!
Five seconds to part on the gentlest of terms,
Then force takes the stage when the timeout returns. 🔌✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: making socket closure asynchronous with a timeout-based force-destroy mechanism for resource cleanup.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch debug/jampy-picofuzz-repeat-1
📝 Coding Plan
  • Generate coding plan for human review comments

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

@tomusdrw
Copy link
Copy Markdown
Member Author

@dakk AFAICT, picofuzz sends FIN on the socket, but jampy does not respond with FIN. That causes picofuzz to wait indefinitely for proper socket closure, but it never happens.

I'll patch it on our side to destroy the socket after some timeout, but might be something to look into in jampy as well.

End the socket first, then wait up to 5 seconds for it to close
before forcefully destroying it.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@dakk
Copy link
Copy Markdown
Contributor

dakk commented Mar 16, 2026

@dakk AFAICT, picofuzz sends FIN on the socket, but jampy does not respond with FIN. That causes picofuzz to wait indefinitely for proper socket closure, but it never happens.

I'll patch it on our side to destroy the socket after some timeout, but might be something to look into in jampy as well.

ok thank you for the debugging and for the workaround; I've also opened the issue on jampy

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
picofuzz/index.ts (1)

54-58: ⚠️ Potential issue | 🟠 Major

process.exit(1) in catch bypasses the finally block's async cleanup.

At line 56, process.exit(1) terminates the process immediately without waiting for the await socket.close() at line 58. Per Node.js documentation, process.exit() does not allow pending async operations in finally blocks to complete.

🔧 Suggested fix
   } catch (error) {
     console.error("Error:", error);
-    process.exit(1);
+    process.exitCode = 1;
   } finally {
     await socket.close();
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@picofuzz/index.ts` around lines 54 - 58, The catch block currently calls
process.exit(1), which prevents the async finally block (await socket.close())
from running; change the behavior to set process.exitCode = 1 instead of calling
process.exit(1) (or alternatively await socket.close() inside the catch before
exiting) so that the finally block can complete its async cleanup; update the
catch surrounding code that references process.exit/process.exitCode and ensure
socket.close() is still awaited in the finally block.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@picofuzz/socket.ts`:
- Around line 97-109: In the async close() method, add a fast-path check for an
already destroyed socket by inspecting this.socket.destroyed (or
socket.destroyed after assigning const socket = this.socket) and return
immediately if true; otherwise proceed with the current logic but avoid calling
socket.end() when destroyed so you won't hang waiting for the 'close' event and
forced 5s timeout.

---

Outside diff comments:
In `@picofuzz/index.ts`:
- Around line 54-58: The catch block currently calls process.exit(1), which
prevents the async finally block (await socket.close()) from running; change the
behavior to set process.exitCode = 1 instead of calling process.exit(1) (or
alternatively await socket.close() inside the catch before exiting) so that the
finally block can complete its async cleanup; update the catch surrounding code
that references process.exit/process.exitCode and ensure socket.close() is still
awaited in the finally block.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e99da258-3e44-4492-8767-6b17f65153fa

📥 Commits

Reviewing files that changed from the base of the PR and between d7d8d68 and 376f5e8.

📒 Files selected for processing (2)
  • picofuzz/index.ts
  • picofuzz/socket.ts

Comment thread picofuzz/socket.ts
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
picofuzz/socket.ts (1)

97-119: ⚠️ Potential issue | 🟡 Minor

Add a fast-path for already-destroyed sockets to avoid an unnecessary 5s wait.

At Line 97, close() can stall for the full timeout if socket.destroyed is already true, because a new 'close' event is not expected in that state.

🔧 Proposed fix
   async close() {
     const socket = this.socket;
+    if (socket.destroyed) {
+      return;
+    }

     await new Promise<void>((resolve) => {
       let timeout: ReturnType<typeof setTimeout> | null = null;
       // resolve promise when socket is fully closed.
       socket.once("close", () => {
         if (timeout !== null) {
           clearTimeout(timeout);
         }
         resolve();
       });

       // send FIN to the socket
       socket.end();

       // when the other end does not terminate as well
       // we forcefully destroy
       timeout = setTimeout(() => {
         socket.destroy();
         resolve();
       }, 5000);
     });
   }
For Node.js net.Socket: if socket.destroyed is already true, does calling socket.end() emit a new 'close' event, or can it result in ERR_STREAM_DESTROYED / no new close event?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@picofuzz/socket.ts` around lines 97 - 119, The close() method can hang
waiting for a 'close' event when this.socket.destroyed is already true; add a
fast-path at the start of close() that checks if socket.destroyed and
immediately return (resolve) without calling socket.end() or installing the
timeout/once handler. Otherwise only call socket.end(), attach the
socket.once('close') handler and set the timeout as before; reference the
close() function, this.socket, socket.destroyed, socket.end(),
socket.once('close') and the timeout logic to locate the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@picofuzz/socket.ts`:
- Around line 97-119: The close() method can hang waiting for a 'close' event
when this.socket.destroyed is already true; add a fast-path at the start of
close() that checks if socket.destroyed and immediately return (resolve) without
calling socket.end() or installing the timeout/once handler. Otherwise only call
socket.end(), attach the socket.once('close') handler and set the timeout as
before; reference the close() function, this.socket, socket.destroyed,
socket.end(), socket.once('close') and the timeout logic to locate the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5b67c76f-a4f8-4b23-9e81-046d8baf8e22

📥 Commits

Reviewing files that changed from the base of the PR and between 376f5e8 and 0d86291.

📒 Files selected for processing (1)
  • picofuzz/socket.ts

@tomusdrw tomusdrw changed the title debug: reduce jampy picofuzz repeat to 1 Destroy picofuzz socket after a timeout Mar 16, 2026
@tomusdrw tomusdrw merged commit 1a237da into main Mar 16, 2026
1 check passed
@tomusdrw tomusdrw deleted the debug/jampy-picofuzz-repeat-1 branch March 16, 2026 18:07
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