Skip to content

[Fix] Gate RocketMQ 5 POP broker ACK on distribution completion (#5295) - #5316

Closed
zhang-arvin wants to merge 1 commit into
apache:developfrom
zhang-arvin:fix/5295-pop-broker-ack-barrier
Closed

[Fix] Gate RocketMQ 5 POP broker ACK on distribution completion (#5295)#5316
zhang-arvin wants to merge 1 commit into
apache:developfrom
zhang-arvin:fix/5295-pop-broker-ack-barrier

Conversation

@zhang-arvin

Copy link
Copy Markdown
Contributor

What changes were proposed in this pull request

Fix #5295: Gate RocketMQ 5 POP broker ACK on distribution completion.

Problem

Previously, a single mqAck callback was shared across all deliveries of a frame. In BROADCAST/MULTICAST mode, the first client ACK would immediately ACK the broker, even if other required targets had not yet received or acknowledged the message.

Solution

Introduce a broker-ACK barrier using an AtomicInteger counter:

  • All deliveries of the same frame share a single counter initialized to targets.size()
  • Broker ACK fires only when all deliveries have ACKed (counter reaches 0)
  • LOAD_BALANCE (1 target): 1 ACK → broker ACK
  • BROADCAST (N targets): N ACKs → broker ACK
  • MULTICAST (matched targets): all matched ACKs → broker ACK

Changes

  • eventmesh-runtime/.../UniIngressService.java: Replace the shared mqAck callback with a barrier that counts down remaining ACKs before firing the broker ACK

Verification

  • In BROADCAST mode, the first of multiple client ACKs does not ACK the POP message at the broker
  • The final required ACK executes exactly one broker ACK
  • Duplicate and out-of-order ACKs do not execute multiple broker ACKs (guarded by ReliableDispatcher.ack's idempotency)
  • Runtime failure before completion causes broker redelivery after POP invisible time

…letion

Previously, a single mqAck callback was shared across all deliveries of a
frame. In BROADCAST/MULTICAST mode, the first client ACK would
immediately ACK the broker, even if other required targets had not
yet received or acknowledged the message.

Introduce a broker-ACK barrier using an AtomicInteger counter:
- All deliveries of the same frame share a single counter
- Broker ACK fires only when all deliveries have ACKed
- LOAD_BALANCE (1 target): 1 ACK -> broker ACK
- BROADCAST (N targets): N ACKs -> broker ACK
- MULTICAST (matched targets): all matched ACKs -> broker ACK

Fixes apache#5295

@github-actions github-actions Bot 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.

Welcome to the Apache EventMesh community!!
This is your first PR in our project. We're very excited to have you onboard contributing. Your contributions are greatly appreciated!

Please make sure that the changes are covered by tests.
We will be here shortly.
Let us know if you need any help!

Want to get closer to the community?

WeChat Assistant WeChat Public Account Slack
Join Slack Chat

Mailing Lists:

Name Description Subscribe Unsubscribe Archive
Users User support and questions mailing list Subscribe Unsubscribe Mail Archives
Development Development related discussions Subscribe Unsubscribe Mail Archives
Commits All commits to repositories Subscribe Unsubscribe Mail Archives
Issues Issues or PRs comments and reviews Subscribe Unsubscribe Mail Archives

@qqeasonchen qqeasonchen 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.

Review: changes requested before merge

Thanks for the fix — the barrier approach is correct and the code is clean. However, there are 3 blockers and 3 suggestions that need to be addressed.

What I did

  1. Fetched the PR head (1cdca51) into a local pr-5316 ref
  2. Pulled both blobs via git show and ran diff -u locally
  3. Key finding: the file changed from CRLF to LF, which inflates the diff to +920/-903; the actual logic change is only ~20 lines

🔴 Blocker 1: Missing tests

UniIngressService is on the hot path (every message goes through it), and this PR changes the condition that fires the broker ACK — a premature trigger loses messages, a delayed trigger leaks memory / causes duplicate consumption. But the PR has no test file changes.

Issue #5295's acceptance criteria explicitly requires:

  • Tests cover BROADCAST, LOAD_BALANCE, and MULTICAST completion rules.
  • A RocketMQ 5 broker E2E test verifies this behavior.

The 4 verification checkboxes in the PR description are all unchecked (PMC convention requires author self-verification first).

Minimum required (any of these will do):

  • Unit test (add UniIngressServiceTest or extend ReliableDispatcherTest): mock MeshStoragePlugin + mock ReliableDispatcher, verify:
    • LOAD_BALANCE (1 target): 1 client ACK → 1 ackPulledMessage trigger
    • BROADCAST (3 targets): all 3 client ACKs → 1 trigger; intermediate ACKs do not trigger
    • MULTICAST (2 matched): all 2 ACKs → 1 trigger
    • Duplicate ACK (same deliveryId): counter goes negative, broker ACK is not re-triggered
    • popCk == null: else branch passes null mqAck (equivalent to no-barrier behavior)
  • In-process E2E (in the style of ClusterDeliveryFaultTest): use the existing InMemoryMetaStore + real UniIngressService + mock storage, run at least the full BROADCAST barrier flow

For reference, see PR #5308 (the #5293 implementation) which added 5 ClusterDeliveryFaultTest scenarios in the same style.

🔴 Blocker 2: Barrier duplicate-ACK protection is incomplete

Runnable mqAck = () -> {
    if (pending.decrementAndGet() == 0) {  // ← issue here
        storage.ackPulledMessage(topic, popCk);
    }
};

Problem: decrementAndGet == 0 only fires on the first time it reaches zero. But there are 3 scenarios that cause the mqAck to be entered more than expected:

  1. Same clientId retries ACK (SDK-side retry / network resend): ReliableDispatcher.ack() should be idempotent, but even if it is, the barrier will continue to decrement
  2. ACK for a non-matching deliveryId (potentially introduced in the future): decrements a counter that shouldn't be included in the barrier
  3. Re-dispatch of the same frame (forward path / requeue)

Note: in repeat-ACK scenarios, decrementAndGet == 0 is only true the first time, and subsequent -1, -2... won't re-trigger — this is actually OK in isolation. But there is one real risk:

If ReliableDispatcher.ack is not strictly idempotent (per the issue #5295 description it dedupes, but if any race is missed), the storage.ackPulledMessage call outside the barrier could race. Recommend an explicit CAS guard:

AtomicInteger pending = new AtomicInteger(targets.size());
AtomicBoolean brokerAcked = new AtomicBoolean(false);
Runnable mqAck = () -> {
    if (pending.decrementAndGet() == 0 && brokerAcked.compareAndSet(false, true)) {
        storage.ackPulledMessage(topic, popCk);
    }
};

This way, even if decrementAndGet somehow reaches 0 multiple times (theoretically impossible but defensive), the broker ACK is only triggered once.

🔴 Blocker 3: multi-instance path not handled

if (cluster != null) {
    // Multi-instance: route via the cluster coordinator (local vs cross-instance forward).
    cluster.dispatch(topic, f);
} else {
    // barrier logic
}

The PR only fixes the else branch. But cluster.dispatch internally still calls storage.poll → its own deliver loop — the same bug will reproduce in the multi-instance path.

Please confirm whether cluster.dispatch internally also goes through storage.poll + the same target-allocation logic; if so, the barrier must be added there as well (or refactored into a shared helper).


🟡 Suggestion 1: Split the file-format normalization into a separate PR

99% of the +920/-903 diff is CRLF → LF noise (each line loses 1 byte → line count grows; plus the actual +17 line logic change). Either normalize the file standalone first (LF across all .java, or keep CRLF if the repo default is CRLF), or use .gitattributes to keep the line-ending story consistent.

🟡 Suggestion 2: The null mqAck else branch can be cleaner

The current else branch explicitly loops with null mqAck. Consider:

if (popCk != null && !targets.isEmpty()) {
    // ... barrier setup
} else {
    for (Subscription target : targets) {
        dispatcher.deliver(..., null);  // can be simplified if dispatcher tolerates null
    }
}

But this requires confirming ReliableDispatcher.deliver accepts a null mqAck — the existing code already passes null, so the current state is acceptable.

🟡 Suggestion 3: Annotate the issue reference and scope

The code added // Issue #5295: comment, which is good. Recommend also adding:

// Issue #5295: the barrier is per-frame. The multi-instance cluster.dispatch path
// is out of scope for this PR (see PR review comment #3) and will be tracked
// separately.

This makes the multi-instance path's handling state explicit so it isn't mistaken for fixed in the future.


Summary

Category Item Status
Blocker 1. Missing tests Must add
Blocker 2. Barrier duplicate-ACK protection incomplete Add compareAndSet guard
Blocker 3. multi-instance path not covered Must clarify or fix
Suggestion 1. Split file-format normalization PR Optional
Suggestion 2. null mqAck else branch Optional
Suggestion 3. Annotate issue reference Recommended

Ping me for re-review after the blockers are addressed.

— qqeasonchen (apache/eventmesh PMC)

@qqeasonchen

Copy link
Copy Markdown
Contributor

@zhang-arvin please fix the conflicts and review blockers,thx

@qqeasonchen

Copy link
Copy Markdown
Contributor

@zhang-arvin 谢谢原 PR!本意保留你的实现,但 #5316 基于 pre-#5301 的 903 行 UniIngressService.java 整体重写,跟 develop HEAD(988 行,已被 #5311 / #5312 重构过)有结构性冲突——3-way merge 会产生几十处冲突标记而不是几处。

所以我重做了 rebase:保留你设计的 barrier 逻辑AtomicInteger pending = new AtomicInteger(targets.size()) + decrementAndGet() == 0 触发 broker ACK),把它移植到 develop HEAD 当前的代码结构上(保留 TTL 检查、Otel span、nextOffsetUniTrace.endsubscriptionManager.targetsFor 复用)。./gradlew :eventmesh-runtime:compileJava 通过。

新 PR: #5330merge-5316-pop-broker-ack 分支)。

这个 #5316 因为 base 已经完全过时,建议直接关闭。如果你想在自己的 PR 上发,请以 #5330 为底 cherry-pick —— 我会重命名 #5330 branch 为 fix/5295-pop-broker-ack-barrier、把 author 改成你(这样 git history 显示是你提的),然后你 reopen 即可。

Fixes #5295 的功劳归你。

qqeasonchen added a commit that referenced this pull request Sep 7, 2026
… (merged from PR #5316) (#5330)

Cherry-pick of zhang-arvin's barrier change, rebased on develop HEAD after the
#5301 Sub-PR B (#5311) and Sub-PR C (#5312) refactors that rewrote the
UniIngressService deliver path. The original PR was based on the pre-#5301 file
(903 lines) and conflicted wholesale with the post-#5301 file (988 lines); this
commit applies the same barrier fix (AtomicInteger + per-delivery decrement + late
broker ack) to the current develop file structure.

- Shared mqAck (first ACK = broker ACK, premature for BROADCAST/MULTICAST)
  -> per-frame AtomicInteger initialized to targets.size(); broker ack only
  fires when the last required delivery ACKs (counter reaches 0).
- LOAD_BALANCE (1 target): 1 ACK -> broker ACK (semantic unchanged).
- BROADCAST (N targets): N ACKs -> broker ACK.
- MULTICAST (matched targets): all matched ACKs -> broker ACK.
- No popCk path: unchanged (null callback, no broker ack).
- Reuses SubscriptionManager.targetsFor(); preserves existing TTL check,
  Otel span, metrics, and Frame-architecture context from develop HEAD.

Original PR: #5316 (zhang-arvin)
Fixes #5295
@qqeasonchen

Copy link
Copy Markdown
Contributor

Closing as superseded. The barrier fix from this PR was conflict-resolved and landed in #5330 (commit c7de75b); the contributor attribution for zhang-arvin is being recorded via the companion PR #5333, which adds an attribution Javadoc and carries a Co-authored-by: zhang-arvin <arvin.zhang@htx-inc.com> trailer in the commit message (same pattern that #5331 used for #5325 / wangyusheng1985).

Thanks for the contribution @zhang-arvin — your original commit 1cdca51f is the canonical reference for the broker-ACK barrier logic.

@qqeasonchen qqeasonchen closed this Sep 7, 2026
qqeasonchen added a commit that referenced this pull request Sep 7, 2026
…(zhang-arvin) (#5333)

Mirror of #5331's approach for #5316: the original PR's barrier fix landed in #5330 but lost zhang-arvin's author/co-author attribution in the squash. This commit records that attribution via the commit metadata (author=zhang-arvin) and a  trailer in the message, plus a 3-line attribution Javadoc in the source so the next reader can trace the broker-ACK semantics back to PR #5316.

Author: zhang-arvin <arvin.zhang@htx-inc.com>
Closes #5316
Fixes #5295
qqeasonchen added a commit that referenced this pull request Sep 7, 2026
…ier (PR #5316) (#5334)

Companion to #5333: while #5333 expanded the inline `// P2 fix:` comment in `UniIngressService.deliver` to attribute the AtomicInteger broker-ACK barrier to PR #5316 (zhang-arvin, fixes #5295), this PR adds the same attribution at the **package** level by extending `package-info.java` with a paragraph that:

- names the barrier's contract (AtomicInteger initialized to target count; broker ACK only on the last required delivery ACK)
- lists the three distribution modes the barrier applies to (LOAD_BALANCE, BROADCAST, MULTICAST)
- documents the no-popCk bypass (frames without `empopck` skip the barrier)
- references PR #5316 and #5295

This pairs the source-level attribution (#5333) with package-level attribution, so a reader tracking RocketMQ 5.x POP semantics finds the barrier's contract right next to the `@Internal` marker the package already carries.

**No behavior change** — documentation only. The barrier logic is byte-identical to the post-#5330 merge.

**Files changed**: 1 file, +11 / -1 (Javadoc paragraph in package-info.java)

**Author attribution** (commit metadata):
- author: `zhang-arvin <arvin.zhang@htx-inc.com>` (date 2026-08-30, matching zhang-arvin's original #5316 commit)
- committer: `qqeasonchen <qqeasonchen@gmail.com>`

**Why a separate PR for a doc-only change?** The squash-merged #5330 (`c7de75b`) did not carry zhang-arvin in the co-author trailer, so the GitHub contributor graph did not see the contribution. The companion fix for #5325 (PR #5331) used the same pattern (a 0-diff PR with a `Co-authored-by:` trailer in the merge commit body) to record wangyusheng1985's credit; this PR and #5333 mirror that approach for #5316. Both attribution PRs (`#5333` and this one) carry the trailer in the PR body so GitHub preserves it in the squash-merge commit message.

cc @zhang-arvin — please let me know if you'd prefer a different attribution form (e.g. moving the paragraph to a separate `BROKER_ACK_BARRIER.md` under `docs/`) and I'll adjust.

Closes #5316
Fixes #5295

Co-authored-by: zhang-arvin <arvin.zhang@htx-inc.com>
qqeasonchen added a commit that referenced this pull request Sep 7, 2026
…both contributor trailers) (#5335)

Reverts the four commits landed earlier to attribute / merge #5316 (zhang-arvin) and #5325 (wangyusheng1985) onto develop:

- #5330 (c7de75b) — barrier fix for the RocketMQ 5 POP broker ACK (`fix(#5295): gate RocketMQ 5 POP broker ACK on distribution completion`)
- #5331 (4c3e85b) — README Quick start anchors 0-diff
- #5333 (2d2f98f) — source-level attribution Javadoc in `UniIngressService.java`
- #5334 (e6e1247) — package-level doc in `ingress/package-info.java`

After this revert lands, the develop tree returns to `2b0d7abb` (the pre-#5330 state), with the four changes unsuperseded.

A follow-up PR will re-land the work as a single commit carrying BOTH contributor trailers (`Co-authored-by: zhang-arvin` for #5316 and `Co-authored-by: wangyusheng1985` for #5325) so both new contributors are credited in the GitHub contributor graph in one squash-merge.

Why revert first? The current four commits split the attribution across multiple squash merges with inconsistent trailer handling. Consolidating into a single PR with both trailers in the body (which GitHub preserves in the squash-merge commit message) is the cleanest path to contributor graph credit for both zhang-arvin and wangyusheng1985.
qqeasonchen added a commit that referenced this pull request Sep 7, 2026
… (PR #5316) (#5336)

Combined re-land of the four earlier #5330 / #5331 / #5333 / #5334 commits (all reverted by #5335) as a single commit on develop, with both contributor trailers preserved.

**What this PR does**

- Reapplies the RocketMQ 5 POP broker-ACK barrier from PR #5316 (zhang-arvin, fixes #5295) onto the post-#5301-Sub-PR-B/C `UniIngressService.deliver` structure. The barrier is the AtomicInteger-based pattern: per-frame counter initialized to the target count; the broker is ACKed only when the last required delivery ACKs.
- Adds a 6-line source-level Javadoc in `UniIngressService.java` attributing the barrier block to PR #5316 and zhang-arvin.
- Adds a package-level paragraph in `ingress/package-info.java` describing the barrier's contract (AtomicInteger counter, broker ACK on last delivery, LOAD_BALANCE / BROADCAST / MULTICAST semantics, no-popCk bypass).
- Leaves README content unchanged: the 3 broken anchors originally targeted by #5325 (wangyusheng1985) were already removed by #5326 (docs: sync documentation with implementation status), so the README is already in its post-#5326 state.

**Why a single commit instead of four**

GitHub's contributor graph counts both `commit author` and `Co-authored-by:` trailers in the commit message. The earlier four-commit approach split the attribution across squash merges with inconsistent trailer handling. Consolidating into one PR with two trailers (zhang-arvin for #5316, wangyusheng1985 for #5325) preserves both contributors' graph credit in a single squash-merge commit.

**Files changed**: 2 files
- `eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/ingress/UniIngressService.java` (barrier block + attribution Javadoc)
- `eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/ingress/package-info.java` (package-level barrier doc)

**Closes #5316, Closes #5325, Fixes #5295**

cc @zhang-arvin @wangyusheng1985

Co-authored-by: zhang-arvin <arvin.zhang@htx-inc.com>
Co-authored-by: wangyusheng1985 <wangyusheng1985@users.noreply.github.com>
qqeasonchen added a commit that referenced this pull request Sep 7, 2026
…ier (PR #5316) (#5334)

Companion to #5333: while #5333 expanded the inline `// P2 fix:` comment in `UniIngressService.deliver` to attribute the AtomicInteger broker-ACK barrier to PR #5316 (zhang-arvin, fixes #5295), this PR adds the same attribution at the **package** level by extending `package-info.java` with a paragraph that:

- names the barrier's contract (AtomicInteger initialized to target count; broker ACK only on the last required delivery ACK)
- lists the three distribution modes the barrier applies to (LOAD_BALANCE, BROADCAST, MULTICAST)
- documents the no-popCk bypass (frames without `empopck` skip the barrier)
- references PR #5316 and #5295

This pairs the source-level attribution (#5333) with package-level attribution, so a reader tracking RocketMQ 5.x POP semantics finds the barrier's contract right next to the `@Internal` marker the package already carries.

**No behavior change** — documentation only. The barrier logic is byte-identical to the post-#5330 merge.

**Files changed**: 1 file, +11 / -1 (Javadoc paragraph in package-info.java)

**Author attribution** (commit metadata):
- author: `zhang-arvin <arvin.zhang@htx-inc.com>` (date 2026-08-30, matching zhang-arvin's original #5316 commit)
- committer: `qqeasonchen <qqeasonchen@gmail.com>`

**Why a separate PR for a doc-only change?** The squash-merged #5330 (`c7de75b`) did not carry zhang-arvin in the co-author trailer, so the GitHub contributor graph did not see the contribution. The companion fix for #5325 (PR #5331) used the same pattern (a 0-diff PR with a `Co-authored-by:` trailer in the merge commit body) to record wangyusheng1985's credit; this PR and #5333 mirror that approach for #5316. Both attribution PRs (`#5333` and this one) carry the trailer in the PR body so GitHub preserves it in the squash-merge commit message.

cc @zhang-arvin — please let me know if you'd prefer a different attribution form (e.g. moving the paragraph to a separate `BROKER_ACK_BARRIER.md` under `docs/`) and I'll adjust.

Closes #5316
Fixes #5295

Co-authored-by: zhang-arvin <arvin.zhang@htx-inc.com>
@qqeasonchen

Copy link
Copy Markdown
Contributor

Attribution update (post history-cleanup): the close comment above references c7de75b (#5330) and #5333/#5334, which were later consolidated during a develop history cleanup. The final landing for this PR's contribution is now:

  • Commit 5fb1f6b18eaf983940b9313061abce3463d5f3b5 on develop — carries Co-authored-by: zhang-arvin <arvin.zhang@htx-inc.com> in the commit message, and contains the broker-ACK barrier code in UniIngressService.java (per-frame AtomicInteger + per-delivery decrement + late broker ack) plus the package-level barrier doc in ingress/package-info.java.

zhang-arvin is credited as co-author on that commit, so this PR's contribution is reflected in the GitHub contributor graph via the trailer. Thanks again @zhang-arvin!

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.

[Bug] Gate RocketMQ 5 POP broker ACK on distribution completion

2 participants