@raft-hlc-sync-protocol/raft-sync-lib 1.0.5 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -30
- package/README.zh-CN.md +19 -29
- package/index.js +15 -8
- package/package.json +1 -1
- package/sync-engine.js +79 -7
package/README.md
CHANGED
|
@@ -168,35 +168,22 @@ setInterval(() => engine.tickCleanup(), 3600_000); // Cleanup old logs every 1
|
|
|
168
168
|
|
|
169
169
|
### Step 5: Write data
|
|
170
170
|
|
|
171
|
-
|
|
171
|
+
> **Important:** All writes (INSERT/UPDATE/DELETE) to registered tables **must** go through `engine.write()` or be manually wrapped with `engine.beginManualTransaction()` / `commitManualTransaction()`. Do NOT write directly via `db.run(...)` outside of these wrappers — the engine needs to manage transactions and 2PC coordination to ensure data is correctly synced across nodes.
|
|
172
172
|
|
|
173
|
-
|
|
174
|
-
const ts = engine.hlc.tick();
|
|
175
|
-
db.run('INSERT INTO users (user_id, name, _hlc) VALUES (?, ?, ?)',
|
|
176
|
-
['u1', 'Alice', ts]);
|
|
177
|
-
engine.notifyLocalWrite(); // pushes to peers
|
|
178
|
-
```
|
|
173
|
+
**Recommended: `engine.write()` (high-level API)**
|
|
179
174
|
|
|
180
|
-
|
|
175
|
+
The engine handles the entire lifecycle automatically: BEGIN → execute → trigger logs → 2PC prepare/ack → COMMIT → broadcast commit → notify peers.
|
|
181
176
|
|
|
182
177
|
```js
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
JSON.stringify({ user_id: 'u1' }),
|
|
190
|
-
JSON.stringify({ user_id: 'u1', name: 'Alice', _hlc: ts }),
|
|
191
|
-
ts);
|
|
192
|
-
engine.notifyLocalWrite();
|
|
178
|
+
const result = await engine.write(async (db) => {
|
|
179
|
+
const ts = engine.hlc.tick();
|
|
180
|
+
db.run('INSERT INTO users (user_id, name, _hlc) VALUES (?, ?, ?)',
|
|
181
|
+
['u1', 'Alice', ts]);
|
|
182
|
+
return { userId: 'u1' }; // return value is passed through
|
|
183
|
+
}, userId); // shardKey for routing
|
|
193
184
|
```
|
|
194
185
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
## 2PC: Strong Consistency Writes
|
|
198
|
-
|
|
199
|
-
When you need atomic multi-node writes:
|
|
186
|
+
**Low-level API (for fine-grained control)**
|
|
200
187
|
|
|
201
188
|
```js
|
|
202
189
|
const writeId = crypto.randomUUID();
|
|
@@ -479,13 +466,14 @@ VALUES ('${tableName}', 'DELETE', JSON_OBJECT(${delKeyExpr}), NULL, OLD._hlc)`,
|
|
|
479
466
|
| `tickPull()` | Pull changes from all peers |
|
|
480
467
|
| `tickHeartbeat()` | Check timeouts, send pings |
|
|
481
468
|
| `tickCleanup()` | Clean old sync_log and tombstones |
|
|
482
|
-
| `
|
|
483
|
-
| `
|
|
484
|
-
| `
|
|
485
|
-
| `
|
|
486
|
-
| `
|
|
487
|
-
| `
|
|
488
|
-
| `
|
|
469
|
+
| `write(fn, shardKey?)` | **High-level 2PC API**: execute fn in a 2PC transaction, engine handles everything automatically |
|
|
470
|
+
| `beginManualTransaction(id)` | Low-level: start 2PC transaction |
|
|
471
|
+
| `getManualTransactionEntries(id)` | Low-level: read sync_log entries from current txn |
|
|
472
|
+
| `commitManualTransaction(id)` | Low-level: commit 2PC transaction |
|
|
473
|
+
| `rollbackManualTransaction(id)` | Low-level: rollback 2PC transaction |
|
|
474
|
+
| `waitForPrepareAck(writeId, entries, term, shardId)` | Low-level 2PC: broadcast prepare, wait for acks |
|
|
475
|
+
| `broadcastCommit(writeId)` | Low-level 2PC: broadcast commit |
|
|
476
|
+
| `broadcastAbort(writeId, reason)` | Low-level 2PC: broadcast abort |
|
|
489
477
|
| `proxyRequest(key, payload)` | Forward write request to leader |
|
|
490
478
|
| `getShardId(key)` | Get shard ID for a key |
|
|
491
479
|
| `isLeaderForShard(shardId)` | Check if local node is leader for shard |
|
package/README.zh-CN.md
CHANGED
|
@@ -168,35 +168,24 @@ setInterval(() => engine.tickCleanup(), 3600_000); // 每 1 小时清理旧日
|
|
|
168
168
|
|
|
169
169
|
### 第 5 步:写入数据
|
|
170
170
|
|
|
171
|
-
|
|
171
|
+
> **重要:** 对已注册表的所有写操作(INSERT/UPDATE/DELETE)**必须**通过 `engine.write()` 或手动使用 `engine.beginManualTransaction()` / `commitManualTransaction()` 包裹。**不要**在这些包裹之外直接调用 `db.run(...)` 写入——引擎需要管理事务和 2PC 协调,以确保数据正确同步到其他节点。
|
|
172
172
|
|
|
173
|
-
|
|
174
|
-
const ts = engine.hlc.tick();
|
|
175
|
-
db.run('INSERT INTO users (user_id, name, _hlc) VALUES (?, ?, ?)',
|
|
176
|
-
['u1', 'Alice', ts]);
|
|
177
|
-
engine.notifyLocalWrite(); // 推送到对等节点
|
|
178
|
-
```
|
|
173
|
+
**推荐:`engine.write()`(高阶 API)**
|
|
179
174
|
|
|
180
|
-
|
|
175
|
+
引擎自动处理完整生命周期:BEGIN → 执行 → 触发器记录 → 2PC prepare/ack → COMMIT → 广播 commit → 通知对等节点。
|
|
181
176
|
|
|
182
177
|
```js
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
JSON.stringify({ user_id: 'u1' }),
|
|
190
|
-
JSON.stringify({ user_id: 'u1', name: 'Alice', _hlc: ts }),
|
|
191
|
-
ts);
|
|
192
|
-
engine.notifyLocalWrite();
|
|
178
|
+
const result = await engine.write(async (db) => {
|
|
179
|
+
const ts = engine.hlc.tick();
|
|
180
|
+
db.run('INSERT INTO users (user_id, name, _hlc) VALUES (?, ?, ?)',
|
|
181
|
+
['u1', 'Alice', ts]);
|
|
182
|
+
return { userId: 'u1' }; // 返回值透传给调用方
|
|
183
|
+
}, userId); // shardKey 用于路由
|
|
193
184
|
```
|
|
194
185
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
## 2PC:强一致性写入
|
|
186
|
+
**低阶 API(需要精细控制时)**
|
|
198
187
|
|
|
199
|
-
|
|
188
|
+
如果需要更精细的控制(如在 prepare 前读取 entries),可使用低阶 API:
|
|
200
189
|
|
|
201
190
|
```js
|
|
202
191
|
const writeId = crypto.randomUUID();
|
|
@@ -479,13 +468,14 @@ VALUES ('${tableName}', 'DELETE', JSON_OBJECT(${delKeyExpr}), NULL, OLD._hlc)`,
|
|
|
479
468
|
| `tickPull()` | 从所有对等节点拉取变更 |
|
|
480
469
|
| `tickHeartbeat()` | 检查超时,发送心跳 |
|
|
481
470
|
| `tickCleanup()` | 清理旧的 sync_log 和墓碑记录 |
|
|
482
|
-
| `
|
|
483
|
-
| `
|
|
484
|
-
| `
|
|
485
|
-
| `
|
|
486
|
-
| `
|
|
487
|
-
| `
|
|
488
|
-
| `
|
|
471
|
+
| `write(fn, shardKey?)` | **高阶 2PC API**:在 2PC 事务中执行 fn,引擎自动处理全部流程 |
|
|
472
|
+
| `beginManualTransaction(id)` | 低阶:开始 2PC 事务 |
|
|
473
|
+
| `getManualTransactionEntries(id)` | 低阶:读取当前事务的 sync_log 条目 |
|
|
474
|
+
| `commitManualTransaction(id)` | 低阶:提交 2PC 事务 |
|
|
475
|
+
| `rollbackManualTransaction(id)` | 低阶:回滚 2PC 事务 |
|
|
476
|
+
| `waitForPrepareAck(writeId, entries, term, shardId)` | 低阶 2PC:广播 prepare,等待确认 |
|
|
477
|
+
| `broadcastCommit(writeId)` | 低阶 2PC:广播 commit |
|
|
478
|
+
| `broadcastAbort(writeId, reason)` | 低阶 2PC:广播 abort |
|
|
489
479
|
| `proxyRequest(key, payload)` | 将写请求转发给 Leader |
|
|
490
480
|
| `getShardId(key)` | 获取 key 对应的分片 ID |
|
|
491
481
|
| `isLeaderForShard(shardId)` | 检查本节点是否为该分片的 Leader |
|
package/index.js
CHANGED
|
@@ -81,19 +81,26 @@
|
|
|
81
81
|
* setInterval(() => engine.tickCleanup(), 3600_000);
|
|
82
82
|
* setInterval(() => engine.tickHeartbeat(), 15_000);
|
|
83
83
|
*
|
|
84
|
-
* // 7.
|
|
85
|
-
* db.run('INSERT
|
|
86
|
-
* engine.
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
* engine.notifyLocalWrite();
|
|
84
|
+
* // 7. 写操作:已注册表的所有增删改必须通过 engine.write() 或手动事务包裹
|
|
85
|
+
* // ⚠️ 不要直接 db.run('INSERT ...') — 引擎需要管理事务和 2PC 同步
|
|
86
|
+
* const result = await engine.write(async (db) => {
|
|
87
|
+
* const ts = engine.hlc.tick();
|
|
88
|
+
* db.run('INSERT INTO users ...', [ts, ...]);
|
|
89
|
+
* return { userId: 'u1' };
|
|
90
|
+
* }, userId);
|
|
92
91
|
*
|
|
93
92
|
* ═══════════════════════════════════════════════════════════════════
|
|
94
93
|
* 2PC 写操作流程(多节点强一致)
|
|
95
94
|
* ═══════════════════════════════════════════════════════════════════
|
|
96
95
|
*
|
|
96
|
+
* // 高阶 API(推荐):引擎自动处理 BEGIN/COMMIT/ROLLBACK 和 2PC 协调
|
|
97
|
+
* const result = await engine.write(async (db) => {
|
|
98
|
+
* const ts = engine.hlc.tick();
|
|
99
|
+
* db.run('INSERT INTO users ...', [ts, ...]);
|
|
100
|
+
* return { userId: 'u1' }; // 返回值透传
|
|
101
|
+
* }, userId); // shardKey 用于路由
|
|
102
|
+
*
|
|
103
|
+
* // 低阶 API(需要精细控制时使用)
|
|
97
104
|
* const writeId = crypto.randomUUID();
|
|
98
105
|
* engine.beginManualTransaction(writeId);
|
|
99
106
|
* try {
|
package/package.json
CHANGED
package/sync-engine.js
CHANGED
|
@@ -11,9 +11,12 @@
|
|
|
11
11
|
* - 不依赖 Node.js 特有 API
|
|
12
12
|
* - 不持有任何定时器(setTimeout/setInterval)
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
* 1.
|
|
16
|
-
*
|
|
14
|
+
* 所有外部能力通过构造函数注入:
|
|
15
|
+
* 1. dialect — 预定义 SQL 方言('sqlite' | 'postgresql' | 'mysql'),
|
|
16
|
+
* 自动填充 DatabaseAdapter 上的事务、upsert、schema、触发器方法。
|
|
17
|
+
* 不指定时需手动在 db 上实现全部方言方法,否则报错。
|
|
18
|
+
* 2. DatabaseAdapter — 数据库操作(基础查询:run/get/all/exec)
|
|
19
|
+
* 3. onXXX 回调 — 向外部通知事件 / 请求外部执行动作
|
|
17
20
|
*
|
|
18
21
|
* 所有周期性任务通过 tick*() 方法暴露,由外部调度:
|
|
19
22
|
* - tickPull() — 从所有 peer 拉取数据(建议 10s 间隔)
|
|
@@ -107,10 +110,14 @@
|
|
|
107
110
|
* setInterval(() => engine.tickCleanup(), 3600_000);
|
|
108
111
|
* setInterval(() => engine.tickHeartbeat(), 15_000);
|
|
109
112
|
*
|
|
110
|
-
* // 6.
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
* engine.
|
|
113
|
+
* // 6. 写操作:已注册表的所有增删改必须通过 engine.write() 或手动事务包裹
|
|
114
|
+
* // ⚠️ 不要直接 db.run('INSERT ...') — 引擎需要管理事务和 2PC 同步
|
|
115
|
+
* app.post('/api/create', async (req, res) => {
|
|
116
|
+
* const result = await engine.write(async (db) => {
|
|
117
|
+
* const ts = engine.hlc.tick();
|
|
118
|
+
* db.run('INSERT INTO users ...', [ts, ...]);
|
|
119
|
+
* return { ok: true };
|
|
120
|
+
* }, userId);
|
|
114
121
|
* });
|
|
115
122
|
*/
|
|
116
123
|
|
|
@@ -554,6 +561,71 @@ export class SyncEngine {
|
|
|
554
561
|
}
|
|
555
562
|
}
|
|
556
563
|
|
|
564
|
+
// ╔═══════════════════════════════════════╗
|
|
565
|
+
// ║ 2PC 写操作高阶 API ║
|
|
566
|
+
// ╚═══════════════════════════════════════╝
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* 在 2PC 事务中执行写操作(高阶 API,自动处理事务开启/提交/回滚)
|
|
570
|
+
*
|
|
571
|
+
* 适用于多节点部署下需要强一致性的写操作。
|
|
572
|
+
* 引擎自动完成:BEGIN → 执行 fn → 广播 prepare → 等待 quorum ack → COMMIT → 广播 commit
|
|
573
|
+
*
|
|
574
|
+
* @param {function(db: DatabaseAdapter): any} fn - 写操作函数,在事务中执行
|
|
575
|
+
* fn 接收 db 适配器,可直接调用 db.run/get/all 执行业务写操作。
|
|
576
|
+
* fn 的返回值会作为 write() 的返回值透传。
|
|
577
|
+
* @param {string} [shardKey] - 分片键(如 userId),用于确定 term 和 shardId
|
|
578
|
+
* @returns {Promise<any>} fn 的返回值
|
|
579
|
+
*
|
|
580
|
+
* @example
|
|
581
|
+
* const result = await engine.write(async (db) => {
|
|
582
|
+
* const ts = engine.hlc.tick();
|
|
583
|
+
* db.run('INSERT INTO users (user_id, name, _hlc) VALUES (?, ?, ?)', ['u1', 'Alice', ts]);
|
|
584
|
+
* return { userId: 'u1' };
|
|
585
|
+
* }, userId);
|
|
586
|
+
*/
|
|
587
|
+
async write(fn, shardKey) {
|
|
588
|
+
const writeId = randomUUID();
|
|
589
|
+
const shardId = shardKey ? this.getShardId(shardKey) : 0;
|
|
590
|
+
const term = this._shardElections.get(shardId)?._currentTerm || 0;
|
|
591
|
+
|
|
592
|
+
this.beginManualTransaction(writeId);
|
|
593
|
+
let result;
|
|
594
|
+
let prepareSent = false;
|
|
595
|
+
let committed = false;
|
|
596
|
+
try {
|
|
597
|
+
result = await fn(this.db);
|
|
598
|
+
|
|
599
|
+
const entries = this.getManualTransactionEntries(writeId);
|
|
600
|
+
if (entries.length > 0 && this._getActivePeerIds().length > 0) {
|
|
601
|
+
prepareSent = true;
|
|
602
|
+
await this.waitForPrepareAck(writeId, entries, term, shardId);
|
|
603
|
+
|
|
604
|
+
// 验证 term 未变(防止 prepare 等待期间发生选举导致脑裂)
|
|
605
|
+
const termAfter = this._shardElections.get(shardId)?._currentTerm || 0;
|
|
606
|
+
if (termAfter !== term) {
|
|
607
|
+
throw new Error('2PC prepare rejected: term changed during prepare (was ' + term + ', now ' + termAfter + ')');
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
this.commitManualTransaction(writeId);
|
|
612
|
+
committed = true;
|
|
613
|
+
|
|
614
|
+
if (entries && entries.length > 0) {
|
|
615
|
+
try { this.broadcastCommit(writeId); } catch (_) {}
|
|
616
|
+
}
|
|
617
|
+
} catch (err) {
|
|
618
|
+
if (!committed) {
|
|
619
|
+
this.rollbackManualTransaction(writeId);
|
|
620
|
+
}
|
|
621
|
+
if (prepareSent && !committed) {
|
|
622
|
+
try { this.broadcastAbort(writeId, err.message); } catch (_) {}
|
|
623
|
+
}
|
|
624
|
+
throw err;
|
|
625
|
+
}
|
|
626
|
+
return result;
|
|
627
|
+
}
|
|
628
|
+
|
|
557
629
|
// ╔═══════════════════════════════════════╗
|
|
558
630
|
// ║ 手动事务管理(2PC Leader 侧) ║
|
|
559
631
|
// ╚═══════════════════════════════════════╝
|