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.
@@ -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
- await context.rollback();
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
- console.log(`执行watcher【${watcher.name}】成功,耗时【${Date.now() - start}】,结果是:`, result);
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.dbStore.checkpoint(process.env.NODE_ENV === 'development' ? now - 30 * 1000 : now - 120 * 1000);
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 { timer: timerFn } = timer;
372
- const result = await timerFn(context);
373
- console.log(`定时器【${name}】执行成功,耗时${Date.now() - start}毫秒,结果是【${result}】`);
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
- await context.rollback();
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
- private connect;
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
  }
@@ -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
- connect() {
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('disconnect', () => {
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] = 1;
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
- await context.rollback();
66
- if (!(err instanceof types_1.OakMakeSureByMySelfException)) {
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
- this.dataSubscriber.publishVolatileTrigger(entity, trigger.name, seqMod, ids2, cxtStr, option);
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.connect();
106
- this.csTriggers = {};
121
+ this.connectServerSocket();
107
122
  }
108
123
  registerTrigger(trigger) {
109
124
  // 如果是cluster sensative的trigger,注册到socket事件上
110
- if (trigger.when === 'commit' && trigger.cs) {
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;
@@ -1,4 +1,4 @@
1
- import { EntityDict, StorageSchema, EndpointItem, SyncConfig, FreeRoutine } from 'oak-domain/lib/types';
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
- * 向某一个远端对象push opers。根据幂等性,这里如果失败了必须反复推送
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
  }
@@ -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
- let json;
29
- try {
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('同步oper返回结果', JSON.stringify(json), 'txnId:', context.getCurrentTxnId());
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 { successIds, failed, redundantIds } = json;
73
- if (failed) {
74
- const { id, error } = failed;
75
- console.error('同步过程中发生异常', id, error, retry);
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
- const unsuccessfulOpers = queue.filter(ele => !successIds.includes(ele.oper.id) && !redundantIds.includes(ele.oper.id));
78
- // 重新开始前,可以将已经完成的oper的triggerData位清零。要注意,在多个remote配置下,有可能一个oper要推给多个channel
79
- // 这里可能设计过度了,代码也没经过测试
80
- channel.queue = unsuccessfulOpers;
81
- const aliveOperIds = [];
82
- for (const k in this.channelDict) {
83
- if (this.channelDict[k].queue.length > 0) {
84
- aliveOperIds.push(...this.channelDict[k].queue.map(ele => ele.oper.id));
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
- const overIds = (0, lodash_1.difference)(successIds.concat(redundantIds), aliveOperIds);
88
- if (overIds.length > 0) {
89
- await context.operate('oper', {
90
- id: await (0, uuid_1.generateNewIdAsync)(),
91
- action: 'update',
92
- data: {
93
- [types_1.TriggerDataAttribute]: null,
94
- [types_1.TriggerUuidAttribute]: null,
95
- },
96
- filter: {
97
- id: {
98
- $in: overIds,
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
- return this.startChannel(context, channel, 0);
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
- const context = this.contextBuilder();
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
- try {
268
- let dirtyOpers = await context.select('oper', {
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
- [types_1.TriggerDataAttribute]: {
274
- $exists: true,
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
- // 这一步是加锁,保证只有一个进程完成推送,推送者提交前会将$$triggerData$$清零
280
- const ids = dirtyOpers.map(ele => ele.id);
281
- dirtyOpers = await context.select('oper', {
282
- data: {
283
- id: 1,
284
- action: 1,
285
- data: 1,
286
- targetEntity: 1,
287
- operatorId: 1,
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
- if (pushedIds.length > 0) {
339
- await this.startAllChannel(context);
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
- catch (err) {
346
- await context.rollback();
347
- console.error(err);
348
- throw err;
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
- (0, assert_1.default)(ids.length === 1);
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
- await Promise.all([
548
- // 无法严格保证推送按bornAt,所以一旦还有outdatedOpers,检查其已经被apply
549
- (async () => {
550
- const ids = staleOpers.map(ele => ele.id);
551
- if (ids.length > 0) {
552
- const opersExisted = await context.select('oper', {
553
- data: {
554
- id: 1,
555
- },
556
- filter: {
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
- (async () => {
571
- for (const freshOper of freshOpers) {
572
- // freshOpers是按$$seq$$序产生的
573
- const { id, targetEntity, action, data, $$seq$$, filter } = freshOper;
574
- const ids = (0, filter_1.getRelevantIds)(filter);
575
- (0, assert_1.default)(ids.length > 0);
576
- try {
577
- if (pullEntityDict && pullEntityDict[targetEntity]) {
578
- const { process } = pullEntityDict[targetEntity];
579
- if (process) {
580
- await process(action, data, context);
581
- }
582
- }
583
- const operation = {
584
- id,
585
- data,
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
- return {
610
- successIds,
611
- failed,
612
- redundantIds,
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, OperateOption, OpRecord } from 'oak-domain/lib/types';
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
- publishVolatileTrigger(entity: keyof ED, name: string, instanceNumber: string, ids: string[], cxtStr: string, option: OperateOption): void;
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
- publishVolatileTrigger(entity, name, instanceNumber, ids, cxtStr, option) {
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(`${name}-${instanceNumber}`).emit('data', entity, name, ids, cxtStr, option);
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.2",
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": "~3.0.0",
24
- "oak-db": "~3.3.0",
25
- "oak-domain": "~5.0.16",
26
- "oak-frontend-base": "~5.3.0",
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"