Siyao Meng created HDDS-17271:
---------------------------------

             Summary: KeyOutputStream releases a request permit it never 
acquired when write, flush or hsync is interrupted
                 Key: HDDS-17271
                 URL: https://issues.apache.org/jira/browse/HDDS-17271
             Project: Apache Ozone
          Issue Type: Bug
            Reporter: Siyao Meng
         Attachments: CR-4-acquire-key-output-stream-permit-before-try.patch, 
TestBugCR4InterruptedCallReleasesPermit.java

h3. Mechanism
{{KeyOutputStream.write}}, {{flush}} and {{hsync}} call 
{{KeyOutputStreamSemaphore.acquire()}} as the first statement inside {{try}} 
and {{release()}} in {{finally}}. When the thread is interrupted, either 
already interrupted on entry or while it waits for a permit, {{acquire()}} 
throws {{InterruptedIOException}} without taking a permit, and the {{finally}} 
block still releases one. Every such call adds one permit to the stream's 
semaphore for the rest of its life. The pattern came in with HDDS-11184.

{{ozone.client.key.write.concurrency}} defaults to 1 and is forced to 1 unless 
{{ozone.client.hbase.enhancements.allowed=true}}. HDDS-11184 added it as a 
safety fallback to the behaviour before HDDS-9844, where writes and hsyncs on 
one key did not overlap. After one interrupted call that fallback no longer 
holds.

h3. Trigger
# Two or more threads use the same key output stream (for example a writer 
thread and a thread calling {{hflush}} or {{hsync}}, as HBase does).
# One of them is interrupted while it waits for the permit, or calls {{write}}, 
{{flush}} or {{hsync}} with its interrupt flag already set 
({{Future.cancel(true)}}, {{ExecutorService.shutdownNow}}).

No configuration change and no fault are needed.

h3. Impact
* After the interrupted call, the stream admits two calls at once at the 
default concurrency of 1, and more after further interrupts (run, see 
Reproduction).
* {{write}} (around {{handleWrite}}) and {{close}} still take the stream's 
write lock, so two writes, or a write and close, still do not overlap. The 
calls that do overlap are {{flush}} and {{hsync}} with each other and with a 
{{write}}, which today only happens with the HBase enhancements enabled and the 
limit raised. {{hflush}} and {{hsync}} with {{ozone.fs.hsync.enabled=false}} 
reach {{KeyOutputStream.flush}} without the monitor that 
{{OzoneOutputStream.flush}} holds, so two {{hflush}} callers on one ofs/o3fs 
stream can overlap too.
* HDDS-16979 (open) reports an hsync or flush that never returns when two such 
calls overlap and two blocks fail in a row, and notes that with the default 
concurrency of 1 the semaphore keeps the second call out. This defect removes 
that protection on a stream that saw one interrupted call. Argued from the 
code, not run.
* No data loss or wrong data was observed. Erasure coded keys 
({{ECKeyOutputStream}} does not use the semaphore) and the S3 Gateway (one 
request thread per stream, no flush or hsync) are not affected.

h3. Reproduction
PASS on unmodified source, deterministic, no cluster. 
[^TestBugCR4InterruptedCallReleasesPermit.java] ({{hadoop-ozone/client}}) uses 
the setup of {{TestKeyOutputStream.testConcurrentWriteLimitOne}}: a spied 
{{KeyOutputStream}} with a real {{KeyOutputStreamSemaphore(1)}} and a 
{{handleWrite}} that blocks until released.
* {{interruptedWaiterAdmitsSecondWrite}}: a first write holds the only permit, 
a second write is interrupted while it waits, and a third write then gets past 
the semaphore (0 calls queued on it instead of 1) and waits on the write lock.
* {{interruptedCallsAddPermitsForGood}}: one interrupted {{write}}, {{flush}} 
and {{hsync}} each on an idle stream, then four writes all get past the 
semaphore (0 queued instead of 3).
A passing test means the defect is present. With the patch both tests fail on 
the queue length assertion (1 and 3 queued).

h3. Patch
[^CR-4-acquire-key-output-stream-permit-before-try.patch], against 
b10a7d427bdf83807a7e48a1be507d3b02d5a503. It also applies to master at 
e0de543cbe9.

{{write}}, {{flush}} and {{hsync}} call {{acquire()}} before the {{try}} block, 
so {{release()}} only runs after a permit was taken. Nothing else changes; 
uninterrupted calls behave as before.

Covered by {{testInterruptedCallDoesNotReleasePermit}} in the existing 
{{TestKeyOutputStream}}: a write waiting for the permit is interrupted, 
{{flush}} and {{hsync}} are called on an interrupted thread, and a further 
write must still queue on the semaphore while the first write holds the permit, 
then complete once it is released. Without the change it fails with {{expected: 
1 but was: 0}}. With it the {{hadoop-ozone/client}} unit tests (339) and 
{{TestHSync}} on a mini cluster (43) pass, and checkstyle is clean.

Found by code review of the S3 gateway object put and key commit paths, as part 
of the TLA+ verification effort under HDDS-15926, on commit 
b10a7d427bdf83807a7e48a1be507d3b02d5a503. Checked against HDDS issues and 
apache/ozone pull requests for duplicates before filing. The attached patch is 
a proposal for review. Generated with Specula (Claude Opus 5.5).



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to