Skip to content

Commit 77fc6d5

Browse files
committed
fix: harden native addon lifecycle, shutdown semantics, and error propagation
- Eliminate Producer double-free by switching from AutoDeleteSendCallback to SendCallback - Replace ineffective set_shutdown_on_destroy with explicit shutdown in PushConsumer - Add shutdown_worker_active_ reentrancy guard to PushConsumer shutdown - Propagate consumer native errors to JS instead of silent RECONSUME_LATER nacks - Harden TSFN reference counting and lifecycle management - Fix race conditions between send/shutdown/finalize paths
1 parent c5be07d commit 77fc6d5

49 files changed

Lines changed: 5661 additions & 1438 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/build.yml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,35 @@ on:
1616
default: false
1717

1818
jobs:
19+
validate:
20+
runs-on: ubuntu-latest
21+
steps:
22+
- name: Checkout
23+
uses: actions/checkout@v4
24+
25+
- name: Setup Node.js
26+
uses: actions/setup-node@v4
27+
with:
28+
node-version: 20
29+
cache: 'npm'
30+
31+
- name: Install dependencies
32+
run: npm ci --ignore-scripts
33+
34+
- name: Typecheck
35+
run: npm run typecheck
36+
37+
- name: Lint
38+
run: npm run lint -- --no-fix
39+
40+
- name: Build test binary
41+
run: npm run build:test
42+
43+
- name: Test
44+
run: npx vitest run --reporter=verbose
45+
1946
linux-gnu-x64:
47+
needs: validate
2048
runs-on: ubuntu-24.04
2149
container:
2250
image: alibaba-cloud-linux-3-registry.cn-hangzhou.cr.aliyuncs.com/alinux3/alinux3:latest
@@ -107,6 +135,7 @@ jobs:
107135
path: Release/linux-x86_64-gnu-rocketmq.node
108136

109137
linux-gnu-arm64:
138+
needs: validate
110139
runs-on: ubuntu-24.04-arm
111140
container:
112141
image: alibaba-cloud-linux-3-registry.cn-hangzhou.cr.aliyuncs.com/alinux3/alinux3:latest
@@ -197,6 +226,7 @@ jobs:
197226
path: Release/linux-aarch64-gnu-rocketmq.node
198227

199228
linux-musl-x64:
229+
needs: validate
200230
runs-on: ubuntu-24.04
201231
container:
202232
image: node:20-alpine
@@ -282,6 +312,7 @@ jobs:
282312
path: Release/linux-x86_64-musl-rocketmq.node
283313

284314
linux-musl-arm64:
315+
needs: validate
285316
runs-on: ubuntu-24.04-arm
286317
env:
287318
ZIG_TARGET: aarch64-linux-musl
@@ -370,6 +401,7 @@ jobs:
370401
path: Release/linux-aarch64-musl-rocketmq.node
371402

372403
macos-universal:
404+
needs: validate
373405
runs-on: macos-latest
374406
steps:
375407
- name: Checkout

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,3 +220,5 @@ coverage.*.html
220220
dist/
221221
*.tsbuildinfo
222222
*.test.js.map
223+
test/*.js
224+
coverage_output.txt

AGENTS.md

Lines changed: 70 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,84 +1,92 @@
1-
# AI 编码助手项目指令
1+
# CLAUDE.md
22

3-
> **语言要求**: 始终使用中文回复
3+
## Quick Start
44

5-
---
6-
7-
## 🔴 强制规则 (MUST)
8-
9-
以下规则必须严格遵守,违反任何一条都是不可接受的:
5+
```bash
6+
npm run build:test # 构建测试版本 (含 C++ stub,无需真实 RocketMQ)
7+
npm run vitest # 跑测试
8+
npm run validate # typecheck + lint + test 全量验证
9+
```
1010

11+
## Architecture
1112

12-
### 代码质量检查
13-
编码任务完成前,**必须按顺序执行以下检查**
14-
1. TypeScript 检查必须通过
15-
2. Lint 检查必须通过
16-
3. 单元测试必须全部通过
17-
4. Code Review 必须完成
13+
Native addon (C++) ↔ TypeScript wrapper ↔ User code
1814

19-
### 单元测试要求
20-
- **服务端代码必须编写单测**
21-
- **单测覆盖率必须达到 100%**
22-
- **禁止 skip 任何测试用例**
23-
- **必须使用终端工具实际运行单测** - 不能假装运行
15+
```
16+
lib/ # C++ N-API 实现 (node-addon-api)
17+
├── rocketmq.cpp # addon 入口,注册 Producer/PushConsumer
18+
├── producer.{h,cpp} # Producer: start/shutdown/send (AsyncWorker + TSFN)
19+
├── push_consumer.{h,cpp} # PushConsumer: start/shutdown/subscribe/listener
20+
├── consumer_ack.{h,cpp} # ACK 回调对象
21+
├── common_utils.{h,cpp} # 共享工具 (参数校验、日志配置)
22+
└── addon_data.h # per-addon instance data
23+
24+
src/ # TypeScript 封装
25+
├── binding.ts # native binary 加载 + 接口类型声明
26+
├── constants.ts # Status enum, LogLevel enum
27+
├── producer.ts # RocketMQProducer class (Promise 队列串行化)
28+
├── consumer.ts # RocketMQPushConsumer class (EventEmitter)
29+
└── index.ts # public API re-export
30+
31+
test/ # Vitest + C++ stub
32+
├── mocks/rocketmq/ # C++ stub 实现 (替代真实 SDK)
33+
├── helpers/ # 测试辅助函数
34+
└── *.test.ts # 各模块测试
35+
36+
deps/rocketmq/ # 真实 RocketMQ C++ SDK (仅生产构建)
37+
```
2438

25-
---
39+
## Build Commands
2640

27-
## 📋 命令参考
41+
```bash
42+
npm run build # 完整构建 (native + TS)
43+
npm run build:native # 仅 native (真实 SDK)
44+
npm run build:test # native stub + TS (测试用,快)
45+
npm run build:ts # 仅 TypeScript → dist/
46+
```
2847

29-
### TypeScript 检查
48+
## Test Commands
3049

31-
## TIPS
32-
- ts check
33-
```shell
34-
npm run build:ts
50+
```bash
51+
npm run test # build:test + vitest
52+
npm run vitest # 仅跑测试 (需先 build:test)
53+
npm run test:coverage # 带覆盖率 (阈值: lines/functions 80%, branches 70%)
3554
```
3655

37-
### Lint 检查
38-
```shell
39-
# TypeScript/JavaScript 文件
40-
npm run lint
56+
## Verify (CI 等价)
4157

58+
```bash
59+
npm run validate # typecheck && lint && test
4260
```
4361

44-
### 单元测试
45-
```shell
46-
## 可以有选择执行,对 lib 下的 C++ 文件进行编译
47-
./build.sh local
48-
## 然后执行
49-
npm run test <文件路径>
50-
```
62+
## Key Conventions
5163

64+
### C++ 层
5265

53-
## ⚡ 执行流程
66+
- **Lifecycle 是一次性的**:Producer/Consumer start 失败或 shutdown 后不可重新 start
67+
- **状态机**`LifecycleState` enum + `std::atomic` + `TryTransitionState()` CAS
68+
- **异步模式**
69+
- start/shutdown → `Napi::AsyncWorker` (在 libuv 线程池执行)
70+
- send callback → `napi_threadsafe_function` (SDK 线程回调 → JS 主线程)
71+
- **参数校验**:入口处用 `utils::ValidateCallback` / `ValidateStringArguments`
72+
- **命名空间**:所有代码在 `__node_rocketmq__`
73+
- **条件编译**`ROCKETMQ_USE_STUB` 启用 stub;`ROCKETMQ_COVERAGE` 启用覆盖率
5474

55-
完成编码任务的标准流程:
75+
### TypeScript 层
5676

57-
```
58-
1. 理解需求
59-
60-
2. 编写/修改代码
61-
62-
3. 运行 TypeScript 检查
63-
64-
4. 运行 Lint 检查
65-
66-
5. 编写单测 (如需要)
67-
68-
6. 运行单测并等待完成
69-
70-
7. Code Review
71-
72-
8. 如有问题,返回步骤 2 修复
73-
74-
9. 完成
75-
```
77+
- **Promise/Callback 双模式**:所有异步方法同时支持两种调用风格
78+
- **串行化**:start/shutdown 通过 `operationQueue` Promise 链保证串行
79+
- **状态检查**:send 前检查 `this.status === Status.STARTED`
80+
- **优雅关闭**:shutdown 时 drain pending sends,reject 所有未完成的 send
7681

77-
---
82+
### 测试
7883

79-
## ⏳ 单测执行注意事项
84+
- 测试使用 C++ stub(`test/mocks/rocketmq/`),不依赖真实 broker
85+
- `pool: 'forks'` + `fileParallelism: false` 确保 native addon 隔离
86+
- `--expose-gc` 用于 GC 相关测试
8087

81-
- 单测启动和执行需要较长时间,**耐心等待**
82-
- 如支持后台进程读取,可使用 Background process 异步等待
83-
- **绝对禁止使用 sleep 命令等待**
88+
## Breaking Changes (v1.1.2 → v2.0.0)
8489

90+
- Producer/Consumer 变为一次性对象:start 失败或 shutdown 后不可重新 start
91+
- shutdown() 失败后状态重置为 STOPPED(不再保留 STOPPING)
92+
- 真实 SDK 不支持 start 失败后重试,JS 封装已对齐此行为

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
AGENTS.md

CMakeLists.txt

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ cmake_minimum_required(VERSION 3.26)
33
project(rocketmq)
44

55
add_definitions(-DNAPI_VERSION=9)
6+
add_definitions(-DNAPI_CPP_EXCEPTIONS)
67
add_definitions(-DNODE_RUNTIME=node)
78
add_definitions(-DBUILDING_NODE_EXTENSION)
89

@@ -18,6 +19,9 @@ execute_process(
1819
OUTPUT_VARIABLE CMAKE_JS_INC
1920
)
2021

22+
string(REPLACE "\n" "" CMAKE_JS_INC "${CMAKE_JS_INC}")
23+
string(REPLACE "\r" "" CMAKE_JS_INC "${CMAKE_JS_INC}")
24+
2125
message(STATUS "CMake.js configurations: INC=${CMAKE_JS_INC}")
2226

2327
include_directories(${CMAKE_JS_INC})
@@ -27,13 +31,47 @@ if(ROCKETMQ_USE_STUB)
2731
set(ROCKETMQ_STUB_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/test/mocks/rocketmq")
2832
file(GLOB ROCKETMQ_STUB_SOURCES "${ROCKETMQ_STUB_ROOT}/src/*.cpp")
2933
add_library(rocketmq_stub STATIC ${ROCKETMQ_STUB_SOURCES})
30-
set_target_properties(rocketmq_stub PROPERTIES POSITION_INDEPENDENT_CODE ON)
34+
set_target_properties(rocketmq_stub PROPERTIES
35+
POSITION_INDEPENDENT_CODE ON
36+
ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/build")
3137
target_include_directories(rocketmq_stub PUBLIC "${ROCKETMQ_STUB_ROOT}/include")
3238
include_directories("${ROCKETMQ_STUB_ROOT}/include")
3339
set(ROCKETMQ_LIB rocketmq_stub)
3440
else()
35-
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/deps/rocketmq/include)
36-
set(ROCKETMQ_LIB ${CMAKE_CURRENT_SOURCE_DIR}/deps/rocketmq/bin/librocketmq.a)
41+
set(ROCKETMQ_SDK_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/deps/rocketmq")
42+
43+
file(GLOB_RECURSE ROCKETMQ_SDK_SOURCES
44+
"${ROCKETMQ_SDK_ROOT}/src/*.cpp"
45+
"${ROCKETMQ_SDK_ROOT}/src/*.c")
46+
list(FILTER ROCKETMQ_SDK_SOURCES EXCLUDE REGEX "dllmain\\.cpp$")
47+
48+
file(GLOB ROCKETMQ_SIG_SOURCES "${ROCKETMQ_SDK_ROOT}/libs/signature/src/*.c")
49+
50+
add_library(rocketmq_sdk STATIC ${ROCKETMQ_SDK_SOURCES} ${ROCKETMQ_SIG_SOURCES})
51+
set_target_properties(rocketmq_sdk PROPERTIES
52+
POSITION_INDEPENDENT_CODE ON
53+
ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/build")
54+
55+
file(GLOB ROCKETMQ_SRC_CHILDREN "${ROCKETMQ_SDK_ROOT}/src/*")
56+
set(ROCKETMQ_SRC_INCLUDE_DIRS "${ROCKETMQ_SDK_ROOT}/src")
57+
foreach(child ${ROCKETMQ_SRC_CHILDREN})
58+
if(IS_DIRECTORY ${child})
59+
list(APPEND ROCKETMQ_SRC_INCLUDE_DIRS ${child})
60+
endif()
61+
endforeach()
62+
63+
target_include_directories(rocketmq_sdk PRIVATE
64+
"${ROCKETMQ_SDK_ROOT}/include"
65+
${ROCKETMQ_SRC_INCLUDE_DIRS}
66+
"${ROCKETMQ_SDK_ROOT}/bin/include"
67+
"${ROCKETMQ_SDK_ROOT}/libs/signature/include")
68+
69+
include_directories("${ROCKETMQ_SDK_ROOT}/include")
70+
set(ROCKETMQ_LIB rocketmq_sdk
71+
"${ROCKETMQ_SDK_ROOT}/bin/lib/libevent.a"
72+
"${ROCKETMQ_SDK_ROOT}/bin/lib/libevent_pthreads.a"
73+
"${ROCKETMQ_SDK_ROOT}/bin/lib/libjsoncpp.a"
74+
"${ROCKETMQ_SDK_ROOT}/bin/lib/libz.a")
3775
endif()
3876

3977
file(GLOB SOURCE_FILES "lib/*.cpp")
@@ -50,7 +88,7 @@ target_link_libraries(${PROJECT_NAME} ${ROCKETMQ_LIB})
5088
# versions when using Makefile generators.
5189

5290
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
53-
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -std=c++11")
91+
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -std=c++17")
5492
if(ROCKETMQ_FORCE_STATIC_MUSL)
5593
set(MUSL_LIBC_PATH $ENV{MUSL_LIBC_PATH})
5694
if(NOT MUSL_LIBC_PATH)
@@ -65,7 +103,7 @@ if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
65103
set_target_properties(${PROJECT_NAME} PROPERTIES LINK_DEPENDS_NO_SHARED TRUE)
66104
endif()
67105
else()
68-
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -std=c++11 -stdlib=libc++")
106+
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -std=c++17 -stdlib=libc++")
69107
endif()
70108

71109
if(ROCKETMQ_ENABLE_COVERAGE)

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@ producer.shutdown((err) => {
107107
await producer.shutdown();
108108
```
109109

110+
> **Note:** Producer is a one-shot object. Once `shutdown()` is called, the instance cannot be restarted. Create a new `Producer` instance if you need to send messages again.
111+
110112
### PushConsumer
111113

112114
#### Constructor
@@ -193,6 +195,8 @@ consumer.shutdown((err) => {
193195
await consumer.shutdown();
194196
```
195197

198+
> **Note:** PushConsumer is a one-shot object. Once `shutdown()` is called, the instance cannot be restarted. Create a new `PushConsumer` instance if you need to consume messages again.
199+
196200
### Aliyun RocketMQ
197201

198202
For Aliyun RocketMQ, additional configuration is required:

0 commit comments

Comments
 (0)