oak-backend-base 4.1.2 → 4.1.4
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/lib/AppLoader.d.ts +6 -3
- package/lib/AppLoader.js +38 -19
- package/lib/ClusterAppLoader.d.ts +9 -5
- package/lib/ClusterAppLoader.js +66 -23
- package/lib/DbStore.d.ts +1 -0
- package/lib/DbStore.js +3 -0
- package/lib/Synchronizer.d.ts +4 -8
- package/lib/Synchronizer.js +189 -272
- package/lib/cluster/DataSubscriber.d.ts +2 -2
- package/lib/cluster/DataSubscriber.js +2 -4
- package/package.json +5 -5
package/lib/AppLoader.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
2
|
import { EntityDict as BaseEntityDict } from 'oak-domain/lib/base-app-domain';
|
|
3
|
-
import { AppLoader as GeneralAppLoader, Trigger, EntityDict, Watcher, OpRecord, OperationResult } from "oak-domain/lib/types";
|
|
3
|
+
import { AppLoader as GeneralAppLoader, Trigger, EntityDict, Watcher, OpRecord, FreeTimer, OperationResult } from "oak-domain/lib/types";
|
|
4
4
|
import { DbStore } from "./DbStore";
|
|
5
5
|
import { BackendRuntimeContext } from 'oak-frontend-base/lib/context/BackendRuntimeContext';
|
|
6
6
|
import { IncomingHttpHeaders, IncomingMessage } from 'http';
|
|
@@ -34,10 +34,13 @@ export declare class AppLoader<ED extends EntityDict & BaseEntityDict, Cxt exten
|
|
|
34
34
|
initialize(): Promise<void>;
|
|
35
35
|
getStore(): DbStore<ED, Cxt>;
|
|
36
36
|
getEndpoints(prefix: string): [string, "post" | "get" | "put" | "delete", string, (params: Record<string, string>, headers: IncomingHttpHeaders, req: IncomingMessage, body?: any) => Promise<any>][];
|
|
37
|
-
protected operateInWatcher<T extends keyof ED>(entity: T, operation: ED[T]['Update'], context: Cxt): Promise<OperationResult<ED>>;
|
|
38
|
-
protected selectInWatcher<T extends keyof ED>(entity: T, selection: ED[T]['Selection'], context: Cxt): Promise<Partial<ED[T]["Schema"]>[]>;
|
|
37
|
+
protected operateInWatcher<T extends keyof ED>(entity: T, operation: ED[T]['Update'], context: Cxt, singleton?: true): Promise<OperationResult<ED>>;
|
|
38
|
+
protected selectInWatcher<T extends keyof ED>(entity: T, selection: ED[T]['Selection'], context: Cxt, singleton?: true): Promise<Partial<ED[T]["Schema"]>[]>;
|
|
39
39
|
protected execWatcher(watcher: Watcher<ED, keyof ED, Cxt>): Promise<OperationResult<ED> | undefined>;
|
|
40
|
+
protected getCheckpointTs(): number;
|
|
41
|
+
protected checkpoint(): Promise<number>;
|
|
40
42
|
startWatchers(): void;
|
|
43
|
+
protected execFreeTimer(timer: FreeTimer<ED, Cxt>, context: Cxt): Promise<OperationResult<ED>> | undefined;
|
|
41
44
|
startTimers(): void;
|
|
42
45
|
execStartRoutines(): Promise<void>;
|
|
43
46
|
execRoutine(routine: <Cxt extends AsyncContext<ED>>(context: Cxt) => Promise<void>): Promise<void>;
|
package/lib/AppLoader.js
CHANGED
|
@@ -270,10 +270,10 @@ class AppLoader extends types_1.AppLoader {
|
|
|
270
270
|
}
|
|
271
271
|
return endPointRouters;
|
|
272
272
|
}
|
|
273
|
-
operateInWatcher(entity, operation, context) {
|
|
273
|
+
operateInWatcher(entity, operation, context, singleton) {
|
|
274
274
|
return this.dbStore.operate(entity, operation, context, {});
|
|
275
275
|
}
|
|
276
|
-
selectInWatcher(entity, selection, context) {
|
|
276
|
+
selectInWatcher(entity, selection, context, singleton) {
|
|
277
277
|
return this.dbStore.select(entity, selection, context, {
|
|
278
278
|
blockTrigger: true,
|
|
279
279
|
forUpdate: true,
|
|
@@ -284,24 +284,24 @@ class AppLoader extends types_1.AppLoader {
|
|
|
284
284
|
let result;
|
|
285
285
|
try {
|
|
286
286
|
if (watcher.hasOwnProperty('actionData')) {
|
|
287
|
-
const { entity, action, filter, actionData } = watcher;
|
|
287
|
+
const { entity, action, filter, actionData, singleton } = watcher;
|
|
288
288
|
const filter2 = typeof filter === 'function' ? await filter() : (0, lodash_1.cloneDeep)(filter);
|
|
289
289
|
const data = typeof actionData === 'function' ? await (actionData)() : (0, lodash_1.cloneDeep)(actionData);
|
|
290
290
|
result = await this.operateInWatcher(entity, {
|
|
291
291
|
id: await (0, uuid_1.generateNewIdAsync)(),
|
|
292
292
|
action: action,
|
|
293
293
|
data,
|
|
294
|
-
filter: filter2
|
|
295
|
-
}, context);
|
|
294
|
+
filter: filter2,
|
|
295
|
+
}, context, singleton);
|
|
296
296
|
}
|
|
297
297
|
else {
|
|
298
|
-
const { entity, projection, fn, filter } = watcher;
|
|
298
|
+
const { entity, projection, fn, filter, singleton } = watcher;
|
|
299
299
|
const filter2 = typeof filter === 'function' ? await filter() : (0, lodash_1.cloneDeep)(filter);
|
|
300
300
|
const projection2 = typeof projection === 'function' ? await projection() : (0, lodash_1.cloneDeep)(projection);
|
|
301
301
|
const rows = await this.selectInWatcher(entity, {
|
|
302
302
|
data: projection2,
|
|
303
303
|
filter: filter2,
|
|
304
|
-
}, context);
|
|
304
|
+
}, context, singleton);
|
|
305
305
|
if (rows.length > 0) {
|
|
306
306
|
result = await fn(context, rows);
|
|
307
307
|
}
|
|
@@ -310,10 +310,22 @@ class AppLoader extends types_1.AppLoader {
|
|
|
310
310
|
return result;
|
|
311
311
|
}
|
|
312
312
|
catch (err) {
|
|
313
|
-
|
|
313
|
+
if (err instanceof types_1.OakPartialSuccess) {
|
|
314
|
+
await context.commit();
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
await context.rollback();
|
|
318
|
+
}
|
|
314
319
|
throw err;
|
|
315
320
|
}
|
|
316
321
|
}
|
|
322
|
+
getCheckpointTs() {
|
|
323
|
+
const now = Date.now();
|
|
324
|
+
return process.env.NODE_ENV === 'development' ? now - 30 * 1000 : now - 120 * 1000;
|
|
325
|
+
}
|
|
326
|
+
checkpoint() {
|
|
327
|
+
return this.dbStore.checkpoint(this.getCheckpointTs());
|
|
328
|
+
}
|
|
317
329
|
startWatchers() {
|
|
318
330
|
const watchers = this.requireSth('lib/watchers/index');
|
|
319
331
|
const { ActionDefDict } = require(`${this.path}/lib/oak-app-domain/ActionDefDict`);
|
|
@@ -323,7 +335,9 @@ class AppLoader extends types_1.AppLoader {
|
|
|
323
335
|
const execOne = async (watcher, start) => {
|
|
324
336
|
try {
|
|
325
337
|
const result = await this.execWatcher(watcher);
|
|
326
|
-
|
|
338
|
+
if (result) {
|
|
339
|
+
console.log(`执行watcher【${watcher.name}】成功,耗时【${Date.now() - start}】,结果是:`, result);
|
|
340
|
+
}
|
|
327
341
|
}
|
|
328
342
|
catch (err) {
|
|
329
343
|
console.error(`执行watcher【${watcher.name}】失败,耗时【${Date.now() - start}】,结果是:`, err);
|
|
@@ -337,9 +351,8 @@ class AppLoader extends types_1.AppLoader {
|
|
|
337
351
|
}
|
|
338
352
|
const duration = Date.now() - start;
|
|
339
353
|
console.log(`第${count}次执行watchers,共执行${watchers.length}个,耗时${duration}毫秒`);
|
|
340
|
-
const now = Date.now();
|
|
341
354
|
try {
|
|
342
|
-
await this.
|
|
355
|
+
await this.checkpoint();
|
|
343
356
|
}
|
|
344
357
|
catch (err) {
|
|
345
358
|
console.error(`执行了checkpoint,发生错误:`, err);
|
|
@@ -348,6 +361,10 @@ class AppLoader extends types_1.AppLoader {
|
|
|
348
361
|
};
|
|
349
362
|
doWatchers();
|
|
350
363
|
}
|
|
364
|
+
execFreeTimer(timer, context) {
|
|
365
|
+
const { timer: timerFn } = timer;
|
|
366
|
+
return timerFn(context);
|
|
367
|
+
}
|
|
351
368
|
startTimers() {
|
|
352
369
|
const timers = this.requireSth('lib/timers/index');
|
|
353
370
|
if (timers) {
|
|
@@ -368,14 +385,20 @@ class AppLoader extends types_1.AppLoader {
|
|
|
368
385
|
else {
|
|
369
386
|
const context = await this.makeContext();
|
|
370
387
|
try {
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
|
|
388
|
+
const result = await this.execFreeTimer(timer, context);
|
|
389
|
+
if (result) {
|
|
390
|
+
console.log(`定时器【${name}】执行成功,耗时${Date.now() - start}毫秒,结果是【${result}】`);
|
|
391
|
+
}
|
|
374
392
|
await context.commit();
|
|
375
393
|
}
|
|
376
394
|
catch (err) {
|
|
377
395
|
console.warn(`定时器【${name}】执行失败,耗时${Date.now() - start}毫秒,错误是`, err);
|
|
378
|
-
|
|
396
|
+
if (err instanceof types_1.OakPartialSuccess) {
|
|
397
|
+
await context.commit();
|
|
398
|
+
}
|
|
399
|
+
else {
|
|
400
|
+
await context.rollback();
|
|
401
|
+
}
|
|
379
402
|
}
|
|
380
403
|
}
|
|
381
404
|
});
|
|
@@ -384,10 +407,6 @@ class AppLoader extends types_1.AppLoader {
|
|
|
384
407
|
}
|
|
385
408
|
async execStartRoutines() {
|
|
386
409
|
const routines = this.requireSth('lib/routines/start') || [];
|
|
387
|
-
if (this.synchronizer) {
|
|
388
|
-
const routine = this.synchronizer.getSyncRoutine();
|
|
389
|
-
routines.push(routine);
|
|
390
|
-
}
|
|
391
410
|
for (const routine of routines) {
|
|
392
411
|
if (routine.hasOwnProperty('entity')) {
|
|
393
412
|
const start = Date.now();
|
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
import { EntityDict as BaseEntityDict } from 'oak-domain/lib/base-app-domain';
|
|
2
|
-
import { EntityDict, OperationResult, Trigger } from 'oak-domain/lib/types';
|
|
2
|
+
import { EntityDict, OperationResult, Trigger, Watcher, FreeTimer } from 'oak-domain/lib/types';
|
|
3
3
|
import { BackendRuntimeContext } from 'oak-frontend-base/lib/context/BackendRuntimeContext';
|
|
4
4
|
import { AppLoader } from './AppLoader';
|
|
5
5
|
import { Namespace } from 'socket.io';
|
|
6
6
|
import { Socket } from 'socket.io-client';
|
|
7
7
|
export declare class ClusterAppLoader<ED extends EntityDict & BaseEntityDict, Cxt extends BackendRuntimeContext<ED>> extends AppLoader<ED, Cxt> {
|
|
8
|
-
protected socket: Socket;
|
|
9
8
|
private csTriggers;
|
|
10
|
-
|
|
9
|
+
static VolatileTriggerEvent: string;
|
|
10
|
+
protected socket: Socket;
|
|
11
|
+
private connectServerSocket;
|
|
11
12
|
private sub;
|
|
12
13
|
constructor(path: string, nsDs: Namespace, nsServer: Namespace, socketPath: string);
|
|
13
14
|
protected registerTrigger(trigger: Trigger<ED, keyof ED, Cxt>): void;
|
|
14
|
-
protected operateInWatcher<T extends keyof ED>(entity: T, operation: ED[T]['Update'], context: Cxt): Promise<OperationResult<ED>>;
|
|
15
|
-
protected selectInWatcher<T extends keyof ED>(entity: T, selection: ED[T]['Selection'], context: Cxt): Promise<Partial<ED[T]['Schema']>[]>;
|
|
15
|
+
protected operateInWatcher<T extends keyof ED>(entity: T, operation: ED[T]['Update'], context: Cxt, singleton?: true): Promise<OperationResult<ED>>;
|
|
16
|
+
protected selectInWatcher<T extends keyof ED>(entity: T, selection: ED[T]['Selection'], context: Cxt, singleton?: true): Promise<Partial<ED[T]['Schema']>[]>;
|
|
17
|
+
protected execWatcher(watcher: Watcher<ED, keyof ED, Cxt>): Promise<OperationResult<ED> | undefined>;
|
|
18
|
+
protected execFreeTimer(timer: FreeTimer<ED, Cxt>, context: Cxt): Promise<OperationResult<ED>> | undefined;
|
|
19
|
+
protected checkpoint(): Promise<number>;
|
|
16
20
|
}
|
package/lib/ClusterAppLoader.js
CHANGED
|
@@ -10,9 +10,10 @@ const AppLoader_1 = require("./AppLoader");
|
|
|
10
10
|
const assert_1 = tslib_1.__importDefault(require("assert"));
|
|
11
11
|
const socket_io_client_1 = require("socket.io-client");
|
|
12
12
|
class ClusterAppLoader extends AppLoader_1.AppLoader {
|
|
13
|
-
socket;
|
|
14
13
|
csTriggers;
|
|
15
|
-
|
|
14
|
+
static VolatileTriggerEvent = 'vtEvent';
|
|
15
|
+
socket;
|
|
16
|
+
connectServerSocket() {
|
|
16
17
|
const { instanceId } = (0, env_1.getClusterInfo)();
|
|
17
18
|
this.socket.on('connect', () => {
|
|
18
19
|
const csTriggerNames = Object.keys(this.csTriggers).map(ele => `${ele}-${instanceId}`);
|
|
@@ -20,14 +21,11 @@ class ClusterAppLoader extends AppLoader_1.AppLoader {
|
|
|
20
21
|
this.socket.emit('sub', csTriggerNames);
|
|
21
22
|
}
|
|
22
23
|
});
|
|
23
|
-
this.socket.on(
|
|
24
|
-
const csTriggerNames = Object.keys(this.csTriggers).map(ele => `${ele}-${instanceId}`);
|
|
25
|
-
if (csTriggerNames.length > 0) {
|
|
26
|
-
this.socket.connect();
|
|
27
|
-
}
|
|
28
|
-
});
|
|
29
|
-
this.socket.on('data', async (entity, name, ids, cxtStr, option) => {
|
|
24
|
+
this.socket.on(ClusterAppLoader.VolatileTriggerEvent, async (entity, name, ids, cxtStr, option) => {
|
|
30
25
|
const context = await this.makeContext(cxtStr);
|
|
26
|
+
if (process.env.NODE_ENV === 'development') {
|
|
27
|
+
console.log(`「${(0, env_1.getClusterInfo)().instanceId}」号实例接收到来自其它进程的volatileTrigger请求, name是「${name}」, ids是「${ids.join(',')}」`);
|
|
28
|
+
}
|
|
31
29
|
// await context.begin();
|
|
32
30
|
try {
|
|
33
31
|
await this.dbStore.execVolatileTrigger(entity, name, ids, context, option);
|
|
@@ -38,12 +36,11 @@ class ClusterAppLoader extends AppLoader_1.AppLoader {
|
|
|
38
36
|
console.error('在集群环境下,处理来自其它实例的trigger数据,execVolatileTrigger异常', entity, name, ids, option, err);
|
|
39
37
|
}
|
|
40
38
|
});
|
|
41
|
-
this.socket.connect();
|
|
42
39
|
}
|
|
43
|
-
sub(name) {
|
|
40
|
+
sub(name, clusterSensative) {
|
|
44
41
|
const { instanceId } = (0, env_1.getClusterInfo)();
|
|
45
42
|
(0, assert_1.default)(!this.csTriggers[name], `命名为${name}的trigger出现了多次,请检查`);
|
|
46
|
-
this.csTriggers[name] =
|
|
43
|
+
this.csTriggers[name] = !!clusterSensative;
|
|
47
44
|
if (this.socket.connected) {
|
|
48
45
|
this.socket.emit('sub', [`${name}-${instanceId}`]);
|
|
49
46
|
}
|
|
@@ -62,8 +59,12 @@ class ClusterAppLoader extends AppLoader_1.AppLoader {
|
|
|
62
59
|
await context.commit();
|
|
63
60
|
}
|
|
64
61
|
catch (err) {
|
|
65
|
-
|
|
66
|
-
|
|
62
|
+
if (err instanceof types_1.OakPartialSuccess) {
|
|
63
|
+
await context.commit();
|
|
64
|
+
console.error('execVolatileTrigger异常', entity, trigger.name, ids2, option, err);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
await context.rollback();
|
|
67
68
|
console.error('execVolatileTrigger异常', entity, trigger.name, ids2, option, err);
|
|
68
69
|
}
|
|
69
70
|
}
|
|
@@ -89,35 +90,52 @@ class ClusterAppLoader extends AppLoader_1.AppLoader {
|
|
|
89
90
|
await execLocal(ids2);
|
|
90
91
|
}
|
|
91
92
|
else {
|
|
92
|
-
|
|
93
|
+
if (process.env.NODE_ENV === 'development') {
|
|
94
|
+
console.log(`在trigger「${trigger.name}」上,因为cs原因,数据「${ids2.join(',')}」将被推送到「${seqMod}」号进程操作(当前进程号是「${instanceId}」)。`);
|
|
95
|
+
}
|
|
96
|
+
this.dataSubscriber.publishServerEvent(`${trigger.name}-${seqMod}`, ClusterAppLoader.VolatileTriggerEvent, entity, trigger.name, ids2, cxtStr, option);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
else if (trigger.singleton) {
|
|
101
|
+
if ((0, env_1.getClusterInfo)().instanceId === 0) {
|
|
102
|
+
await execLocal(ids);
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
if (process.env.NODE_ENV === 'development') {
|
|
106
|
+
console.log(`在trigger「${trigger.name}」上,因为singleton原因,数据「${ids.join(',')}」将被推送到「0」号进程操作(当前进程号是「${(0, env_1.getClusterInfo)().instanceId}」)`);
|
|
93
107
|
}
|
|
108
|
+
this.dataSubscriber.publishServerEvent(`${trigger.name}-0`, ClusterAppLoader.VolatileTriggerEvent, entity, trigger.name, ids, cxtStr, option);
|
|
94
109
|
}
|
|
95
110
|
}
|
|
96
111
|
else {
|
|
97
112
|
await execLocal(ids);
|
|
98
113
|
}
|
|
99
114
|
});
|
|
115
|
+
this.csTriggers = {};
|
|
100
116
|
const { name } = nsServer;
|
|
101
117
|
const socketUrl = `http://localhost:${process.env.PM2_PORT || 8080}${name}`;
|
|
102
118
|
this.socket = (0, socket_io_client_1.io)(socketUrl, {
|
|
103
119
|
path: socketPath,
|
|
104
120
|
});
|
|
105
|
-
this.
|
|
106
|
-
this.csTriggers = {};
|
|
121
|
+
this.connectServerSocket();
|
|
107
122
|
}
|
|
108
123
|
registerTrigger(trigger) {
|
|
109
124
|
// 如果是cluster sensative的trigger,注册到socket事件上
|
|
110
|
-
|
|
125
|
+
// 如果是singletone的trigger,只有0号实例注册
|
|
126
|
+
const { when, cs, singleton } = trigger;
|
|
127
|
+
(0, assert_1.default)(!(cs && singleton));
|
|
128
|
+
if (when === 'commit' && (cs || singleton && (0, env_1.getClusterInfo)().instanceId === 0)) {
|
|
111
129
|
const { name } = trigger;
|
|
112
|
-
this.sub(name);
|
|
130
|
+
this.sub(name, cs);
|
|
113
131
|
}
|
|
114
132
|
this.dbStore.registerTrigger(trigger);
|
|
115
133
|
}
|
|
116
|
-
operateInWatcher(entity, operation, context) {
|
|
134
|
+
operateInWatcher(entity, operation, context, singleton) {
|
|
117
135
|
const { instanceCount, instanceId } = (0, env_1.getClusterInfo)();
|
|
118
136
|
(0, assert_1.default)(instanceCount && typeof instanceId === 'number');
|
|
119
137
|
const { filter } = operation;
|
|
120
|
-
const filter2 = (0, filter_1.combineFilters)(entity, this.dbStore.getSchema(), [filter, {
|
|
138
|
+
const filter2 = singleton ? filter : (0, filter_1.combineFilters)(entity, this.dbStore.getSchema(), [filter, {
|
|
121
139
|
$$seq$$: {
|
|
122
140
|
$mod: [instanceCount, instanceId]
|
|
123
141
|
}
|
|
@@ -127,11 +145,11 @@ class ClusterAppLoader extends AppLoader_1.AppLoader {
|
|
|
127
145
|
filter: filter2,
|
|
128
146
|
}, context);
|
|
129
147
|
}
|
|
130
|
-
selectInWatcher(entity, selection, context) {
|
|
148
|
+
selectInWatcher(entity, selection, context, singleton) {
|
|
131
149
|
const { instanceCount, instanceId } = (0, env_1.getClusterInfo)();
|
|
132
150
|
(0, assert_1.default)(instanceCount && typeof instanceId === 'number');
|
|
133
151
|
const { filter } = selection;
|
|
134
|
-
const filter2 = (0, filter_1.combineFilters)(entity, this.dbStore.getSchema(), [filter, {
|
|
152
|
+
const filter2 = singleton ? filter : (0, filter_1.combineFilters)(entity, this.dbStore.getSchema(), [filter, {
|
|
135
153
|
$$seq$$: {
|
|
136
154
|
$mod: [instanceCount, instanceId]
|
|
137
155
|
}
|
|
@@ -141,5 +159,30 @@ class ClusterAppLoader extends AppLoader_1.AppLoader {
|
|
|
141
159
|
filter: filter2,
|
|
142
160
|
}, context);
|
|
143
161
|
}
|
|
162
|
+
async execWatcher(watcher) {
|
|
163
|
+
if (watcher.singleton && (0, env_1.getClusterInfo)().instanceId !== 0) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
return super.execWatcher(watcher);
|
|
167
|
+
}
|
|
168
|
+
execFreeTimer(timer, context) {
|
|
169
|
+
if (timer.singleton && (0, env_1.getClusterInfo)().instanceId !== 0) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
return super.execFreeTimer(timer, context);
|
|
173
|
+
}
|
|
174
|
+
async checkpoint() {
|
|
175
|
+
const { instanceCount, instanceId } = (0, env_1.getClusterInfo)();
|
|
176
|
+
let count = 0;
|
|
177
|
+
for (const name in this.csTriggers) {
|
|
178
|
+
if (this.csTriggers[name]) {
|
|
179
|
+
count += await this.dbStore.independentCheckPoint(name, this.getCheckpointTs(), instanceCount, instanceId);
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
count += await this.dbStore.independentCheckPoint(name, this.getCheckpointTs());
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return count;
|
|
186
|
+
}
|
|
144
187
|
}
|
|
145
188
|
exports.ClusterAppLoader = ClusterAppLoader;
|
package/lib/DbStore.d.ts
CHANGED
|
@@ -17,4 +17,5 @@ export declare class DbStore<ED extends EntityDict & BaseEntityDict, Cxt extends
|
|
|
17
17
|
setOnVolatileTrigger(onVolatileTrigger: <T extends keyof ED>(entity: T, trigger: VolatileTrigger<ED, T, Cxt>, ids: string[], cxtStr: string, option: OperateOption) => Promise<void>): void;
|
|
18
18
|
execVolatileTrigger<T extends keyof ED>(entity: T, name: string, ids: string[], context: Cxt, option: OperateOption): Promise<void>;
|
|
19
19
|
checkpoint(ts: number): Promise<number>;
|
|
20
|
+
independentCheckPoint(name: string, ts: number, instanceCount?: number, instanceId?: number): Promise<number>;
|
|
20
21
|
}
|
package/lib/DbStore.js
CHANGED
|
@@ -118,5 +118,8 @@ class DbStore extends oak_db_1.MysqlStore {
|
|
|
118
118
|
checkpoint(ts) {
|
|
119
119
|
return this.executor.checkpoint(ts);
|
|
120
120
|
}
|
|
121
|
+
independentCheckPoint(name, ts, instanceCount, instanceId) {
|
|
122
|
+
return this.executor.independentCheckPoint(name, ts, instanceCount, instanceId);
|
|
123
|
+
}
|
|
121
124
|
}
|
|
122
125
|
exports.DbStore = DbStore;
|
package/lib/Synchronizer.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EntityDict, StorageSchema, EndpointItem, SyncConfig
|
|
1
|
+
import { EntityDict, StorageSchema, EndpointItem, SyncConfig } from 'oak-domain/lib/types';
|
|
2
2
|
import { VolatileTrigger } from 'oak-domain/lib/types/Trigger';
|
|
3
3
|
import { EntityDict as BaseEntityDict } from 'oak-domain/lib/base-app-domain';
|
|
4
4
|
import { BackendRuntimeContext } from 'oak-frontend-base/lib/context/BackendRuntimeContext';
|
|
@@ -9,12 +9,8 @@ export default class Synchronizer<ED extends EntityDict & BaseEntityDict, Cxt ex
|
|
|
9
9
|
private channelDict;
|
|
10
10
|
private contextBuilder;
|
|
11
11
|
private pushAccessMap;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
* @param channel
|
|
15
|
-
* @param retry
|
|
16
|
-
*/
|
|
17
|
-
private startChannel;
|
|
12
|
+
private startChannel2;
|
|
13
|
+
/**开始同步这些channel上的oper。注意,这时候即使某个channel上失败了,也不应影响本事务提交(其它的channel成功了) */
|
|
18
14
|
private startAllChannel;
|
|
19
15
|
private pushOperToChannel;
|
|
20
16
|
private refineOperData;
|
|
@@ -32,6 +28,6 @@ export default class Synchronizer<ED extends EntityDict & BaseEntityDict, Cxt ex
|
|
|
32
28
|
* @returns
|
|
33
29
|
*/
|
|
34
30
|
getSyncTriggers(): VolatileTrigger<ED, keyof ED, Cxt>[];
|
|
35
|
-
getSyncRoutine(): FreeRoutine<ED, Cxt>;
|
|
36
31
|
getSelfEndpoint(): EndpointItem<ED, Cxt>;
|
|
32
|
+
tryCreateSyncProcess(): void;
|
|
37
33
|
}
|
package/lib/Synchronizer.js
CHANGED
|
@@ -18,135 +18,85 @@ class Synchronizer {
|
|
|
18
18
|
channelDict = {};
|
|
19
19
|
contextBuilder;
|
|
20
20
|
pushAccessMap = {};
|
|
21
|
-
|
|
22
|
-
* 向某一个远端对象push opers。根据幂等性,这里如果失败了必须反复推送
|
|
23
|
-
* @param channel
|
|
24
|
-
* @param retry
|
|
25
|
-
*/
|
|
26
|
-
async startChannel(context, channel, retry) {
|
|
21
|
+
async startChannel2(context, channel) {
|
|
27
22
|
const { queue, api, selfEncryptInfo, entity, entityId } = channel;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
// todo 加密
|
|
31
|
-
const queue = channel.queue;
|
|
32
|
-
const opers = queue.map(ele => ele.oper);
|
|
33
|
-
if (process.env.NODE_ENV === 'development') {
|
|
34
|
-
console.log('向远端结点sync oper', JSON.stringify(opers.map(ele => ({
|
|
35
|
-
id: ele.id,
|
|
36
|
-
seq: ele.$$seq$$,
|
|
37
|
-
}))), 'txnId:', context.getCurrentTxnId());
|
|
38
|
-
}
|
|
39
|
-
const finalApi = (0, path_1.join)(api, selfEncryptInfo.id);
|
|
40
|
-
const res = await fetch(finalApi, {
|
|
41
|
-
method: 'post',
|
|
42
|
-
headers: {
|
|
43
|
-
'Content-Type': 'application/json',
|
|
44
|
-
[OAK_SYNC_HEADER_ENTITY]: entity,
|
|
45
|
-
[OAK_SYNC_HEADER_ENTITYID]: entityId,
|
|
46
|
-
},
|
|
47
|
-
body: JSON.stringify(opers),
|
|
48
|
-
});
|
|
49
|
-
if (res.status !== 200) {
|
|
50
|
-
throw new Error(`sync数据时,访问api「${finalApi}」的结果不是200。「${res.status}」`);
|
|
51
|
-
}
|
|
52
|
-
json = await res.json();
|
|
53
|
-
}
|
|
54
|
-
catch (err) {
|
|
55
|
-
// 最大延迟redo时间512秒
|
|
56
|
-
const retryDelay = Math.pow(2, Math.min(9, retry)) * 1000;
|
|
57
|
-
console.error('sync push时出现error', err);
|
|
58
|
-
console.error(`将于${retryDelay}毫秒后重试`);
|
|
59
|
-
return new Promise((resolve) => {
|
|
60
|
-
setTimeout(async () => {
|
|
61
|
-
await this.startChannel(context, channel, retry + 1);
|
|
62
|
-
resolve(undefined);
|
|
63
|
-
}, retryDelay);
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
/**
|
|
67
|
-
* 返回结构见this.getSelfEndpoint
|
|
68
|
-
*/
|
|
23
|
+
// todo 加密
|
|
24
|
+
const opers = queue.map(ele => ele.oper);
|
|
69
25
|
if (process.env.NODE_ENV === 'development') {
|
|
70
|
-
console.log('
|
|
26
|
+
console.log('向远端结点sync oper', JSON.stringify(opers.map(ele => ({
|
|
27
|
+
id: ele.id,
|
|
28
|
+
seq: ele.$$seq$$,
|
|
29
|
+
}))), 'txnId:', context.getCurrentTxnId());
|
|
71
30
|
}
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
31
|
+
const finalApi = (0, path_1.join)(api, selfEncryptInfo.id);
|
|
32
|
+
channel.queue = [];
|
|
33
|
+
const res = await fetch(finalApi, {
|
|
34
|
+
method: 'post',
|
|
35
|
+
headers: {
|
|
36
|
+
'Content-Type': 'application/json',
|
|
37
|
+
[OAK_SYNC_HEADER_ENTITY]: entity,
|
|
38
|
+
[OAK_SYNC_HEADER_ENTITYID]: entityId,
|
|
39
|
+
},
|
|
40
|
+
body: JSON.stringify(opers),
|
|
41
|
+
});
|
|
42
|
+
if (res.status !== 200) {
|
|
43
|
+
throw new Error(`sync数据时,访问api「${finalApi}」的结果不是200。「${res.status}」`);
|
|
76
44
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
45
|
+
// 如果是200,则已经成功
|
|
46
|
+
for (const ele of queue) {
|
|
47
|
+
const { oper, onSynchronized } = ele;
|
|
48
|
+
if (onSynchronized) {
|
|
49
|
+
const operEntityArr = await context.select('operEntity', {
|
|
50
|
+
data: {
|
|
51
|
+
id: 1,
|
|
52
|
+
entity: 1,
|
|
53
|
+
entityId: 1
|
|
54
|
+
},
|
|
55
|
+
filter: {
|
|
56
|
+
operId: oper.id,
|
|
57
|
+
}
|
|
58
|
+
}, {});
|
|
59
|
+
const entityIds = operEntityArr.map(ele => ele.entityId);
|
|
60
|
+
return onSynchronized && onSynchronized({
|
|
61
|
+
action: oper.action,
|
|
62
|
+
data: oper.data,
|
|
63
|
+
// rowIds: getRelevantIds(oper.filter!),
|
|
64
|
+
rowIds: entityIds
|
|
65
|
+
}, context);
|
|
85
66
|
}
|
|
86
67
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
68
|
+
/**
|
|
69
|
+
* 自主将triggerData属性清零
|
|
70
|
+
*/
|
|
71
|
+
const operIds = queue.map(ele => ele.oper.id);
|
|
72
|
+
await context.operate('oper', {
|
|
73
|
+
id: await (0, uuid_1.generateNewIdAsync)(),
|
|
74
|
+
action: 'update',
|
|
75
|
+
data: {
|
|
76
|
+
[types_1.TriggerDataAttribute]: null,
|
|
77
|
+
[types_1.TriggerUuidAttribute]: null,
|
|
78
|
+
},
|
|
79
|
+
filter: {
|
|
80
|
+
id: {
|
|
81
|
+
$in: operIds,
|
|
100
82
|
}
|
|
101
|
-
}, {});
|
|
102
|
-
}
|
|
103
|
-
if (successIds.length > 0) {
|
|
104
|
-
try {
|
|
105
|
-
await Promise.all(successIds.map(async (id) => {
|
|
106
|
-
const { onSynchronized, oper } = queue.find(ele => ele.oper.id === id);
|
|
107
|
-
const operEntityArr = await context.select('operEntity', {
|
|
108
|
-
data: {
|
|
109
|
-
id: 1,
|
|
110
|
-
entity: 1,
|
|
111
|
-
entityId: 1
|
|
112
|
-
},
|
|
113
|
-
filter: {
|
|
114
|
-
operId: oper.id,
|
|
115
|
-
}
|
|
116
|
-
}, {});
|
|
117
|
-
const entityIds = operEntityArr.map(ele => ele.entityId);
|
|
118
|
-
return onSynchronized && onSynchronized({
|
|
119
|
-
action: oper.action,
|
|
120
|
-
data: oper.data,
|
|
121
|
-
// rowIds: getRelevantIds(oper.filter!),
|
|
122
|
-
rowIds: entityIds
|
|
123
|
-
}, context);
|
|
124
|
-
}));
|
|
125
|
-
}
|
|
126
|
-
catch (err) {
|
|
127
|
-
// 这时候无法处理?
|
|
128
|
-
console.error('onSynchronzied时出错', err);
|
|
129
|
-
(0, assert_1.default)(false);
|
|
130
83
|
}
|
|
131
|
-
}
|
|
132
|
-
if (channel.queue.length > 0) {
|
|
133
|
-
// 最大延迟redo时间512秒
|
|
134
|
-
const retryDelay = Math.pow(2, Math.min(9, retry)) * 1000;
|
|
135
|
-
console.error(`有${channel.queue.length}个oper同步失败,id是「${channel.queue.map(ele => ele.oper.id).join(',')}」,将于${retryDelay}毫秒后重试`);
|
|
136
|
-
return new Promise((resolve) => {
|
|
137
|
-
setTimeout(async () => {
|
|
138
|
-
await this.startChannel(context, channel, retry + 1);
|
|
139
|
-
resolve(undefined);
|
|
140
|
-
}, retryDelay);
|
|
141
|
-
});
|
|
142
|
-
}
|
|
84
|
+
}, {});
|
|
143
85
|
}
|
|
86
|
+
/**开始同步这些channel上的oper。注意,这时候即使某个channel上失败了,也不应影响本事务提交(其它的channel成功了) */
|
|
144
87
|
async startAllChannel(context) {
|
|
145
|
-
await Promise.all(Object.keys(this.channelDict).map(async (k) => {
|
|
88
|
+
return await Promise.all(Object.keys(this.channelDict).map(async (k) => {
|
|
146
89
|
const channel = this.channelDict[k];
|
|
147
90
|
if (channel.queue.length > 0) {
|
|
148
91
|
channel.queue.sort((o1, o2) => o1.oper.$$seq$$ - o2.oper.$$seq$$);
|
|
149
|
-
|
|
92
|
+
try {
|
|
93
|
+
return this.startChannel2(context, channel);
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
const msg = `startChannel推送数据出错,channel是「${k}」,异常是「${err.message}」`;
|
|
97
|
+
console.error(err);
|
|
98
|
+
return new types_1.OakPartialSuccess(msg);
|
|
99
|
+
}
|
|
150
100
|
}
|
|
151
101
|
}));
|
|
152
102
|
}
|
|
@@ -258,94 +208,91 @@ class Synchronizer {
|
|
|
258
208
|
* 每个进程都保证把当前所有的oper顺序处理掉,就不会有乱序的问题,大家通过database上的锁来完成同步
|
|
259
209
|
* @param context
|
|
260
210
|
*/
|
|
261
|
-
async trySynchronizeOpers() {
|
|
262
|
-
|
|
263
|
-
await context.begin();
|
|
211
|
+
async trySynchronizeOpers(context) {
|
|
212
|
+
let result = undefined;
|
|
264
213
|
// 暂时全用root身份去执行(未来不一定对)
|
|
265
214
|
await context.initialize();
|
|
266
215
|
context.openRootMode();
|
|
267
|
-
|
|
268
|
-
|
|
216
|
+
let dirtyOpers = await context.select('oper', {
|
|
217
|
+
data: {
|
|
218
|
+
id: 1,
|
|
219
|
+
},
|
|
220
|
+
filter: {
|
|
221
|
+
[types_1.TriggerDataAttribute]: {
|
|
222
|
+
$exists: true,
|
|
223
|
+
},
|
|
224
|
+
}
|
|
225
|
+
}, { dontCollect: true });
|
|
226
|
+
if (dirtyOpers.length > 0) {
|
|
227
|
+
// 这一步是加锁,保证只有一个进程完成推送,推送者提交前会将$$triggerData$$清零
|
|
228
|
+
const ids = dirtyOpers.map(ele => ele.id);
|
|
229
|
+
dirtyOpers = await context.select('oper', {
|
|
269
230
|
data: {
|
|
270
231
|
id: 1,
|
|
232
|
+
action: 1,
|
|
233
|
+
data: 1,
|
|
234
|
+
targetEntity: 1,
|
|
235
|
+
operatorId: 1,
|
|
236
|
+
[types_1.TriggerDataAttribute]: 1,
|
|
237
|
+
bornAt: 1,
|
|
238
|
+
$$createAt$$: 1,
|
|
239
|
+
$$seq$$: 1,
|
|
240
|
+
filter: 1,
|
|
241
|
+
operEntity$oper: {
|
|
242
|
+
$entity: 'operEntity',
|
|
243
|
+
data: {
|
|
244
|
+
entityId: 1,
|
|
245
|
+
operId: 1,
|
|
246
|
+
entity: 1,
|
|
247
|
+
id: 1,
|
|
248
|
+
}
|
|
249
|
+
}
|
|
271
250
|
},
|
|
272
251
|
filter: {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
}, { dontCollect: true });
|
|
252
|
+
id: { $in: ids },
|
|
253
|
+
},
|
|
254
|
+
}, { dontCollect: true, forUpdate: true });
|
|
255
|
+
dirtyOpers = dirtyOpers.filter(ele => !!ele[types_1.TriggerDataAttribute]);
|
|
278
256
|
if (dirtyOpers.length > 0) {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
[types_1.TriggerDataAttribute]: 1,
|
|
289
|
-
bornAt: 1,
|
|
290
|
-
$$createAt$$: 1,
|
|
291
|
-
$$seq$$: 1,
|
|
292
|
-
filter: 1,
|
|
293
|
-
operEntity$oper: {
|
|
294
|
-
$entity: 'operEntity',
|
|
295
|
-
data: {
|
|
296
|
-
entityId: 1,
|
|
297
|
-
operId: 1,
|
|
298
|
-
entity: 1,
|
|
299
|
-
id: 1,
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
},
|
|
303
|
-
filter: {
|
|
304
|
-
id: { $in: ids },
|
|
305
|
-
},
|
|
306
|
-
}, { dontCollect: true, forUpdate: true });
|
|
307
|
-
dirtyOpers = dirtyOpers.filter(ele => !!ele[types_1.TriggerDataAttribute]);
|
|
308
|
-
if (dirtyOpers.length > 0) {
|
|
309
|
-
for (const c in this.channelDict) {
|
|
310
|
-
(0, assert_1.default)(this.channelDict[c].queue.length === 0);
|
|
311
|
-
}
|
|
312
|
-
const pushedIds = [];
|
|
313
|
-
const unpushedIds = [];
|
|
314
|
-
await Promise.all(dirtyOpers.map(async (oper) => {
|
|
315
|
-
const result = await this.dispatchOperToChannels(oper, context);
|
|
316
|
-
if (result) {
|
|
317
|
-
pushedIds.push(oper.id);
|
|
318
|
-
}
|
|
319
|
-
else {
|
|
320
|
-
unpushedIds.push(oper.id);
|
|
321
|
-
}
|
|
322
|
-
}));
|
|
323
|
-
if (unpushedIds.length > 0) {
|
|
324
|
-
await context.operate('oper', {
|
|
325
|
-
id: await (0, uuid_1.generateNewIdAsync)(),
|
|
326
|
-
action: 'update',
|
|
327
|
-
data: {
|
|
328
|
-
[types_1.TriggerDataAttribute]: null,
|
|
329
|
-
[types_1.TriggerUuidAttribute]: null,
|
|
330
|
-
},
|
|
331
|
-
filter: {
|
|
332
|
-
id: {
|
|
333
|
-
$in: unpushedIds,
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
}, {});
|
|
257
|
+
for (const c in this.channelDict) {
|
|
258
|
+
(0, assert_1.default)(this.channelDict[c].queue.length === 0);
|
|
259
|
+
}
|
|
260
|
+
const pushedIds = [];
|
|
261
|
+
const unpushedIds = [];
|
|
262
|
+
await Promise.all(dirtyOpers.map(async (oper) => {
|
|
263
|
+
const result = await this.dispatchOperToChannels(oper, context);
|
|
264
|
+
if (result) {
|
|
265
|
+
pushedIds.push(oper.id);
|
|
337
266
|
}
|
|
338
|
-
|
|
339
|
-
|
|
267
|
+
else {
|
|
268
|
+
unpushedIds.push(oper.id);
|
|
340
269
|
}
|
|
270
|
+
}));
|
|
271
|
+
if (unpushedIds.length > 0) {
|
|
272
|
+
await context.operate('oper', {
|
|
273
|
+
id: await (0, uuid_1.generateNewIdAsync)(),
|
|
274
|
+
action: 'update',
|
|
275
|
+
data: {
|
|
276
|
+
[types_1.TriggerDataAttribute]: null,
|
|
277
|
+
[types_1.TriggerUuidAttribute]: null,
|
|
278
|
+
},
|
|
279
|
+
filter: {
|
|
280
|
+
id: {
|
|
281
|
+
$in: unpushedIds,
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}, {});
|
|
285
|
+
}
|
|
286
|
+
if (pushedIds.length > 0) {
|
|
287
|
+
result = await this.startAllChannel(context);
|
|
341
288
|
}
|
|
342
289
|
}
|
|
343
|
-
await context.commit();
|
|
344
290
|
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
291
|
+
if (result) {
|
|
292
|
+
const exception = result.find(ele => ele instanceof Error);
|
|
293
|
+
if (exception) {
|
|
294
|
+
throw exception;
|
|
295
|
+
}
|
|
349
296
|
}
|
|
350
297
|
}
|
|
351
298
|
makeCreateOperTrigger() {
|
|
@@ -445,17 +392,16 @@ class Synchronizer {
|
|
|
445
392
|
action: 'create',
|
|
446
393
|
when: 'commit',
|
|
447
394
|
strict: 'makeSure',
|
|
395
|
+
singleton: true,
|
|
396
|
+
grouped: true,
|
|
448
397
|
check: (operation) => {
|
|
449
398
|
const { data } = operation;
|
|
450
399
|
const { targetEntity, action } = data;
|
|
451
400
|
return pushEntities.includes(data.targetEntity)
|
|
452
401
|
&& !!this.pushAccessMap[targetEntity].find(({ actions }) => !actions || actions.includes(action));
|
|
453
402
|
},
|
|
454
|
-
fn: async ({ ids }) => {
|
|
455
|
-
|
|
456
|
-
this.trySynchronizeOpers();
|
|
457
|
-
// 内部自主处理triggerData,因此不需要让triggerExecutor处理
|
|
458
|
-
throw new types_1.OakMakeSureByMySelfException();
|
|
403
|
+
fn: async ({ ids }, context) => {
|
|
404
|
+
return this.trySynchronizeOpers(context);
|
|
459
405
|
}
|
|
460
406
|
};
|
|
461
407
|
return createOperTrigger;
|
|
@@ -472,15 +418,6 @@ class Synchronizer {
|
|
|
472
418
|
getSyncTriggers() {
|
|
473
419
|
return [this.makeCreateOperTrigger()];
|
|
474
420
|
}
|
|
475
|
-
getSyncRoutine() {
|
|
476
|
-
return {
|
|
477
|
-
name: 'checkpoint routine for sync',
|
|
478
|
-
routine: async () => {
|
|
479
|
-
this.trySynchronizeOpers();
|
|
480
|
-
return {};
|
|
481
|
-
},
|
|
482
|
-
};
|
|
483
|
-
}
|
|
484
421
|
getSelfEndpoint() {
|
|
485
422
|
return {
|
|
486
423
|
name: this.config.self.endpoint || 'sync',
|
|
@@ -493,9 +430,6 @@ class Synchronizer {
|
|
|
493
430
|
if (process.env.NODE_ENV === 'development') {
|
|
494
431
|
console.log('接收到来自远端的sync数据', entity, JSON.stringify(body));
|
|
495
432
|
}
|
|
496
|
-
const successIds = [];
|
|
497
|
-
const redundantIds = [];
|
|
498
|
-
let failed;
|
|
499
433
|
// todo 这里先缓存,不考虑本身同步相关信息的更新
|
|
500
434
|
if (!this.remotePullInfoMap[entity]) {
|
|
501
435
|
this.remotePullInfoMap[entity] = {};
|
|
@@ -544,75 +478,58 @@ class Synchronizer {
|
|
|
544
478
|
(0, assert_1.default)(oper.$$seq$$ > maxStaleSeq, '发现了seq没有按序进行同步');
|
|
545
479
|
}
|
|
546
480
|
}
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
id: {
|
|
558
|
-
$in: ids,
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
}, { dontCollect: true });
|
|
562
|
-
if (opersExisted.length < ids.length) {
|
|
563
|
-
const missed = (0, lodash_1.difference)(ids, opersExisted.map(ele => ele.id));
|
|
564
|
-
// todo 这里如果远端业务逻辑严格,发生乱序应是无关的oper,直接执行就好 by Xc
|
|
565
|
-
throw new Error(`在sync过程中发现有丢失的oper数据「${missed}」`);
|
|
481
|
+
// 检查已经应用过的opers是否完整
|
|
482
|
+
const staleIds = staleOpers.map(ele => ele.id);
|
|
483
|
+
if (staleIds.length > 0) {
|
|
484
|
+
const opersExisted = await context.select('oper', {
|
|
485
|
+
data: {
|
|
486
|
+
id: 1,
|
|
487
|
+
},
|
|
488
|
+
filter: {
|
|
489
|
+
id: {
|
|
490
|
+
$in: staleIds,
|
|
566
491
|
}
|
|
567
|
-
redundantIds.push(...ids);
|
|
568
492
|
}
|
|
569
|
-
})
|
|
570
|
-
(
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
action,
|
|
587
|
-
filter: {
|
|
588
|
-
id: ids.length === 1 ? ids[0] : {
|
|
589
|
-
$in: ids,
|
|
590
|
-
},
|
|
591
|
-
},
|
|
592
|
-
bornAt: $$seq$$,
|
|
593
|
-
};
|
|
594
|
-
await context.operate(targetEntity, operation, {});
|
|
595
|
-
successIds.push(id);
|
|
596
|
-
}
|
|
597
|
-
catch (err) {
|
|
598
|
-
console.error(err);
|
|
599
|
-
console.error('sync时出错', entity, JSON.stringify(freshOper));
|
|
600
|
-
failed = {
|
|
601
|
-
id,
|
|
602
|
-
error: err.toString(),
|
|
603
|
-
};
|
|
604
|
-
break;
|
|
605
|
-
}
|
|
493
|
+
}, { dontCollect: true });
|
|
494
|
+
if (opersExisted.length < staleIds.length) {
|
|
495
|
+
const missed = (0, lodash_1.difference)(staleIds, opersExisted.map(ele => ele.id));
|
|
496
|
+
// todo 这里如果远端业务逻辑严格,发生乱序应是无关的oper,直接执行就好 by Xc
|
|
497
|
+
throw new Error(`在sync过程中发现有丢失的oper数据「${missed}」`);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
// 应用所有的freshOpers,失败则报错
|
|
501
|
+
for (const freshOper of freshOpers) {
|
|
502
|
+
// freshOpers是按$$seq$$序产生的
|
|
503
|
+
const { id, targetEntity, action, data, $$seq$$, filter } = freshOper;
|
|
504
|
+
const ids = (0, filter_1.getRelevantIds)(filter);
|
|
505
|
+
(0, assert_1.default)(ids.length > 0);
|
|
506
|
+
if (pullEntityDict && pullEntityDict[targetEntity]) {
|
|
507
|
+
const { process } = pullEntityDict[targetEntity];
|
|
508
|
+
if (process) {
|
|
509
|
+
await process(action, data, context);
|
|
606
510
|
}
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
511
|
+
}
|
|
512
|
+
const operation = {
|
|
513
|
+
id,
|
|
514
|
+
data,
|
|
515
|
+
action,
|
|
516
|
+
filter: {
|
|
517
|
+
id: ids.length === 1 ? ids[0] : {
|
|
518
|
+
$in: ids,
|
|
519
|
+
},
|
|
520
|
+
},
|
|
521
|
+
bornAt: $$seq$$,
|
|
522
|
+
};
|
|
523
|
+
await context.operate(targetEntity, operation, {});
|
|
524
|
+
}
|
|
525
|
+
if (process.env.NODE_ENV === 'development') {
|
|
526
|
+
console.log(`同步成功,其中重复的oper ${staleIds.length}个,新的oper ${freshOpers.length}个。`);
|
|
527
|
+
}
|
|
528
|
+
return {};
|
|
614
529
|
}
|
|
615
530
|
};
|
|
616
531
|
}
|
|
532
|
+
tryCreateSyncProcess() {
|
|
533
|
+
}
|
|
617
534
|
}
|
|
618
535
|
exports.default = Synchronizer;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EntityDict,
|
|
1
|
+
import { EntityDict, OpRecord } from 'oak-domain/lib/types';
|
|
2
2
|
import { EntityDict as BaseEntityDict } from 'oak-domain/lib/base-app-domain';
|
|
3
3
|
import { BackendRuntimeContext } from 'oak-frontend-base/lib/context/BackendRuntimeContext';
|
|
4
4
|
import { Namespace } from 'socket.io';
|
|
@@ -18,5 +18,5 @@ export default class DataSubscriber<ED extends EntityDict & BaseEntityDict, Cont
|
|
|
18
18
|
*/
|
|
19
19
|
private startup;
|
|
20
20
|
publishEvent(event: string, records: OpRecord<ED>[], sid?: string): void;
|
|
21
|
-
|
|
21
|
+
publishServerEvent(identifier: string, event: string, ...rest: any[]): void;
|
|
22
22
|
}
|
|
@@ -71,11 +71,9 @@ class DataSubscriber {
|
|
|
71
71
|
this.ns.to(event).emit('data', records, event);
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
|
-
|
|
75
|
-
const { instanceId } = (0, env_1.getClusterInfo)();
|
|
76
|
-
// console.log('publishVolatileTrigger', instanceId, instanceNumber);
|
|
74
|
+
publishServerEvent(identifier, event, ...rest) {
|
|
77
75
|
(0, console_1.assert)(this.nsServer);
|
|
78
|
-
this.nsServer.to(
|
|
76
|
+
this.nsServer.to(identifier).emit(event, ...rest);
|
|
79
77
|
}
|
|
80
78
|
}
|
|
81
79
|
exports.default = DataSubscriber;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oak-backend-base",
|
|
3
|
-
"version": "4.1.
|
|
3
|
+
"version": "4.1.4",
|
|
4
4
|
"description": "oak-backend-base",
|
|
5
5
|
"main": "lib/index",
|
|
6
6
|
"author": {
|
|
@@ -20,10 +20,10 @@
|
|
|
20
20
|
"mysql": "^2.18.1",
|
|
21
21
|
"mysql2": "^2.3.3",
|
|
22
22
|
"node-schedule": "^2.1.0",
|
|
23
|
-
"oak-common-aspect": "
|
|
24
|
-
"oak-db": "
|
|
25
|
-
"oak-domain": "
|
|
26
|
-
"oak-frontend-base": "
|
|
23
|
+
"oak-common-aspect": "^3.0.1",
|
|
24
|
+
"oak-db": "^3.3.0",
|
|
25
|
+
"oak-domain": "^5.1.0",
|
|
26
|
+
"oak-frontend-base": "^5.3.6",
|
|
27
27
|
"socket.io": "^4.7.2",
|
|
28
28
|
"socket.io-client": "^4.7.2",
|
|
29
29
|
"uuid": "^8.3.2"
|