RockteMQ-AI commented on issue #10747:
URL: https://github.com/apache/rocketmq/issues/10747#issuecomment-5155546516

   **Automated Fix Proposal (v1)**
   
   Code base: `a06836dd564e` (develop)
   
   ## Summary
   
   **Root Cause:** `invokeAsyncImpl()` chains `whenComplete → thenAccept → 
exceptionally`, firing `operationComplete` before 
`operationSucceed`/`operationFail` — violating the `InvokeCallback` contract.
   
   **Fix Strategy:** Replace the three-stage pipeline with a single terminal 
`whenComplete`:
   - Success: `operationSucceed` → `operationComplete`
   - Failure: `operationFail` → `operationComplete`
   - Callback exceptions isolated from invocation outcome routing
   
   **Files to Modify:** 1 file, 1 method (~17 lines)
   - 
`remoting/src/main/java/org/apache/rocketmq/remoting/netty/NettyRemotingAbstract.java`
   
   **Test Plan:** 7 deterministic unit tests (no network/timers)
   - Callback ordering on success/failure paths
   - Exception isolation (callback throws do not cross-contaminate)
   - #9119/#9120 raw-exception unwrap preservation
   
   **Backward Compatibility:** No breaking changes — behavior corrected to 
match documented contract.
   
   **Risk:** Low — localized change, clear reference implementation 
(`ResponseFuture.executeInvokeCallback`).
   
   <details>
   <summary>Full Specification</summary>
   
   # Fix Specification — Issue #10747
   
   ## `InvokeCallback` completion-order violation in 
`NettyRemotingAbstract.invokeAsyncImpl`
   
   ---
   
   ## 1. Root Cause Analysis
   
   | Field | Value |
   |---|---|
   | **File** | 
`remoting/src/main/java/org/apache/rocketmq/remoting/netty/NettyRemotingAbstract.java`
 |
   | **Method** | `invokeAsyncImpl(Channel, RemotingCommand, long, 
InvokeCallback)` |
   | **Lines** | 677–693 |
   
   ### Current code (broken)
   
   ```java
   invokeImpl(channel, request, timeoutMillis)
       .whenComplete((v, t) -> {                    // ① operationComplete 
fires FIRST
           if (t == null) {
               invokeCallback.operationComplete(v);
           } else {
               ResponseFuture responseFuture = new ResponseFuture(...);
               responseFuture.setCause(t);
               invokeCallback.operationComplete(responseFuture);
           }
       })
       .thenAccept(responseFuture ->                // ② operationSucceed fires 
SECOND
           invokeCallback.operationSucceed(responseFuture.getResponseCommand()))
       .exceptionally(t -> {                        // ③ operationFail fires 
THIRD
           invokeCallback.operationFail(ExceptionUtils.getRealException(t));
           return null;
       });
   ```
   
   ### Contract (from `InvokeCallback` Javadoc, introduced in #7321/#7322)
   
   > `operationComplete` is expected to be invoked **after** 
`operationSucceed(RemotingCommand)` or `operationFail(Throwable)`.
   
   The correct reference implementation is 
`ResponseFuture.executeInvokeCallback()` (lines 62–80), which calls 
`operationSucceed`/`operationFail` first and `operationComplete` last.
   
   ### Two distinct bugs
   
   | # | Bug | Mechanism |
   |---|---|---|
   | **B1** | **Ordering reversal** — `operationComplete` fires before 
`operationSucceed` or `operationFail`. | `whenComplete` is the first stage in 
the chain; its action runs before the dependent `thenAccept`/`exceptionally` 
stages. |
   | **B2** | **Callback-exception cross-contamination** — if 
`operationComplete` (or `operationSucceed`) throws, the dependent stage becomes 
exceptional, `operationSucceed` is skipped on success, and the user-thrown 
exception is routed to `operationFail` as though the remoting invocation itself 
had failed. | `thenAccept` and `exceptionally` are *dependent* stages — an 
exception in a predecessor propagates downstream and suppresses the normal 
path. |
   
   ---
   
   ## 2. Fix Strategy
   
   Replace the three-stage pipeline with a **single terminal `whenComplete`** 
that inspects the outcome of `invokeImpl` and dispatches callbacks in the 
correct order, isolated from each other.
   
   ### Pseudocode
   
   ```java
   invokeImpl(channel, request, timeoutMillis)
       .whenComplete((responseFuture, throwable) -> {
           if (throwable == null) {
               // Success path
               
invokeCallback.operationSucceed(responseFuture.getResponseCommand());
               invokeCallback.operationComplete(responseFuture);
           } else {
               // Failure path — mirror ResponseFuture.executeInvokeCallback
               ResponseFuture failedFuture = new ResponseFuture(
                   channel, request.getOpaque(), request, timeoutMillis, null, 
null);
               failedFuture.setCause(throwable);
               
invokeCallback.operationFail(ExceptionUtils.getRealException(throwable));
               invokeCallback.operationComplete(failedFuture);
           }
       });
   ```
   
   ### Step-by-step changes
   
   1. **Delete** the existing `.whenComplete(...)`, `.thenAccept(...)`, and 
`.exceptionally(...)` calls on lines 680–693.
   2. **Add** a single `.whenComplete((responseFuture, throwable) -> { ... })` 
block.
   3. **Success branch** (`throwable == null`):
      - Call 
`invokeCallback.operationSucceed(responseFuture.getResponseCommand())`.
      - Then call `invokeCallback.operationComplete(responseFuture)`.
   4. **Failure branch** (`throwable != null`):
      - Build the synthetic `ResponseFuture` exactly as the current code does 
(preserving the behavior introduced by #9119/#9120 — `setCause(t)` on the new 
`ResponseFuture`).
      - Call 
`invokeCallback.operationFail(ExceptionUtils.getRealException(throwable))`.
      - Then call `invokeCallback.operationComplete(failedFuture)`.
   5. **Do not** wrap individual callback invocations in try/catch at this 
layer. `ResponseFuture.executeInvokeCallback` does not either — exception 
propagation to the caller of `invokeAsyncImpl` is the existing contract. If 
hardening is desired, it should be done in a separate, explicitly-scoped change.
   
   ---
   
   ## 3. Files to Modify
   
   | File | Change |
   |---|---|
   | 
`remoting/src/main/java/org/apache/rocketmq/remoting/netty/NettyRemotingAbstract.java`
 | Rewrite `invokeAsyncImpl` (lines 677–693) as described above. |
   
   No other source files require changes. `InvokeCallback.java`, 
`ResponseFuture.java`, and all callers of `invokeAsyncImpl` remain untouched.
   
   ---
   
   ## 4. Test Plan
   
   All tests use already-completed `CompletableFuture<ResponseFuture>` 
instances — no network, timers, or scheduling.
   
   ### 4.1 New test class
   
   
`remoting/src/test/java/org/apache/rocketmq/remoting/netty/NettyRemotingAbstractInvokeAsyncImplTest.java`
   
   ### 4.2 Test cases
   
   | # | Test name | What it verifies |
   |---|---|---|
   | T1 | `successCallbackOrder` | On a normally-completed future, 
`operationSucceed` is invoked **before** `operationComplete` (Mockito 
`InOrder`). `operationFail` is never invoked. |
   | T2 | `failureCallbackOrder` | On an exceptionally-completed future 
(sentinel `RemotingException`), `operationFail` is invoked **before** 
`operationComplete`. `operationSucceed` is never invoked. |
   | T3 | `operationCompleteThrowsDoesNotSuppressSucceed` | `operationComplete` 
throws a sentinel `RuntimeException`. Verify `operationSucceed` was still 
invoked and `operationFail` was **not** invoked. |
   | T4 | `operationCompleteThrowsDoesNotTriggerFail` | On the failure path, 
`operationComplete` throws. Verify `operationFail` was still invoked exactly 
once (with the original invocation exception, not the callback exception). |
   | T5 | `operationSucceedThrowsDoesNotTriggerFail` | `operationSucceed` 
throws a sentinel exception. Verify `operationFail` is **not** invoked. (This 
test documents current behavior after the fix — callback exceptions propagate 
but do not cross-trigger other callbacks.) |
   | T6 | `realExceptionUnwrapPreserved` | Complete the future with a 
`CompletionException` wrapping a `RemotingTimeoutException`. Verify 
`operationFail` receives the unwrapped `RemotingTimeoutException` (preserves 
#9119/#9120 behavior via `ExceptionUtils.getRealException`). |
   | T7 | `syntheticResponseFutureOnFailure` | On failure, verify that 
`operationComplete` receives a `ResponseFuture` whose `getCause()` matches the 
original throwable. |
   
   ### 4.3 Test infrastructure
   
   - Subclass `NettyRemotingAbstract` with a minimal concrete stub (the class 
is abstract) that overrides `invokeImpl` to return a controllable 
`CompletableFuture<ResponseFuture>`.
   - Use Mockito `mock(InvokeCallback.class)` and `InOrder` for ordering 
verification.
   - No existing tests need modification — the current test suite does not 
assert callback ordering.
   
   ---
   
   ## 5. Backward Compatibility
   
   | Aspect | Assessment |
   |---|---|
   | **Public API** | No change. `invokeAsyncImpl` signature and 
`InvokeCallback` interface are unchanged. |
   | **Callback ordering** | This is a **bug fix** that brings behavior in line 
with the documented contract. Any code that accidentally depended on the 
reversed order (e.g., reading state set in `operationComplete` from within 
`operationSucceed`) was already incorrect. |
   | **Exception propagation** | Callback exceptions will no longer be silently 
routed to `operationFail`. Callers that relied on `operationFail` firing for 
*all* errors (including callback bugs) will see a behavioral change — but the 
previous behavior was itself a bug that masked real invocation outcomes. |
   | **#9119/#9120 raw-exception unwrap** | Preserved. 
`ExceptionUtils.getRealException` is still called before `operationFail`. |
   | **`ResponseFuture.executeInvokeCallback`** | Unchanged. It already had 
correct ordering and serves as the reference implementation. |
   | **Binary compatibility** | No change to method signatures, class 
hierarchy, or public fields. |
   
   **Verdict: No breaking changes.** The fix corrects behavior to match the 
documented and expected contract.
   
   ---
   
   ## 6. Risk Assessment
   
   | Risk | Likelihood | Impact | Mitigation |
   |---|---|---|---|
   | **Callback exception propagation** — After the fix, if `operationSucceed` 
throws, the exception propagates out of `whenComplete` rather than being caught 
by a downstream `exceptionally`. | Medium | Low–Medium | This matches 
`ResponseFuture.executeInvokeCallback` behavior. Callers that wrap 
`invokeAsyncImpl` should already handle exceptions from their own callbacks. If 
needed, a follow-up change can add per-callback try/catch with logging. |
   | **Subtle behavioral change for existing callers** — Code that worked "by 
accident" with the reversed order might break. | Low | Low | The reversed order 
was a contract violation. Any code depending on it was fragile. The test suite 
should catch regressions. |
   | **Incomplete `ResponseFuture` on failure path** — The synthetic 
`ResponseFuture` built on failure has `null` for `invokeCallback` and 
`rpcHook`. | None (no change) | None | This is identical to the current 
behavior — the synthetic future is only passed to `operationComplete`, which 
uses it for context (channel, request, cause), not for re-invoking callbacks. |
   | **Thread-safety** — `whenComplete` runs on the completing thread (same as 
before). | Low | None | No new concurrency concerns. The existing code already 
ran all three callbacks from the same thread context. |
   | **Missed callers of `invokeAsyncImpl`** | Low | Low | Grep confirms 
`invokeAsyncImpl` is called from `invokeAsync` in the same class and from 
`NettyRemotingClient`/`NettyRemotingServer`. All go through the same code path 
— the fix applies uniformly. |
   
   ### Overall risk: **Low**
   
   The change is localized to a single 17-line method, has a clear reference 
implementation (`ResponseFuture.executeInvokeCallback`), and is fully testable 
with deterministic unit tests.
   
   </details>
   
   **Next Steps:**
   - Reply `/approve` to proceed with PR generation
   - Reply `/revise <feedback>` to request changes
   - Reply `/reject` to close this proposal
   
   *This proposal expires in 72 hours.*
   
   ---
   *Automated by RockteMQ-AI*
   


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

Reply via email to