Skip to content

Retry poll when interrupted by a signal - #181

Open
jattisha wants to merge 2 commits into
kohlschutter:mainfrom
jattisha:fix-poll-eintr
Open

jattisha wants to merge 2 commits into
kohlschutter:mainfrom
jattisha:fix-poll-eintr

Conversation

@jattisha

@jattisha jattisha commented Oct 16, 2026 •

Copy link
Copy Markdown

Summary

Retry the POSIX poll() call when it fails with EINTR.

NativeUnixSocket.poll currently converts an interrupted system call into a SocketException. A signal delivered while the native call is blocked can therefore surface as java.net.SocketException: Interrupted system call, even though EINTR is transient and the operation can be retried. This is particularly visible when socket I/O is coordinated through the virtual-thread poller.

This applies the same established EINTR retry pattern already used by accept, connect, receive, and send.

Testing

  • Built all 29 native C source files and linked the JNI library on macOS arm64 using JDK 21 and Maven 3.9.11.
  • Used a macOS dynamic interposer to make the actual poll() call originating from libjunixsocket-native return -1 with errno = EINTR exactly once.
    • Before this change, the harness failed with java.net.SocketException: Interrupted system call from NativeUnixSocket.poll.
    • With this change, the same injected EINTR was retried and NativeUnixSocket.poll returned normally.
  • Ran VirtualThreadPollerTest: 4 tests passed with no failures or skips.
  • Ran git diff --check successfully.

@@ -418,7 +418,9 @@ JNIEXPORT jint JNICALL Java_org_newsclub_net_unix_NativeUnixSocket_poll
#if defined(_WIN32)
ret = WSAPoll(pollFd, nfds, timeout);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

why is Windows excluded from this logic?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

This change addresses POSIX poll() returning EINTR. WSAPoll does not appear to document an equivalent interruption error (and I did use some AI to search through and I couldn't find an equivalent either). If there is an equivalent Windows case that should have the same behavior, I’m open to handling it here as well.

Comment thread junixsocket-native/src/main/c/polling.c Outdated
#else
ret = poll(pollFd, nfds, timeout);
do {
ret = poll(pollFd, nfds, timeout);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

timeout would need to be reduced by the time already elapsed since the first call, otherwise we may be polling for too long

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Good call, I'll update the PR reducing timeout when retrying.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in d2d8022. Finite polls now use a monotonic deadline and pass only the remaining time after EINTR, so retries cannot extend the original timeout.

@kohlschuetter

Copy link
Copy Markdown
Member

@jattisha Thanks for the PR!

To clarify, did you ever observe EINTR in a regular setup, i.e.., without a "dynamic interposer"?
Can you perhaps provide a test setup that doesn't require modifying the system call behavior?

@kohlschuetter

kohlschuetter commented Oct 20, 2026 •

Copy link
Copy Markdown
Member

Also, while I agree that throwing a plain SocketException is not a proper way of dealing with this, why shouldn't we just map EINTR to InterruptedIOException?

@jattisha

Copy link
Copy Markdown
Author

To clarify, did you ever observe EINTR in a regular setup, i.e.., without a "dynamic interposer"?
Can you perhaps provide a test setup that doesn't require modifying the system call behavior?

I have not personally reproduced it without the interposer. The issue was motivated by a failure observed while profiler signals were being delivered, and I used the interposer to reproduce the same native return condition deterministically.

I can look into a test setup.

Also, while I agree that throwing a plain SocketException is not a proper way of dealing with this, why shouldn't we just map EINTR to InterruptedIOException?

My concern is that EINTR means a native signal interrupted the call, not necessarily that the Java thread was interrupted. Retrying avoids exposing unrelated signals, such as profiler activity, to the application.

Track a monotonic deadline so retries cannot extend finite polls. Add an opt-in native test that interrupts poll with a real POSIX signal and verifies the original deadline is retained.
@jattisha

Copy link
Copy Markdown
Author

I added the non-interposed test setup in d2d8022. It delivers a real SIGALRM while POSIX poll() is blocked, then makes a pipe readable only after the original timeout. This verifies both that EINTR is retried and that the retry preserves the original deadline. The test can be run with mvn -pl junixsocket-native -Dpolling-eintr-test clean test.

@jattisha

Copy link
Copy Markdown
Author

My AI agent just went a bit above and beyond here, lol. Feel free to push back if anything here is too complex.

This branch has not been deployed

No deployments
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