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

Gargi Jaiswal reassigned HDDS-17444:
------------------------------------

    Assignee: Gargi Jaiswal

> Domain socket Receiver keeps the connection after a response send fails 
> partway, so the next response follows a cut off frame
> -----------------------------------------------------------------------------------------------------------------------------
>
>                 Key: HDDS-17444
>                 URL: https://issues.apache.org/jira/browse/HDDS-17444
>             Project: Apache Ozone
>          Issue Type: Bug
>    Affects Versions: 2.3.0
>            Reporter: Siyao Meng
>            Assignee: Gargi Jaiswal
>            Priority: Major
>         Attachments: 
> MC-8-close-domain-socket-connection-after-failed-response-send.patch, 
> TestBugMC8PartialResponseKeepsConnection.java
>
>
> h3. Mechanism
> {{Receiver.sendResponse}} writes the version, the command type, the 
> length-prefixed response and, for a granted GetBlock, the file descriptor. If 
> any of these writes fails, for example on the send timeout 
> ({{ozone.client.write.timeout}}, 30 s by default) after part of the response 
> is on the wire, its {{catch (Throwable)}} only logs "Failed to send response" 
> and closes the block file. The connection stays open and the read loop keeps 
> handling requests, so the next response is written right after the cut off 
> one.
> The client ({{XceiverClientShortCircuit.ReceiveResponseTask}}) reads the 
> length the first frame declared and fills it with the bytes that follow, then 
> matches the parsed message by its clientId and callId, which are the last 
> fields of the message. By code reading the same happens when the descriptor 
> send fails after the response was flushed: the client then takes the first 
> byte of the next frame as the descriptor byte and the stream is out of step 
> by one byte (not run).
> The log and continue handling dates from HDDS-11627.
> h3. Trigger
> Responses are pending on a connection and the client does not read for the 
> whole send timeout, so the datanode's socket buffer fills and a send times 
> out partway. The client's reader is a dedicated thread, so this needs the 
> client's receive thread to stall for that long. By default the client's own 
> request timeout ({{ozone.client.read.timeout}}, 30 s) equals the datanode's 
> send timeout, so the request whose response is cut has usually already failed 
> on the client when the send fails; a later request completes from the stream 
> only if it was sent while the receive thread alone was stalled.
> h3. Impact
> * Shown on a real {{XceiverServerDomainSocket}}: after a send timeout the 
> connection stays open, and the bytes on it are the first part of a 4 MB 
> response (8192 bytes on macOS with the default socket buffer) followed by the 
> complete next response.
> * By code reading, the client then parses the next bytes as the rest of the 
> cut-off frame. If too few bytes follow, its read blocks until 
> {{ozone.client.read.timeout}} and then fails every pending request. Otherwise 
> the merged bytes are parsed as one message, which usually fails to parse or 
> carries a callId the client no longer waits for, and then the receive loop 
> also fails every pending request. Both outcomes end in the gRPC fallback. 
> Which one occurs depends on where the cut falls. In a run with the real 
> {{XceiverClientShortCircuit}} (receive thread held until the datanode logged 
> the failed send, three Echo requests pending, not attached), all three 
> requests stayed pending until their request timers fired, and the client 
> still treated the connection as open.
> * By code reading, not run: when the cut-off bytes parse as one message for 
> the same block, protobuf merges the two chunk lists, and since 
> {{BlockInputStream}} computes chunk positions from chunk lengths, a read 
> bound to that descriptor could return bytes that do not belong at that 
> offset. This was not shown. Either way, the framing on the connection can no 
> longer be trusted.
> h3. Reproduction
> [^TestBugMC8PartialResponseKeepsConnection.java] 
> ({{hadoop-ozone/integration-test}}) runs a real {{XceiverServerDomainSocket}} 
> with {{HddsDispatcher}} and {{KeyValueHandler}}, a 1 s send timeout and the 
> native libhadoop ({{-Phadoop-native-lib}}), and changes no source. It sends 
> two Echo requests on one connection, the first asking for a 4 MB response, 
> and reads nothing until the datanode has logged the failed send. The client's 
> read then times out on the open connection, and the bytes it received are the 
> first part of the first response (8192 of 4194341 bytes on macOS with the 
> default socket buffer) followed by the second response, compared byte for 
> byte. Echo with a large response only makes the send timeout deterministic; 
> GetBlock responses and descriptors use the same write path. The test passes 
> on unmodified source, so a passing test means the defect is present.
> h3. Patch
> [^MC-8-close-domain-socket-connection-after-failed-response-send.patch], 
> against 69d77beff60b9106e2517c36a7a1b2cc726af568. It also applies to master 
> at 1acf315d546.
> When a send fails, {{sendResponse}} now closes the peer after logging, while 
> it still holds the connection's send lock, so no other response can follow 
> the partial one. The read loop then ends through its normal path and 
> {{XceiverServerDomainSocket.closePeer}} releases the connection once, as 
> before. Responses still queued for that connection fail at once and close 
> their block files. On the client, the receive loop fails on the truncated 
> message and fails every pending request at once, as it does when the datanode 
> closes an idle connection, and the reads fall back to gRPC. In the client run 
> described under Impact, with the patch the three pending requests failed 
> within a few milliseconds with {{InvalidProtocolBufferException}} (the input 
> ended in the middle of a field) through the receive loop's generic {{catch}}, 
> and a later request failed with "... is not open". Nothing changes while 
> sends succeed. A client side check cannot make a stream with a partial frame 
> parseable again, so closing is the repair.
> Covered by {{testConnectionClosedAfterFailedResponse}} in the existing 
> {{TestXceiverServerDomainSocket}}: without the change the client's read times 
> out on the open connection ({{SocketTimeoutException}}), with it the client 
> receives part of the first response and then the end of the stream, and the 
> second response is not among the received bytes. This patch and the patch of 
> the separate issue HDDS-17443 add the same test helpers ({{sendRequest}}, 
> {{getEchoRequest}}) to {{TestXceiverServerDomainSocket}}, so whichever is 
> applied second needs a trivial rebase that drops the duplicate. The new test 
> passes the read executor argument of the {{XceiverServerDomainSocket}} 
> constructor like the existing tests; after the change in HDDS-17442, which 
> removes that argument, it drops it as well. With the change 
> {{TestXceiverServerDomainSocket}}, {{TestXceiverClientManagerSC}}, 
> {{TestLocalChunkInputStream}} and {{TestDomainSocketFactory}} (18 tests) and 
> {{TestKeyValueHandler}} (43 tests) pass, checkstyle is clean, and the 
> reproduction fails because the stream ends instead.
> Found by TLA+ model checking and code review of the datanode short-circuit 
> read paths under HDDS-15926, on commit 
> 69d77beff60b9106e2517c36a7a1b2cc726af568. Checked against HDDS issues and 
> apache/ozone pull requests for duplicates before filing. Related but not 
> duplicates: HDDS-11627 (added GetBlock on the short-circuit channel and this 
> send path), HDDS-16335 (concurrent access on the client side of the domain 
> socket). 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