[ 
https://issues.apache.org/jira/browse/HDDS-17436?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Gargi Jaiswal reassigned HDDS-17436:
------------------------------------

    Assignee: Gargi Jaiswal

> 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
>    Affects Versions: 2.3.0
>            Reporter: Siyao Meng
>            Assignee: Gargi Jaiswal
>            Priority: Minor
>         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]

Reply via email to