Andrey Yarovoy created HDDS-17494:
-------------------------------------

             Summary: close evicted XceiverClients outside the 
XceiverClientManager cache lock
                 Key: HDDS-17494
                 URL: https://issues.apache.org/jira/browse/HDDS-17494
             Project: Apache Ozone
          Issue Type: Bug
          Components: Ozone Client
            Reporter: Andrey Yarovoy
            Assignee: Andrey Yarovoy


*Problem*

`XceiverClientManager` closes a datanode client while holding the `clientCache` 
monitor. Every
`acquireClient`/`releaseClient` call in the JVM synchronizes on that monitor. 
While one thread closes a client, all
other reads and writes therefore wait to acquire or release theirs.

A client is closed once it is evicted and its reference count is 0 
(`XceiverClientSpi.cleanup()`). That check, and
the close, run in three places, all under the monitor:

- **Removal listener.** `onRemoval` → `synchronized (clientCache)` → 
`setEvicted()` → `close()`. Guava calls the
listener synchronously, on the thread whose cache operation evicted the entry:
- `acquireClient` → `clientCache.get(key, loader)`, which can evict by size or 
expiry. It runs inside
`acquireClient`'s own `synchronized (clientCache)`.
- `releaseClient(client, invalidateClient=true)` → 
`clientCache.invalidate(key)`, inside `releaseClient`'s
`synchronized (clientCache)`.
- The periodic `clientCache.cleanUp()` added by HDDS-17491. Here the listener 
takes the monitor itself.
- **Release.** `releaseClient` → `decrementReference()` → `close()`, when an 
already evicted client is released.
- **Manager close.** `close()` → `invalidateAll()` / `cleanUp()`, through the 
listener.

How long a close can take:
- `XceiverClientGrpc.close()` calls `shutdownNow()`, then `awaitTermination` 
for up to 5 s per channel.
- `XceiverClientRatis.close()` closes its `RaftClient`. That closes the gRPC 
client, and with data streaming also the
`NettyClientStreamRpc`, which first flushes outstanding stream requests.

HDDS-15849 showed what this costs. When `XceiverClientGrpc.close()` waited 
about 100 ms for a graceful shutdown,
eviction-driven closes serialized every concurrent `acquireClient`. It was 
fixed by making the gRPC close fast
(`shutdownNow()`), not by moving the close out of the lock. Any close that 
still waits blocks everyone else for its
duration, for example a channel that doesn't terminate promptly, or a Ratis 
client with outstanding stream requests.

*Proposed change*

Decide under the lock which clients to close, and close them after leaving it.

- Change `XceiverClientSpi.setEvicted()` and `decrementReference()` to return 
whether the client must now be closed
(reference count 0 and evicted), without closing it.
- In `XceiverClientManager`, add clients that must be closed to a 
`pendingClose` queue while holding the monitor.
- After its `synchronized` block, every entry point closes what has been 
queued: `acquireClient`, `releaseClient`,
the periodic cleanup task and `close()`. This is needed because the removal 
listener runs inside cache calls that
are themselves made under the monitor.

Why this is safe:
- The lock still makes the reference-count check atomic with `acquireClient`'s 
get-and-increment.
- An evicted client with no references can't be acquired again, because it is 
no longer in the cache, and
`acquireClient` only takes clients from the cache. So closing it a moment 
later, outside the lock, can't race with
a new user.
- `close()` is already idempotent in both client implementations.

Effect: a slow close delays only the thread that triggered it, often the 
cleanup thread from HDDS-17491. It no longer
delays every other reader and writer.

*Testing*

- Unit test, with a client whose `close()` blocks on a latch:
- while it is being closed, `acquireClient` and `releaseClient` on other 
pipelines still complete;
- evict it through each path: size eviction in `acquireClient`, 
`releaseClient(..., true)`, the periodic
cleanup, and `close()`.
- Existing behaviour is unchanged:
- a referenced client isn't closed until its last release;
- every evicted, released client is eventually closed;
- `XceiverClientManager.close()` closes all clients before returning.
- `TestXceiverClientManagerCloseContention` and 
`TestXceiverClientManagerIdleEviction` still pass.



--
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