tchivs opened a new issue, #11010:
URL: https://github.com/apache/rocketmq/issues/11010
### Before Creating the Bug Report
- [x] I found a bug, not just asking a question, which should be created in
GitHub Discussions.
- [x] I have searched the GitHub Issues and GitHub Discussions of this
repository and believe that this is not a duplicate.
- [x] I have confirmed that this bug belongs to the current repository, not
other repositories of RocketMQ.
### Runtime platform environment
OS: Linux 5.14.0 (x86_64), containerized
Client embedded in an Apache Flink 2.2.1 TaskManager (producer only)
Broker: 5.1.4, single node
### RocketMQ version
branch: develop
version: reproduced on 5.3.1; the same code is present unchanged in 5.5.1
and on develop
Git commit id: 88846a0a5b550116d183f62c1a468e75ca3f61d4
### JDK Version
Compiler: OpenJDK 17.0.x
Runtime: OpenJDK 21.0.x (Flink TaskManager)
OS: Linux 5.14.0
### Describe the Bug
When `writeAndFlush` fails, `NettyRemotingAbstract` drops the netty-side
`f.cause()` in two
independent places, so the reason a send failed is unrecoverable both
programmatically and from the
logs.
`remoting/src/main/java/org/apache/rocketmq/remoting/netty/NettyRemotingAbstract.java`
(develop):
1. `invoke0`, the `writeAndFlush` listener — `f.cause()` is available and
used nowhere:
```java
channel.writeAndFlush(request).addListener((ChannelFutureListener) f -> {
if (f.isSuccess()) {
responseFuture.setSendRequestOK(true);
return;
}
requestFail(opaque); //
cause not passed
log.warn("send a request command to channel <{}>, channelId={}, failed.",
RemotingHelper.parseChannelRemoteAddr(channel), channel.id()); //
throwable not passed
});
```
2. `requestFail(final int opaque)` — never calls `setCause`:
```java
private void requestFail(final int opaque) {
ResponseFuture responseFuture = responseTable.remove(opaque);
if (responseFuture != null) {
responseFuture.setSendRequestOK(false);
responseFuture.putResponse(null); // no setCause(...) anywhere
...
}
}
```
The consumer side is already written to carry a cause —
`ResponseFuture#executeInvokeCallback`
builds the exception *from* `getCause()`:
```java
if (!isSendRequestOK()) {
invokeCallback.operationFail(
new RemotingSendRequestException(channel.remoteAddress().toString(),
getCause()));
}
```
Because `requestFail` never populates the field, `getCause()` is always
`null` here. The plumbing
exists; only the assignment is missing. Both the synchronous path
(`invokeSyncImpl`, which rethrows
`e.getCause()` from the `CompletableFuture`) and the async path
(`invokeAsyncImpl`) inherit the
null cause.
Two further sites drop a throwable that is in hand:
3. `invokeOnewayImpl` listener: `log.warn("send a request command to channel
<" + channel.remoteAddress() + "> failed.")` — no `f.cause()`.
4. `invokeOnewayImpl` catch block: `log.warn("write send a request command
to channel <" + channel.remoteAddress() + "> failed.")` — `e` is caught and
wrapped into the thrown exception, but never logged.
`failFast(Channel)` also routes through `requestFail`. It is invoked from
`NettyRemotingClient`'s `close()` handler, so "the channel was closed" is a
known, accurate cause
that could be reported instead of nothing.
### Steps to Reproduce
Unit-level reproduction, no broker required. Make `writeAndFlush` return an
already-failed promise
and assert on what the caller receives:
```java
@Test
public void testWriteFailurePropagatesNettyCauseToCaller() throws
InterruptedException {
final Throwable writeFailure =
new OutOfMemoryError("Cannot reserve 16777216 bytes of direct buffer
memory");
Channel channel = new MockChannel() {
@Override
public ChannelFuture writeAndFlush(Object msg) {
DefaultChannelPromise promise =
new DefaultChannelPromise(this,
ImmediateEventExecutor.INSTANCE);
promise.setFailure(writeFailure);
return promise;
}
@Override
public LocalAddress remoteAddress() {
return new LocalAddress("write-failure-test");
}
};
final Semaphore semaphore = new Semaphore(0);
final AtomicReference<Throwable> observed = new AtomicReference<>();
remotingAbstract.invokeAsyncImpl(channel,
RemotingCommand.createRequestCommand(1, null), 3000,
new InvokeCallback() {
@Override public void operationComplete(ResponseFuture
responseFuture) { }
@Override public void operationSucceed(RemotingCommand response)
{ }
@Override public void operationFail(Throwable throwable) {
observed.set(throwable);
semaphore.release();
}
});
assertThat(semaphore.tryAcquire(1, 10, TimeUnit.SECONDS)).isTrue();
// walk getCause() transitively
assertThat(causeChainOf(observed.get())).contains(writeFailure);
}
```
On current `develop` this fails: `writeFailure` appears nowhere in the cause
chain.
Production reproduction: run two Flink jobs, each with its own producer, in
one TaskManager JVM
whose `-XX:MaxDirectMemorySize` leaves less than one netty arena chunk (16
MiB) free. The second
job's producer fails every send.
### What Did You Expect to See?
The netty write failure reaches the caller and the log:
- `requestFail` records `f.cause()` on the `ResponseFuture`, so
`RemotingSendRequestException.getCause()` is the real failure.
- The `log.warn` calls in `invoke0` and `invokeOnewayImpl` pass the
throwable.
- `failFast` reports channel closure as the cause rather than nothing.
### What Did You See Instead?
`RemotingSendRequestException` with `getCause() == null`, and a one-line
`send a request command to channel <...> failed.` with no stack trace.
In our case the discarded cause was:
```
java.lang.OutOfMemoryError: Cannot reserve 16777216 bytes of direct buffer
memory
(allocated: 595741295, limit: 606664136)
at java.base/java.nio.Bits.reserveMemory(Bits.java:178)
at java.base/java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:111)
at io.netty.buffer.PoolArena$DirectArena.allocateDirect(PoolArena.java:632)
at
io.netty.buffer.PooledByteBufAllocator.newDirectBuffer(PooledByteBufAllocator.java:395)
...
```
Direct memory was 98.2% exhausted, so netty could not allocate a 16 MiB
arena chunk. Every send
failed, each failure closed the channel, and the client entered a reconnect
storm (14045 CONNECT /
13869 ACTIVE / 28125 CLOSE events). Because the cause was discarded at both
the exception and the
log, the only observable symptom was "sends fail and the client reconnects
forever" — pointing at
the network or the broker, not at memory. We reached the real cause only
after independently
obtaining the netty `exceptionCaught` log, which prints `f.cause()` itself.
The fix is small and self-contained: thread the cause through `requestFail`,
and pass the throwable
to the four `log.warn` calls. `ResponseFuture` already stores and consumes
it.
### Additional Context
Happy to open a PR. Our patch against 5.3.1 (identical shape applies to
develop):
- `requestFail(final int opaque)` -> `requestFail(final int opaque, final
Throwable cause)`, calling
`responseFuture.setCause(cause)` before `putResponse(null)` — order
matters, `putResponse`
releases the latch the synchronous caller is blocked on.
- `invoke0` listener: `requestFail(opaque, f.cause())` and `log.warn(...,
f.cause())`.
- `failFast`: `requestFail(opaque, new ClosedChannelException())`, accurate
because the only caller
is `NettyRemotingClient`'s `close()` handler.
- `invokeOnewayImpl`: pass `f.cause()` in the listener, and `e` in the catch
block; switch both to
parameterized logging.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]