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

Gargi Jaiswal reassigned HDDS-17441:
------------------------------------

    Assignee: Gargi Jaiswal

> Interrupted short-circuit GetBlock fails with NullPointerException and leaves 
> the received block file stream open in XceiverClientShortCircuit
> ----------------------------------------------------------------------------------------------------------------------------------------------
>
>                 Key: HDDS-17441
>                 URL: https://issues.apache.org/jira/browse/HDDS-17441
>             Project: Apache Ozone
>          Issue Type: Bug
>    Affects Versions: 2.3.0
>            Reporter: Siyao Meng
>            Assignee: Gargi Jaiswal
>            Priority: Major
>         Attachments: 
> CR-6-release-short-circuit-stream-of-abandoned-request.patch, 
> TestBugCR6InterruptedReadLeaksBlockStream.java
>
>
> h3. Mechanism
> In {{XceiverClientShortCircuit.sendCommandWithTraceID}} an 
> {{InterruptedException}} from the wait for the response is caught and the 
> interrupt flag is restored, but neither a response nor an exception is 
> recorded, so {{Objects.requireNonNull(ioException)}} throws a 
> {{NullPointerException}}. {{BlockInputStream.getBlockDataUsingClient}} falls 
> back to gRPC only on {{IOException}}, so the NPE ends the read.
> The request stays registered, the datanode still answers it, and 
> {{ReceiveResponseTask}} puts the block file stream it receives with the 
> response into {{blockStreamCache}} under the request's call ID. Only 
> {{BlockInputStream.getBlockDataUsingSCClient}} takes streams out of the cache 
> ({{getFileInputStream}}), and only after {{getBlock}} returned normally, so 
> this stream is never claimed. {{close()}} does not touch the cache, and a 
> {{FileInputStream}} built from a received descriptor registers no cleaner, so 
> the descriptor stays open for the life of the process.
> The same leak happens when a validator rejects a successful GetBlock response 
> after it arrived. The validators {{BlockInputStream}} uses do not reject such 
> a response for a normally written block (code reading).
> h3. Trigger
> A thread that reads a key through short-circuit read is interrupted while it 
> waits for the GetBlock response, for example a task cancelled with 
> {{Future.cancel(true)}} or an executor shut down with {{shutdownNow}}. 
> Short-circuit read must be enabled ({{ozone.client.read.short-circuit}}, off 
> by default).
> h3. Impact
> * The read fails with a {{NullPointerException}} instead of an 
> {{IOException}}, so the gRPC fallback and the read retry handling are skipped.
> * Each such read leaves one open descriptor of the block file in the client 
> process, also after {{OzoneClient.close()}}. Repeated cancellations 
> accumulate them toward the process descriptor limit, and they keep the space 
> of deleted block files in use (code reading for these two consequences).
> h3. Reproduction
> PASS on unmodified source. [^TestBugCR6InterruptedReadLeaksBlockStream.java] 
> ({{hadoop-ozone/integration-test}}, needs the hadoop native library) starts a 
> one datanode MiniOzoneCluster with short-circuit read, writes one key, reads 
> it from a thread whose interrupt flag is set, closes the {{OzoneClient}} and 
> looks into the short-circuit client's stream cache by reflection. On 
> unmodified source the read fails with {{java.lang.NullPointerException}}, and 
> after the client is closed one stream with a valid descriptor is still in its 
> cache. A passing test means the defect is present. The reader sets its 
> interrupt before the read starts; an interrupt that arrives during the wait 
> goes through the same catch block (code reading). Run on macOS only.
> h3. Patch
> [^CR-6-release-short-circuit-stream-of-abandoned-request.patch], against 
> 69d77beff60b9106e2517c36a7a1b2cc726af568. It also applies to master at 
> 1acf315d546.
> On an interrupt the client now records an {{InterruptedIOException}}, as 
> {{sendCommand}} already does for its own wait, so the call fails with an 
> {{IOException}} and the gRPC fallback runs (on the interrupted thread the 
> fallback also fails with an {{IOException}}). When the caller gives up on a 
> GetBlock (interrupted, or a validator rejects the response) it completes the 
> request's future exceptionally and then removes and closes any stream already 
> cached for the request. {{ReceiveResponseTask}} closes the stream itself when 
> completing the future fails because the caller already gave up. The receiver 
> caches the stream before it completes the future and the caller completes the 
> future before it looks in the cache, so one of the two always finds the 
> stream, and {{ConcurrentHashMap.remove}} hands it to only one of them, so it 
> is closed exactly once. Successful reads are unchanged.
> {{TestXceiverServerDomainSocket.testClientReleasesStreamOfAbandonedRequest}} 
> covers both ways of giving up and a normal GetBlock on the same connection. 
> It records every stream the client receives (through a recording stream cache 
> set by reflection) and checks that the streams of the abandoned requests were 
> closed, not only removed from the cache. Without the change it fails because 
> the interrupted call throws a {{java.lang.NullPointerException}} instead of a 
> {{java.io.InterruptedIOException}}, and with that check relaxed it fails 
> because the abandoned request's stream is still in the cache. With the cache 
> removal kept but the two closes left out, it fails because the abandoned 
> streams are still open. With the change it passes, together with the rest of 
> {{TestXceiverServerDomainSocket}}, {{TestXceiverClientManagerSC}}, 
> {{TestLocalChunkInputStream}}, {{TestDomainSocketFactory}}, 
> {{TestBlockInputStream}} and {{TestClientThreadShutdown}}, and checkstyle on 
> {{hadoop-hdds/client}} and {{hadoop-ozone/integration-test}} is clean. With 
> the change the reproduction above fails at its first assertion: the 
> interrupted read fails with an {{IOException}} after the gRPC fallback, which 
> sees the same interrupt, was attempted.
> The test also sets {{DefaultMetricsSystem.setMiniClusterMode(true)}} in the 
> suite setup, because the short-circuit client starts the metrics system, 
> after which a later {{ContainerMetrics.create}} in the same suite would fail 
> with a duplicate source name. The new test imports conflict textually with 
> the patches of HDDS-17436 and HDDS-17437 (adjacent AssertJ import lines); a 
> rebase resolves it.
> 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 
> duplicates: HDDS-16517 (open pull request apache/ozone#11294, which 
> simplifies the client and also turns the interrupt into an 
> {{InterruptedIOException}}, without releasing the stream), HDDS-16335 (open 
> pull request apache/ozone#11225 on concurrent access in the same class, which 
> changes {{sendCommandWithTraceID}}, {{sendRequest}} and 
> {{ReceiveResponseTask}}, so it conflicts with this patch too), HDDS-16334 
> (changed the cache key type). Merging with #11294 means: its interrupt 
> handling replaces the first part of this patch; it moves the validator loop 
> from {{sendCommandWithTraceID}} into {{sendCommand}}, so the release of the 
> stream on a rejected response moves there with it; and it makes 
> {{sendCommandInternal}} private, so the new test sends its delaying Echo 
> another way. #11294 does not change {{blockStreamCache}}. 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