Skip to content

HDDS-16335. Change XceiverClientShortCircuit to support concurrent access - #11225

Open
echonesis wants to merge 3 commits into
apache:masterfrom
echonesis:test-HDDS-16335
Open

echonesis wants to merge 3 commits into
apache:masterfrom
echonesis:test-HDDS-16335

Conversation

@echonesis

@echonesis echonesis commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

What changes were proposed in this pull request?

XceiverClientShortCircuit did not consistently synchronize access to its connection state and other non-final fields.

This pull request uses the existing lock to protect all non-final fields and serialize connection setup, request registration and writes, and close transitions. Blocking reads, response waits, and receiver joins remain outside the lock.

Thread-safe final fields are used for response and timeout processing so they can proceed while a socket write is blocked. The client also remains one-shot: repeated connect() calls are allowed while open, but reconnecting after failure or close is rejected.

What is the link to the Apache JIRA

https://issues.apache.org/jira/browse/HDDS-16335

How was this patch tested?

Local Test

mvn -pl :ozone-integration-test -am test \
  -Dtest=TestXceiverClientManagerSC \
  -Dsurefire.failIfNoSpecifiedTests=false \
  -DskipShade -DskipRecon -DskipDocs

GitHub Actions CI: https://github.com/echonesis/ozone/actions/runs/34804142710

Generated-by: Codex (GPT-5)

@echonesis echonesis changed the title HDDS-16335. Change XceiverClientShortCircuit to support concurrent ac… HDDS-16335. Change XceiverClientShortCircuit to support concurrent access Sep 9, 2026
@echonesis
echonesis marked this pull request as draft September 10, 2026 01:51
@echonesis
echonesis marked this pull request as ready for review September 10, 2026 10:08

@Gargi-jais11 Gargi-jais11 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @echonesis for working on the PR. Left few review comemnts

Comment on lines 478 to 484
if (failure != null) {
LOG.error("Failed to send command {}", request, failure);
for (RequestEntry requestEntry : pending) {
requestEntry.fail(failure);
}
metrics.decrPendingContainerOpsMetrics(request.getCmdType());
metrics.addContainerOpsLatency(request.getCmdType(), System.nanoTime() - entry.getCreateTimeNs());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In sendRequest(), when the write faills we are currently fail all entries in pending, but metrics.decrPendingContainerOpsMetrics() and addContainerOpsLatency() are only called once for the current request, so other in-flight requests will leave infleted pending metrics.

@echonesis echonesis Sep 14, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. A write failure makes the shared domain socket unusable, so all requests still registered in sentRequests need terminal metrics accounting, rather than only the request whose write failed.

I changed this path to conditionally remove and collect the pending entries while holding the client lock, then fail each collected request and update its pending and latency metrics after releasing the lock. The conditional removal also prevents duplicate accounting against a concurrent response or timeout.

readDaemon.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
pending.forEach(entry -> entry.fail(new ClosedChannelException()));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here when the close fails, we are not tracking any metrics failures.

@echonesis echonesis Sep 14, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for mentioning.
close() now removes and collects pending entries under the client lock, then fails and accounts for each request outside the lock. Conditional removal ensures that repeated or concurrent close() calls cannot account for the same request more than once.

lock.unlock();
}
if (entry != null) {
entry.getFuture().completeExceptionally(e);

@Gargi-jais11 Gargi-jais11 Sep 11, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the generic catch (Throwable e) block,
if entry != null you are calling the entry.getFuture().completeExceptionally(e) and then pending.forEach(i -> i.fail(e)). If entry is still in sentRequests, it gets failed twice. CompletableFuture ignores the second completion, so this is harmless but redundant.

@echonesis echonesis Sep 14, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current receiver entry had already been removed from sentRequests, so it was not normally part of the snapshot taken later in the same catch block. However, snapshot-based failure handling could still overlap with another close or failure path.

I replaced the snapshots with explicit removal ownership. The receiver accounts for the entry it removed, while the common failure path handles only entries it can conditionally remove from sentRequests. This avoids duplicate future completion and metrics accounting across terminal paths.

}
readDaemon.interrupt();
}
pending = new ArrayList<>(sentRequests.values());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: After failing pending requests on close, consider clearing sentRequests under the lock.
Similarly in sendRequests and recieveResponseTask

@echonesis echonesis Sep 14, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The close, write failure, and receiver failure paths now remove pending requests under the client lock. Future completion and metrics updates happen after releasing the lock because completing a CompletableFuture may synchronously invoke callbacks, including close().

I used remove(key, entry) instead of snapshot followed by clear(), since timeout and response removal intentionally remain lock-free. Conditional removal gives each request a single terminal owner while ensuring failed entries are removed from the map.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants