Siyao Meng created HDDS-17436:
---------------------------------
Summary: XceiverServerDomainSocket retries a failed accept() at
once and spins while the datanode is out of file descriptors
Key: HDDS-17436
URL: https://issues.apache.org/jira/browse/HDDS-17436
Project: Apache Ozone
Issue Type: Bug
Reporter: Siyao Meng
Attachments:
CR-3-pause-before-retrying-failed-domain-socket-accept.patch,
TestBugCR3AcceptFailureRetry.java
h3. Mechanism
{{XceiverServerDomainSocket.run}} catches every {{IOException}} other than a
timeout or an asynchronous close in one branch, which logs a WARN with the
stack trace, closes the peer and calls {{accept()}} again right away. The
branch was written for the rejection of a single connection at the xceiver
limit (its comment says "usually when the xceiver count limit is hit"). When
{{accept()}} itself fails, nothing changes before the next call, so a
persistent failure turns the loop into a busy spin. Only the
{{OutOfMemoryError}} branch waits before retrying. The loop follows HDFS
{{DataXceiverServer.run}}, which has no pause in that branch either. The
generic {{catch (Throwable)}} branch logs "XceiverServerDomainSocket: Exiting."
but keeps looping without a pause; it is not changed here.
h3. Trigger
# Short-circuit read is enabled and the datanode process has reached its open
file limit, for whatever reason outside this server.
# A short-circuit client connects. On Linux the connection stays queued and
each {{accept()}} fails at once with {{accept(2) error: Too many open files}}.
The loop spins for as long as both last. Run with an injected accept failure,
see Reproduction.
h3. Impact
* The accept thread uses most of a core and produces one WARN log event with a
stack trace per attempt: about 470k events per second in the Linux run
described under Reproduction, about 60k per second with the mocked failure on
macOS. This happens during an incident in which the datanode is already short
of resources, and the log volume can rotate away the earlier log lines of that
incident.
* It stops once descriptors are free again. No data is affected.
h3. Reproduction
PASS, deterministic, unmodified source. [^TestBugCR3AcceptFailureRetry.java]
({{hadoop-ozone/integration-test}}, no native library needed) starts the real
server on a mocked listening socket whose {{accept()}} always throws the EMFILE
{{SocketException}} and counts attempts and WARN events over one second: 58112
attempts and 58113 WARN events on macOS. A passing test means the defect is
present.
The real descriptor shortage was run separately on Linux (aarch64 container,
Zulu 17, RLIMIT_NOFILE 10240, a separate client process waiting in the backlog,
unmodified source): the accept thread used 82 to 84% of a core and produced
about 470k WARN events per second, counted in the test rather than written to a
log file, and both fell to zero once descriptors were released. That run was
not repeated with the patch; the patch was shown with the mocked failing
{{accept()}} on macOS. On macOS the real shortage does not produce the spin,
because the kernel takes the pending connection off the queue when no
descriptor is free, so {{accept()}} fails only once.
h3. Patch
[^CR-3-pause-before-retrying-failed-domain-socket-accept.patch], against
69d77beff60b9106e2517c36a7a1b2cc726af568. It also applies to master at
1acf315d546.
When the {{IOException}} came from {{accept()}} itself (no peer was created
yet), the loop sleeps one second before it calls {{accept()}} again, as the
{{OutOfMemoryError}} branch already does for 30 seconds. A connection that is
accepted and then rejected or fails is handled as before, without a pause. An
interrupt during the pause is ignored like in that branch, because {{stop()}}
clears the running flag before it interrupts the thread, so shutdown still ends
the loop at once. The interrupt status is not restored, since a set status
would make every later pause return at once and bring the spin back.
Covered by
{{TestXceiverServerDomainSocket.testAcceptFailureIsRetriedAfterPause}}, which
drives the loop with the same mocked failure and allows at most 2 attempts in
500 ms. Without the change it fails with 17767 attempts. With the patch
{{TestXceiverServerDomainSocket}} (15 tests) and {{TestLocalChunkInputStream}}
pass, checkstyle is clean, and the attached reproduction fails because at most
one attempt is made within its one second window.
Found by code review of the datanode short-circuit read paths, as part of the
TLA+ verification effort under HDDS-15926, on commit
69d77beff60b9106e2517c36a7a1b2cc726af568. Checked against HDDS issues and
apache/ozone pull requests for duplicates before filing. Related but not a
duplicate: HDDS-11627 (introduced the loop). The attached patch is a proposal
for review. Generated with Specula (Claude Opus 5.5).
--
This message was sent by Atlassian Jira
(v10.3.26#10030026)
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]