[Bug] notifyMasterOnline aborts the long-polling wake-up with a NullPointerException when an arrival drains a bucket first
メンテナーはふだん 1 日以内に返信
評価
調査の方向性
PullRequestHoldService.notifyMasterOnline の約191行目から始めてください。ここでは pullRequestTable に対するループが mpr.cloneListAndClear() を呼び出し、その結果を直接反復しています。空のバケットに対して null を返す ManyPullRequest.cloneListAndClear() を読み、notifyMessageArriving の null チェックと比較してください。並行して空にされたバケットがスキップされ、かつ他のキューのリクエストが引き続き起こされたら完了です。
索引モデルが issue の本文から書いたものです。
説明
Describe the Bug
When the smallest broker of a group changes and this broker becomes the master, BrokerController#onMinBrokerChange ends with pullRequestHoldService.notifyMasterOnline(), whose job is to immediately re-execute every pull request that is currently suspended, so that clients do not have to wait for their own suspend timeout after a master comes online.
That sweep assumes a bucket it has just seen as non-empty still holds requests when it drains it:
for (ManyPullRequest mpr : this.pullRequestTable.values()) {
if (mpr == null || mpr.isEmpty()) {
continue;
}
for (PullRequest request : mpr.cloneListAndClear()) {
ManyPullRequest#cloneListAndClear() returns null (not an empty list) whenever the bucket is empty at drain time:
public synchronized List<PullRequest> cloneListAndClear() {
if (!this.pullRequestList.isEmpty()) {
...
return result;
}
return null;
}
The buckets are drained by the message-arrival path on the store's dispatch thread (NotifyMessageArrivingListener -> PullRequestHoldService#notifyMessageArriving, which drains the same bucket and replays the requests that did not match). An arrival that wins the race between the isEmpty() check above and the drain makes the loop iterable null, so the enhanced for throws a NullPointerException.
Impact
- The
NullPointerExceptionescapesnotifyMasterOnline()intoonMinBrokerChange. The sweep is over, so every bucket that had not yet been visited keeps its requests suspended: those clients sit out their full suspend timeout instead of being woken immediately - exactly the stall this method exists to prevent, and in exactly the failover window it is meant for. - The exception is thrown on the thread that updates the min-broker state (including the
UPDATE_MIN_BROKERadmin path), so the caller sees a failure for an operation that has otherwise already been applied.
The same class shows what the intended contract is: notifyMessageArriving drains a bucket the same way and guards the result with if (requestList != null) before iterating, so the missing guard in notifyMasterOnline is an oversight rather than a documented behaviour.
How to Reproduce
- Suspend a pull request on some
topic@queueId(long polling enabled, i.e. the client sent the suspend flag and the pull returnedPULL_NOT_FOUNDlonger than the max. offset). - Produce a message for that queue from another thread, so the dispatch thread drains the same bucket through
notifyMessageArrivingin the same instant asnotifyMasterOnline(). - The stricter, deterministic form of the race: the bucket reports itself as non-empty and is drained before
notifyMasterOnline()drains it, which is what makes the sweep iterablenull. - Result with the current code:
java.lang.NullPointerException at org.apache.rocketmq.broker.longpolling.PullRequestHoldService.notifyMasterOnline(PullRequestHoldService.java:191), and the remaining buckets are not woken.
Expected Behavior
A bucket that lost that race is skipped; the sweep continues and still wakes the requests held for the other queues.
Proposed Fix
Read the drained list into a local variable and skip the bucket when it answers null, instead of iterating the call result directly:
List<PullRequest> requestList = mpr.cloneListAndClear();
if (requestList == null) {
continue;
}
for (PullRequest request : requestList) {
Environment
- 5.5.1-SNAPSHOT (
develop, 0dd48c39db), all platforms
- 主要言語
- Java
- スター
- 22.6k
- フォーク
- 12k
- 平均マージ
- 4日 13時間
- マージ済み PR(30日)
- 20
環境構築
- Dockerfile・Docker Compose ファイルなし
- プルリクエストのテンプレートあり
- コントリビューションガイドを読む
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
apache/rocketmq のほかの issue
-
[rocketmq-client-python] TransactionMQProducer allocates an unused producer during initializationオープン
難易度 2/5 1〜3時間 初心者へのやさしさ 82/100
メンテナーはふだん 1 日以内に返信
-
[Enhancement] FilterFactory duplicate-registration and unregister semantics lack tests対応中かも @chennaji9 が 19 日前に担当しました。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 84/100
apache/rocketmq#11340 · コメント 2 件 ·
メンテナーはふだん 1 日以内に返信
-
[Enhancement] LogicExpression three-valued truth table and short-circuit behavior lack direct tests対応中かも @chennaji9 が 19 日前に担当しました。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
apache/rocketmq#11339 · コメント 2 件 ·
メンテナーはふだん 1 日以内に返信
-
[Enhancement] SyncStateInfo defensive-copy and epoch semantics lack direct tests対応中かも @chennaji9 が 19 日前に担当しました。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
apache/rocketmq#11338 · コメント 2 件 ·
メンテナーはふだん 1 日以内に返信
-
[Enhancement] ControllerClosure request-completion semantics lack direct tests対応中かも @chennaji9 が 19 日前に担当しました。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
apache/rocketmq#11337 · コメント 2 件 ·
メンテナーはふだん 1 日以内に返信
apache/rocketmq の issue をすべて見る
似ている issue
-
bug web-server
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
jooby-project/jooby#4035 ·
メンテナーはふだん 1 日以内に返信
-
L: javascript
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
dependabot/dependabot-core#16744 ·
メンテナーはふだん 1 日以内に返信
-
intake mcp-intake needs-human-review priority:high type:bug
難易度 2/5 1〜3時間 初心者へのやさしさ 62/100
Ikalus1988/MisakaNet#3987 · コメント 3 件 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 82/100
quarkiverse/quarkus-google-cloud-services#1187 ·
メンテナーはふだん 1 日以内に返信
-
難易度 1/5 1時間未満 初心者へのやさしさ 82/100
locationtech/jts#1250 ·