[Bug] A compaction round interrupted in the scratch folder splices stale bytes into the live compaction log of the next round
维护者通常 1 天内回复
评估
调研方向
Start in store/src/main/java/org/apache/rocketmq/store/kv/CompactionLog.java at compaction(...), where the scratch log is built with COMPACTING_SUB_FOLDER, and at TopicPartitionLog.clean(MappedFileQueue), which only checks the in-memory file list. Reproduce with the existing CompactionLogTest harness by pre-creating a leftover file in the compacting folder and checking whether it survives the round. Done means the scratch folder is empty before the round writes to it, the same fix covers the replicating scratch folder in pullMessageFromMaster, and the regression test passes.
由索引模型根据 Issue 内容生成。
描述
Before Creating the Bug Report
- I found a bug, not just asking a question, which should be created in GitHub Discussions.
- I have searched the GitHub Issues and GitHub Discussions of this repository and believe that this is not a duplicate.
- I have confirmed that this bug belongs to the current repository, not other repositories of RocketMQ.
Runtime platform environment
OS: any (broker hosting a compaction topic; the compaction round runs on a schedule, default compactionScheduleInternal = 15 min)
RocketMQ version
branch: develop
Git commit id: 0dd48c39d
JDK Version
Compiler: Temurin 1.8.0_504 (target 1.8, same as CI)
Describe the Bug
Each compaction round writes its output into a scratch sub-folder and only moves it into the live compaction log when the round completes:
CompactionLog.compaction(...)creates the scratch log withcompacting = new TopicPartitionLog(this, COMPACTING_SUB_FOLDER)(store/src/main/java/org/apache/rocketmq/store/kv/CompactionLog.java:682); the constructor only wires the paths (:992-1006) — it neither loads nor cleans the folder;- the scratch folder is cleaned only on the success path:
replaceFiles(...)(:618) moves the files out andcompacting.clean(false, false)(:621) then verifies the folder is empty and throws otherwise (TopicPartitionLog.clean(MappedFileQueue),:1095-1104). That check walks the in-memory file list, which is empty for a freshly constructed scratch log, so files left on disk are invisible to it.
So a round that is aborted after it started writing the scratch folder leaves that folder dirty and nothing ever notices:
- the round can be aborted by a broker restart or
kill -9(the round is scheduled in the background;CompactionStore.shutdownwaits 1 s and then callsshutdownNow), or by any exception inside the round —compactAndReplacehas nofinallyanddoCompactionswallows the throwable.
The next round then corrupts the live compaction log, because the scratch file name is reused:
- the new scratch
MappedFileQueuestarts empty, so the first append creates/opens the same file name (…/compacting/00000000000000000000) —MappedFileQueue.tryCreateMappedFileconstructs aDefaultMappedFileover the existing path, which opens itrwand maps it without truncating and withwrotePosition = 0; - the new content overwrites the file from offset 0 and
putEndMessage(compacting.getLog())(:674-679) declares the file full; - whatever the aborted round had written beyond the new content is still there, and
replaceFiles/replaceCqFiles(:707,:755) move that file into the live compaction log / consume queue.
The result is a compacted view that contains records from two rounds mixed together (with offsets from the aborted round), i.e. duplicated or stale messages for the compaction topic, and a consume queue whose tail does not belong to the log content.
Steps to Reproduce
- Create a topic with the compaction attribute and let a compaction round run (or force one) until it has written into
<compactionLogPath>/<topic>/<queueId>/compacting/. - Abort the round before it finishes the replace step (restart the broker,
kill -9, or make it throw) — the scratch folder keeps its files. - Let the next round run: it reopens the same scratch file and appends the new compacted content on top of the old bytes, then moves it into the live log.
At the unit level the defect is proven with the existing CompactionLogTest harness (no RocksDB involved): pre-create …/<topic>/<queueId>/compacting/00000000000000000000 as a leftover, run compaction(...) with putEndMessage stubbed, and observe that the leftover file still exists afterwards — verified today with assertFalse("a scratch file left by an interrupted round must not survive into the next round", leftover.exists()), which fails.
What Did You Expect to See?
A scratch folder that is guaranteed to be empty when a round starts — the same guarantee TopicPartitionLog.clean(...) is written to check after a round. With that, an aborted round can at worst lose the messages it was compacting (they are re-compacted by the next round) instead of mixing its output into the live log.
What Did You See Instead?
The leftover files of the aborted round are silently reused and their stale bytes end up inside the live compaction log / consume queue, so consumers of the compaction topic can see duplicated or stale records.
Additional Context
Minimal fix: empty the scratch sub-folder right after the scratch log is constructed, for both scratch folders that follow this pattern (COMPACTING_SUB_FOLDER in compaction(...), REPLICATING_SUB_FOLDER in pullMessageFromMaster(...), :202), by deleting the two scratch store paths (scratch.getLog().getStorePath() and scratch.getCQ().getMappedFileQueue().getStorePath()). Clean rounds are unaffected — the folders are empty then, and the existing post-round clean(...) check keeps working.
I will open a PR with that change plus the regression test above (red before: the leftover survives the round; green after: the scratch folder is emptied before the round writes into it).
- 主要语言
- Java
- 星标
- 22.7k
- 派生
- 12k
- 平均合并
- 4 天 20 小时
- 30 天内合并 PR
- 11
环境准备
- 没有 Dockerfile 或 Docker Compose 文件
- 有 Pull Request 模板
- 阅读贡献指南
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
apache/rocketmq 的其他 Issue
-
[Bug] RemotingHelper.connect swaps the socket send and receive buffer configuration可能已有人在做 @amalsaidov 今天认领。 未关闭
难度 2/5 1-3 小时 新手友好度 82/100
维护者通常 1 天内回复
-
CompressionType.of fails under the Turkish locale可能已有人在做 @sharanggupta 今天认领。 未关闭
难度 2/5 1-3 小时 新手友好度 83/100
维护者通常 1 天内回复
-
[rocketmq-client-python] TransactionMQProducer allocates an unused producer during initialization未关闭
难度 2/5 1-3 小时 新手友好度 82/100
维护者通常 1 天内回复
-
[Bug] notifyMasterOnline aborts the long-polling wake-up with a NullPointerException when an arrival drains a bucket first可能已有人在做 @iloveeyjafjalla 于 29 天前认领。 未关闭
难度 2/5 1-3 小时 新手友好度 82/100
apache/rocketmq#11441 · 1 条评论 ·
维护者通常 1 天内回复
-
[Enhancement] FilterFactory duplicate-registration and unregister semantics lack tests可能已有人在做 @chennaji9 于 34 天前认领。 未关闭
难度 2/5 1-3 小时 新手友好度 84/100
apache/rocketmq#11340 · 2 条评论 ·
维护者通常 1 天内回复
相似的 Issue
-
bug emitter:client:csharp
难度 2/5 1-3 小时 新手友好度 84/100
microsoft/typespec#12572 · 1 条评论 · 1 个 reaction ·
维护者通常 1 天内回复
-
GCC 15.2: -Wcast-qual warning - cast discards const qualifier from pointer target type (H5trace.c)未关闭
难度 2/5 1-3 小时 新手友好度 73/100
维护者通常 1 天内回复
-
needs-area-label
难度 2/5 1-3 小时 新手友好度 78/100
维护者通常 2 天内回复
-
area/ai area/backend component/zeebe kind/task
难度 2/5 1-3 小时 新手友好度 68/100
维护者通常 1 天内回复
-
难度 2/5 1-3 小时 新手友好度 72/100
spAnser/reacharound#38 ·