Siyao Meng created HDDS-17443:
---------------------------------
Summary: Domain socket Receiver logs an NPE for every response
without a block file and sends no reply to a GetBlock that is not granted access
Key: HDDS-17443
URL: https://issues.apache.org/jira/browse/HDDS-17443
Project: Apache Ozone
Issue Type: Bug
Reporter: Siyao Meng
Attachments:
CR-1-reply-to-getblock-without-block-file-in-domain-socket-receiver.patch,
TestBugCR1NullBlockFileClose.java, TestBugMC2GetBlockWithoutFlagNoResponse.java
h3. Mechanism
{{Receiver.ProcessRequestTask.run}} attaches a block file to the response only
for a GetBlock that returns SUCCESS, and both places that use the file assume
this is the same as "the response has a file":
* {{Receiver.sendResponse}} closes {{entry.getFile()}} in its {{finally}} block
without a null check, and catches only {{IOException}}. For every response
without a file (Echo, a GetBlock that does not succeed, the UNSUPPORTED_REQUEST
reply) the close throws a {{NullPointerException}} after the response has been
flushed. The exception skips {{ContainerMetrics.incContainerLocalOpsLatencies}}
and is logged by {{ProcessRequestTask.run}} as "Failed to processRequest" at
ERROR with the stack trace.
* {{KeyValueHandler.handleGetBlock}} opens and parks a block file only when the
request sets {{requestShortCircuitAccess}} and the domain socket server is
started, and returns SUCCESS with {{shortCircuitAccessGranted=false}}
otherwise. {{ProcessRequestTask.run}} fetches the file for every SUCCESS
GetBlock, {{Objects.requireNonNull}} throws, the exception is only logged, and
no response is sent.
HDDS-11627 closed the block's stream only when there was one; HDDS-12883
brought the close back for the block file without that null check. The non-null
requirement on the file dates from HDDS-11627.
h3. Trigger
* Logged NPE: every Echo, and every short-circuit GetBlock that does not
succeed. The most frequent one is the routine fallback when the local datanode
has not applied the PutBlock yet ({{UNKNOWN_BCSID}}, see the comment in
{{BlockInputStream.getBlockDataUsingClient}}). Run for {{UNKNOWN_BCSID}} and
{{NO_SUCH_BLOCK}}; other error results such as {{CONTAINER_NOT_FOUND}} or a
token failure take the same path.
* No reply: a GetBlock on the domain socket without
{{requestShortCircuitAccess}}. The field is optional and defaults to false, so
this is a valid request, but the in-tree reader
({{BlockInputStream.getBlockDataUsingSCClient}}) always sets it. By code
reading the same happens when the domain socket server is stopped between the
block lookup and the {{isStarted()}} check (not run); there {{closeAllPeers}}
disconnects the client anyway.
h3. Impact
* Every Echo and every failed short-circuit GetBlock adds an ERROR with a stack
trace to the datanode log, and {{localLatencyNsEcho}} and
{{localLatencyNsGetBlock}} miss those samples ({{numLocalEcho}} and
{{numLocalGetBlock}} are correct). The client receives a correct response and
falls back to gRPC as designed. Reproduced. No in-tree reader sends Echo on the
domain socket (the Echo load generator in freon uses gRPC), so in practice the
errors come from failed GetBlocks.
* A client that sends a GetBlock without the flag gets no reply and waits for
its own read timeout. Only that request is affected: the read thread goes back
to the pool and the same connection serves the next request normally
(reproduced with a single read thread). Other connections and clients are not
affected. No in-tree client sends such a request, so today this only matters
for other clients of the protocol.
h3. Reproduction
[^TestBugCR1NullBlockFileClose.java] and
[^TestBugMC2GetBlockWithoutFlagNoResponse.java]
({{hadoop-ozone/integration-test}}) run a real {{XceiverServerDomainSocket}}
with {{HddsDispatcher}} and {{KeyValueHandler}} and send requests framed as the
short-circuit client frames them. They need the native libhadoop
({{-Phadoop-native-lib}}) and change no source. Both pass on unmodified source,
so a passing test means the defect is present.
* The first sends an Echo, a GetBlock with BCSID 5 against a replica at 0
({{UNKNOWN_BCSID}}) and a GetBlock of a missing block ({{NO_SUCH_BLOCK}}). All
three get the right response, and the datanode logs three
{{NullPointerException: Cannot invoke "java.io.RandomAccessFile.close()"}}
errors. {{numLocalGetBlock}} is 3 with 1 latency sample (the granted control
request) and {{numLocalEcho}} is 1 with 0 samples.
* The second sends a GetBlock with the flag (answered with its descriptor),
then the same GetBlock without the flag: no reply within the 3 s read timeout,
and the datanode logs {{NullPointerException: Failed to get block file for
block}}. A third GetBlock on the same connection is answered normally.
h3. Patch
[^CR-1-reply-to-getblock-without-block-file-in-domain-socket-receiver.patch],
against 69d77beff60b9106e2517c36a7a1b2cc726af568. It also applies to master at
1acf315d546.
Two changes in {{Receiver}}: {{sendResponse}} closes the block file only when
there is one, and {{ProcessRequestTask.run}} fetches the parked file only when
the GetBlock response grants short-circuit access. A SUCCESS GetBlock that
grants none is then sent as the handler built it, with its BlockData and
without a descriptor, and by code reading the in-tree client takes its existing
"Short-circuit access is denied" path (fails the request, disables
short-circuit for the configured interval) and reads through gRPC.
{{KeyValueHandler.handleGetBlock}} grants access exactly when it parks a file,
so the handler is unchanged. The two changes go together: with only the second
one, the reply that is now sent would hit the first problem right after it is
flushed. The new tests pass the read executor argument of the
{{XceiverServerDomainSocket}} constructor like the existing tests; after the
change in HDDS-17442, which removes that argument, they drop it as well.
Covered by {{testResponseWithoutBlockFile}} and
{{testGetBlockWithoutShortCircuitAccess}} in the existing
{{TestXceiverServerDomainSocket}}. Without the change the first fails because
the Receiver logged "Failed to processRequest" for the Echo and the failed
GetBlock, and the second fails with {{SocketTimeoutException}}. With it
{{TestXceiverServerDomainSocket}}, {{TestXceiverClientManagerSC}},
{{TestLocalChunkInputStream}} and {{TestDomainSocketFactory}} (19 tests) and
{{TestKeyValueHandler}} (43 tests) pass, checkstyle is clean, and both
reproductions fail because the error is gone and the reply arrives.
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-12883 (added the close of the
block file after it is sent), HDDS-16918 (fallback to gRPC is not logged on the
datanode), 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]