oak-backend-base 3.4.0 → 3.4.2

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.
@@ -12,7 +12,7 @@ export declare class AppLoader<ED extends EntityDict & BaseEntityDict, Cxt exten
12
12
  private aspectDict;
13
13
  private externalDependencies;
14
14
  protected dataSubscriber?: DataSubscriber<ED, Cxt>;
15
- protected synchronizers?: Synchronizer<ED, Cxt>[];
15
+ protected synchronizer?: Synchronizer<ED, Cxt>;
16
16
  protected contextBuilder: (scene?: string) => (store: DbStore<ED, Cxt>) => Promise<Cxt>;
17
17
  private requireSth;
18
18
  protected makeContext(cxtStr?: string, headers?: IncomingHttpHeaders): Promise<Cxt>;
package/lib/AppLoader.js CHANGED
@@ -21,7 +21,7 @@ class AppLoader extends types_1.AppLoader {
21
21
  aspectDict;
22
22
  externalDependencies;
23
23
  dataSubscriber;
24
- synchronizers;
24
+ synchronizer;
25
25
  contextBuilder;
26
26
  requireSth(filePath) {
27
27
  const depFilePath = (0, path_1.join)(this.path, filePath);
@@ -104,7 +104,7 @@ class AppLoader extends types_1.AppLoader {
104
104
  const syncConfigs = (0, fs_1.existsSync)(syncConfigFile) && require(syncConfigFile).default;
105
105
  return {
106
106
  dbConfig: dbConfig,
107
- syncConfigs: syncConfigs,
107
+ syncConfig: syncConfigs,
108
108
  };
109
109
  }
110
110
  constructor(path, contextBuilder, ns, nsServer) {
@@ -149,20 +149,18 @@ class AppLoader extends types_1.AppLoader {
149
149
  adTriggers.forEach((trigger) => this.registerTrigger(trigger));
150
150
  checkers.forEach((checker) => this.dbStore.registerChecker(checker));
151
151
  adCheckers.forEach((checker) => this.dbStore.registerChecker(checker));
152
- if (this.synchronizers) {
153
- // 同步数据到远端结点通过commit trigger来完成
154
- for (const synchronizer of this.synchronizers) {
155
- const syncTriggers = synchronizer.getSyncTriggers();
156
- syncTriggers.forEach((trigger) => this.registerTrigger(trigger));
157
- }
152
+ if (this.synchronizer) {
153
+ // 同步数据到远端结点通过commit trigger来完成
154
+ const syncTriggers = this.synchronizer.getSyncTriggers();
155
+ syncTriggers.forEach((trigger) => this.registerTrigger(trigger));
158
156
  }
159
157
  }
160
158
  async mount(initialize) {
161
159
  const { path } = this;
162
160
  if (!initialize) {
163
- const { syncConfigs } = this.getConfiguration();
164
- if (syncConfigs) {
165
- this.synchronizers = syncConfigs.map(config => new Synchronizer_1.default(config, this.dbStore.getSchema()));
161
+ const { syncConfig: syncConfig } = this.getConfiguration();
162
+ if (syncConfig) {
163
+ this.synchronizer = new Synchronizer_1.default(syncConfig, this.dbStore.getSchema(), () => this.contextBuilder()(this.dbStore));
166
164
  }
167
165
  this.initTriggers();
168
166
  }
@@ -275,11 +273,9 @@ class AppLoader extends types_1.AppLoader {
275
273
  transformEndpointItem(router, item);
276
274
  }
277
275
  }
278
- if (this.synchronizers) {
279
- this.synchronizers.forEach((synchronizer) => {
280
- const syncEp = synchronizer.getSelfEndpoint();
281
- transformEndpointItem(syncEp.name, syncEp);
282
- });
276
+ if (this.synchronizer) {
277
+ const syncEp = this.synchronizer.getSelfEndpoint();
278
+ transformEndpointItem(syncEp.name, syncEp);
283
279
  }
284
280
  return endPointRouters;
285
281
  }
@@ -399,6 +395,10 @@ class AppLoader extends types_1.AppLoader {
399
395
  }
400
396
  async execStartRoutines() {
401
397
  const routines = this.requireSth('lib/routines/start');
398
+ if (this.synchronizer) {
399
+ const routine = this.synchronizer.getSyncRoutine();
400
+ routines.push(routine);
401
+ }
402
402
  for (const routine of routines) {
403
403
  if (routine.hasOwnProperty('entity')) {
404
404
  const start = Date.now();
@@ -428,13 +428,6 @@ class AppLoader extends types_1.AppLoader {
428
428
  }
429
429
  }
430
430
  }
431
- if (this.synchronizers) {
432
- this.synchronizers.forEach((synchronizer) => {
433
- // 这个routine在内部处理异步
434
- const routine = synchronizer.getSyncRoutine();
435
- this.execWatcher(routine);
436
- });
437
- }
438
431
  }
439
432
  async execRoutine(routine) {
440
433
  const context = await this.makeContext();
@@ -1,4 +1,4 @@
1
- import { EntityDict, StorageSchema, EndpointItem, SyncConfig, Watcher } from 'oak-domain/lib/types';
1
+ import { EntityDict, StorageSchema, EndpointItem, SyncConfig, FreeRoutine } 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';
@@ -7,7 +7,8 @@ export default class Synchronizer<ED extends EntityDict & BaseEntityDict, Cxt ex
7
7
  private schema;
8
8
  private remotePullInfoMap;
9
9
  private pullMaxBornAtMap;
10
- private remotePushChannel;
10
+ private channelDict;
11
+ private contextBuilder;
11
12
  private pushAccessMap;
12
13
  /**
13
14
  * 向某一个远端对象push opers。根据幂等性,这里如果失败了必须反复推送
@@ -15,32 +16,23 @@ export default class Synchronizer<ED extends EntityDict & BaseEntityDict, Cxt ex
15
16
  * @param retry
16
17
  */
17
18
  private startChannel;
18
- private joinChannel;
19
+ private startAllChannel;
20
+ private pushOperToChannel;
21
+ private refineOperData;
22
+ private dispatchOperToChannels;
19
23
  /**
20
- * 推向远端Node的oper,需要严格保证按产生的时间序推送。根据幂等原理,这里必须要推送成功
21
- * 因此在这里要实现两点:
22
- * 1)oper如果推送失败了,必须留存在queue中,以保证在后面产生的oper之前推送
23
- * 2)当对queue中增加oper时,要检查是否有重(有重说明之前失败过),如果无重则将之放置在队列尾
24
- *
25
- * 其实这里还无法严格保证先产生的oper一定先到达被推送,因为volatile trigger是在事务提交后再发生的,但这种情况在目前应该跑不出来,在实际执行oper的时候assert掉先。by Xc 20240226
26
- */
27
- private pushOper;
28
- /**
29
- * 因为应用可能是多租户,得提前确定context下的selfEncryptInfo
30
- * 由于checkpoint时无法区别不同上下文之间的未完成oper数据,所以接口只能这样设计
31
- * @param id
24
+ * 为了保证推送的oper序,采用从database中顺序读取所有需要推送的oper来进行推送
25
+ * 每个进程都保证把当前所有的oper顺序处理掉,就不会有乱序的问题,大家通过database上的锁来完成同步
32
26
  * @param context
33
- * @param selfEncryptInfo
34
- * @returns
35
27
  */
36
- private synchronizeOpersToRemote;
28
+ private trySynchronizeOpers;
37
29
  private makeCreateOperTrigger;
38
- constructor(config: SyncConfig<ED, Cxt>, schema: StorageSchema<ED>);
30
+ constructor(config: SyncConfig<ED, Cxt>, schema: StorageSchema<ED>, contextBuilder: () => Promise<Cxt>);
39
31
  /**
40
32
  * 根据sync的定义,生成对应的 commit triggers
41
33
  * @returns
42
34
  */
43
35
  getSyncTriggers(): VolatileTrigger<ED, keyof ED, Cxt>[];
44
- getSyncRoutine(): Watcher<ED, keyof ED, Cxt>;
36
+ getSyncRoutine(): FreeRoutine<ED, Cxt>;
45
37
  getSelfEndpoint(): EndpointItem<ED, Cxt>;
46
38
  }
@@ -8,6 +8,7 @@ const path_1 = require("path");
8
8
  const lodash_1 = require("oak-domain/lib/utils/lodash");
9
9
  const filter_1 = require("oak-domain/lib/store/filter");
10
10
  const uuid_1 = require("oak-domain/lib/utils/uuid");
11
+ const lodash_2 = require("lodash");
11
12
  const OAK_SYNC_HEADER_ENTITY = 'oak-sync-entity';
12
13
  const OAK_SYNC_HEADER_ENTITYID = 'oak-sync-entity-id';
13
14
  class Synchronizer {
@@ -15,24 +16,21 @@ class Synchronizer {
15
16
  schema;
16
17
  remotePullInfoMap = {};
17
18
  pullMaxBornAtMap = {};
18
- remotePushChannel = {};
19
+ channelDict = {};
20
+ contextBuilder;
19
21
  pushAccessMap = {};
20
22
  /**
21
23
  * 向某一个远端对象push opers。根据幂等性,这里如果失败了必须反复推送
22
24
  * @param channel
23
25
  * @param retry
24
26
  */
25
- async startChannel(channel, retry) {
27
+ async startChannel(context, channel, retry) {
26
28
  const { queue, api, selfEncryptInfo, entity, entityId } = channel;
27
- channel.queue = [];
28
- channel.running = true;
29
- channel.nextPushTimestamp = Number.MAX_SAFE_INTEGER;
30
- const opers = queue.map(ele => ele.oper);
31
- let failedOpers = [];
32
- let needRetry = false;
33
29
  let json;
34
30
  try {
35
31
  // todo 加密
32
+ const queue = channel.queue;
33
+ const opers = queue.map(ele => ele.oper);
36
34
  console.log('向远端结点sync数据', api, JSON.stringify(opers));
37
35
  const finalApi = (0, path_1.join)(api, selfEncryptInfo.id);
38
36
  const res = await fetch(finalApi, {
@@ -50,156 +48,143 @@ class Synchronizer {
50
48
  json = await res.json();
51
49
  }
52
50
  catch (err) {
51
+ // 最大延迟redo时间512秒
52
+ const retryDelay = Math.pow(2, Math.min(9, retry)) * 1000;
53
53
  console.error('sync push时出现error', err);
54
- needRetry = true;
55
- failedOpers = queue;
54
+ console.error(`将于${retryDelay}毫秒后重试`);
55
+ return new Promise((resolve) => {
56
+ setTimeout(async () => {
57
+ await this.startChannel(context, channel, retry + 1);
58
+ resolve(undefined);
59
+ }, retryDelay);
60
+ });
56
61
  }
57
- if (!needRetry) {
58
- /**
59
- * 返回结构见this.getSelfEndpoint
60
- */
61
- const { successIds, failed } = json;
62
- if (failed) {
63
- needRetry = true;
64
- const { id, error } = failed;
65
- console.error('同步过程中发生异常', id, error);
66
- }
67
- for (const req of queue) {
68
- if (successIds.includes(req.oper.id)) {
69
- req.resolve(undefined);
70
- }
71
- else {
72
- failedOpers.push(req);
73
- }
74
- }
62
+ /**
63
+ * 返回结构见this.getSelfEndpoint
64
+ */
65
+ const { successIds, failed, redundantIds } = json;
66
+ if (failed) {
67
+ const { id, error } = failed;
68
+ console.error('同步过程中发生异常', id, error, retry);
75
69
  }
76
- channel.running = false;
77
- channel.handler = undefined;
78
- const retry2 = retry + 1;
79
- console.log('need retry', retry2);
80
- this.joinChannel(channel, failedOpers, retry2);
81
- }
82
- joinChannel(channel, opers, retry) {
83
- // 要去重且有序
84
- let idx = 0;
85
- const now = Date.now();
86
- opers.forEach((oper) => {
87
- for (; idx < channel.queue.length; idx++) {
88
- if (channel.queue[idx].oper.id === oper.oper.id) {
89
- (0, assert_1.default)(false, '不应当出现重复的oper');
90
- break;
91
- }
92
- else if (channel.queue[idx].oper.bornAt > oper.oper.bornAt) {
93
- break;
94
- }
95
- }
96
- channel.queue.splice(idx, 0, oper);
97
- });
98
- const retryWeight = Math.pow(2, Math.min(retry, 10));
99
- const nextPushTimestamp = retryWeight * 1000 + now;
100
- if (channel.queue.length > 0) {
101
- if (channel.running) {
102
- if (channel.nextPushTimestamp > nextPushTimestamp) {
103
- channel.nextPushTimestamp = nextPushTimestamp;
104
- }
70
+ const unsuccessfulOpers = queue.filter(ele => !successIds.includes(ele.oper.id) && !redundantIds.includes(ele.oper.id));
71
+ // 重新开始前,可以将已经完成的oper的triggerData位清零。要注意,在多个remote配置下,有可能一个oper要推给多个channel
72
+ // 这里可能设计过度了,代码也没经过测试
73
+ channel.queue = unsuccessfulOpers;
74
+ const aliveOperIds = [];
75
+ for (const k in this.channelDict) {
76
+ if (this.channelDict[k].queue.length > 0) {
77
+ aliveOperIds.push(...this.channelDict[k].queue.map(ele => ele.oper.id));
105
78
  }
106
- else {
107
- if (channel.nextPushTimestamp > nextPushTimestamp) {
108
- channel.nextPushTimestamp = nextPushTimestamp;
109
- if (channel.handler) {
110
- clearTimeout(channel.handler);
79
+ }
80
+ const overIds = (0, lodash_1.difference)(successIds.concat(redundantIds), aliveOperIds);
81
+ if (overIds.length > 0) {
82
+ await context.operate('oper', {
83
+ id: await (0, uuid_1.generateNewIdAsync)(),
84
+ action: 'update',
85
+ data: {
86
+ [types_1.TriggerDataAttribute]: null,
87
+ [types_1.TriggerUuidAttribute]: null,
88
+ },
89
+ filter: {
90
+ id: {
91
+ $in: overIds,
111
92
  }
112
- channel.handler = setTimeout(async () => {
113
- await this.startChannel(channel, retry);
114
- }, nextPushTimestamp - now);
115
- }
116
- else {
117
- // 当前队列的开始时间要早于自身要求,不用管
118
- (0, assert_1.default)(channel.handler);
119
93
  }
94
+ }, {});
95
+ }
96
+ if (successIds.length > 0) {
97
+ try {
98
+ await Promise.all(successIds.map((id) => {
99
+ const { onSynchronized, oper } = queue.find(ele => ele.oper.id === id);
100
+ return onSynchronized && onSynchronized({
101
+ action: oper.action,
102
+ data: oper.data,
103
+ rowIds: (0, filter_1.getRelevantIds)(oper.filter),
104
+ }, context);
105
+ }));
106
+ }
107
+ catch (err) {
108
+ // 这时候无法处理?
109
+ console.error('onSynchronzied时出错', err);
110
+ (0, assert_1.default)(false);
120
111
  }
121
112
  }
122
- else {
123
- channel.handler = undefined;
124
- channel.nextPushTimestamp = Number.MAX_SAFE_INTEGER;
113
+ if (channel.queue.length > 0) {
114
+ // 最大延迟redo时间512秒
115
+ const retryDelay = Math.pow(2, Math.min(9, retry)) * 1000;
116
+ console.error(`有${channel.queue.length}个oper同步失败,将于${retryDelay}毫秒后重试`);
117
+ return new Promise((resolve) => {
118
+ setTimeout(async () => {
119
+ await this.startChannel(context, channel, retry + 1);
120
+ resolve(undefined);
121
+ }, retryDelay);
122
+ });
125
123
  }
126
124
  }
127
- // 将产生的oper推送到远端Node。注意要尽量在本地阻止重复推送
128
- /**
129
- * 推向远端Node的oper,需要严格保证按产生的时间序推送。根据幂等原理,这里必须要推送成功
130
- * 因此在这里要实现两点:
131
- * 1)oper如果推送失败了,必须留存在queue中,以保证在后面产生的oper之前推送
132
- * 2)当对queue中增加oper时,要检查是否有重(有重说明之前失败过),如果无重则将之放置在队列尾
133
- *
134
- * 其实这里还无法严格保证先产生的oper一定先到达被推送,因为volatile trigger是在事务提交后再发生的,但这种情况在目前应该跑不出来,在实际执行oper的时候assert掉先。by Xc 20240226
135
- */
136
- async pushOper(oper, userId, url, endpoint, remoteEntity, remoteEntityId, selfEncryptInfo) {
137
- if (!this.remotePushChannel[userId]) {
125
+ async startAllChannel(context) {
126
+ await Promise.all(Object.keys(this.channelDict).map(async (k) => {
127
+ const channel = this.channelDict[k];
128
+ if (channel.queue.length > 0) {
129
+ channel.queue.sort((o1, o2) => o1.oper.$$seq$$ - o2.oper.$$seq$$);
130
+ return this.startChannel(context, channel, 0);
131
+ }
132
+ }));
133
+ }
134
+ pushOperToChannel(oper, userId, url, endpoint, remoteEntity, remoteEntityId, selfEncryptInfo, onSynchronized) {
135
+ if (!this.channelDict[userId]) {
138
136
  // channel上缓存这些信息,暂不支持动态更新
139
- this.remotePushChannel[userId] = {
137
+ this.channelDict[userId] = {
140
138
  api: (0, path_1.join)(url, 'endpoint', endpoint),
141
139
  queue: [],
142
140
  entity: remoteEntity,
143
141
  entityId: remoteEntityId,
144
- nextPushTimestamp: Number.MAX_SAFE_INTEGER,
145
- running: false,
146
142
  selfEncryptInfo,
147
143
  };
148
144
  }
149
- const channel = this.remotePushChannel[userId];
145
+ else {
146
+ // 趁机更新一下加密信息
147
+ this.channelDict[userId].selfEncryptInfo = selfEncryptInfo;
148
+ }
149
+ const channel = this.channelDict[userId];
150
150
  (0, assert_1.default)(channel.api === (0, path_1.join)(url, 'endpoint', endpoint));
151
151
  (0, assert_1.default)(channel.entity === remoteEntity);
152
152
  (0, assert_1.default)(channel.entityId === remoteEntityId);
153
- const promise = new Promise((resolve) => {
154
- this.joinChannel(channel, [{
155
- oper,
156
- resolve,
157
- }], 0);
153
+ channel.queue.push({
154
+ oper,
155
+ onSynchronized,
158
156
  });
159
- await promise;
160
157
  }
161
- /**
162
- * 因为应用可能是多租户,得提前确定context下的selfEncryptInfo
163
- * 由于checkpoint时无法区别不同上下文之间的未完成oper数据,所以接口只能这样设计
164
- * @param id
165
- * @param context
166
- * @param selfEncryptInfo
167
- * @returns
168
- */
169
- async synchronizeOpersToRemote(id, context, selfEncryptInfo) {
170
- const [oper] = await context.select('oper', {
171
- data: {
172
- id: 1,
173
- action: 1,
174
- data: 1,
175
- targetEntity: 1,
176
- operatorId: 1,
177
- operEntity$oper: {
178
- $entity: 'operEntity',
179
- data: {
180
- id: 1,
181
- entity: 1,
182
- entityId: 1,
183
- },
184
- },
185
- bornAt: 1,
186
- $$createAt$$: 1,
187
- filter: 1,
188
- },
189
- filter: {
190
- id,
191
- }
192
- }, { dontCollect: true, forUpdate: true });
193
- const { operatorId, targetEntity, operEntity$oper: operEntities, action, data } = oper;
194
- const entityIds = operEntities.map(ele => ele.entityId);
158
+ refineOperData(oper, rowIds) {
159
+ const { action, id, targetEntity, data, $$seq$$, filter } = oper;
160
+ const data2 = (action === 'create' && data instanceof Array) ? data.filter(ele => rowIds.includes(ele.id)) : data;
161
+ // 过滤掉数据中的跨事务trigger信息
162
+ if (data2 instanceof Array) {
163
+ data2.forEach((d) => {
164
+ (0, lodash_2.unset)(d, types_1.TriggerDataAttribute);
165
+ (0, lodash_2.unset)(d, types_1.TriggerUuidAttribute);
166
+ });
167
+ }
168
+ else {
169
+ (0, lodash_2.unset)(data2, types_1.TriggerDataAttribute);
170
+ (0, lodash_2.unset)(data2, types_1.TriggerUuidAttribute);
171
+ }
172
+ return {
173
+ id, action, targetEntity, data: data2, $$seq$$, filter,
174
+ };
175
+ }
176
+ async dispatchOperToChannels(oper, context) {
177
+ const { operatorId, targetEntity, filter, action, data } = oper;
178
+ const entityIds = (0, filter_1.getRelevantIds)(filter);
179
+ (0, assert_1.default)(entityIds.length > 0);
195
180
  const pushEntityNodes = this.pushAccessMap[targetEntity];
181
+ let pushed = false;
196
182
  if (pushEntityNodes && pushEntityNodes.length > 0) {
197
183
  // 每个pushEntityNode代表配置的一个remoteEntity
198
184
  await Promise.all(pushEntityNodes.map(async (node) => {
199
- const { projection, groupByUsers, getRemotePushInfo: getRemoteAccessInfo, endpoint, actions, onSynchronized } = node;
185
+ const { projection, groupByUsers, getRemotePushInfo: getRemoteAccessInfo, groupBySelfEntity, endpoint, actions, onSynchronized } = node;
200
186
  // 定义中应该不可能没有actions
201
187
  if (!actions || actions.includes(action)) {
202
- const pushed = [];
203
188
  const rows = await context.select(targetEntity, {
204
189
  data: {
205
190
  id: 1,
@@ -213,60 +198,117 @@ class Synchronizer {
213
198
  }, { dontCollect: true, includedDeleted: true });
214
199
  // userId就是需要发送给远端的user,但是要将本次操作的user过滤掉(操作的原本产生者)
215
200
  const userSendDict = groupByUsers(rows);
201
+ const selfEntityIdDict = groupBySelfEntity(rows);
202
+ const encryptInfoDict = {};
216
203
  const pushToUserIdFn = async (userId) => {
217
204
  const { entity, entityId, rowIds } = userSendDict[userId];
205
+ const selfEntityIds = rowIds.map((rowId) => selfEntityIdDict[rowId]);
206
+ const uniqSelfEntityIds = (0, lodash_2.uniq)(selfEntityIds);
207
+ (0, assert_1.default)(uniqSelfEntityIds.length === 1, '推向同一个userId的oper不可能关联在多个不同的selfEntity行上');
208
+ const selfEntityId = uniqSelfEntityIds[0];
209
+ if (!encryptInfoDict[selfEntityId]) {
210
+ encryptInfoDict[selfEntityId] = await this.config.self.getSelfEncryptInfo(context, selfEntityId);
211
+ }
212
+ const selfEncryptInfo = encryptInfoDict[selfEntityId];
218
213
  // 推送到远端结点的oper
219
- const oper2 = {
220
- id: oper.id,
221
- action: action,
222
- data: (action === 'create' && data instanceof Array) ? data.filter(ele => rowIds.includes(ele.id)) : data,
223
- filter: {
224
- id: rowIds.length === 1 ? rowIds[0] : {
225
- $in: rowIds,
226
- }
227
- },
228
- bornAt: oper.bornAt,
229
- targetEntity,
230
- };
214
+ const oper2 = this.refineOperData(oper, rowIds);
231
215
  const { url } = await getRemoteAccessInfo(context, {
232
216
  userId,
233
217
  remoteEntityId: entityId,
234
218
  });
235
- await this.pushOper(oper, userId, url, endpoint, entity, entityId, selfEncryptInfo);
219
+ this.pushOperToChannel(oper2, userId, url, endpoint, entity, entityId, selfEncryptInfo, onSynchronized);
236
220
  };
237
221
  for (const userId in userSendDict) {
238
222
  if (userId !== operatorId) {
239
- pushed.push(pushToUserIdFn(userId));
240
- }
241
- }
242
- if (pushed.length > 0) {
243
- // 对单个oper,这里必须要等所有的push返回,不然会一直等在上面
244
- await Promise.all(pushed);
245
- if (onSynchronized) {
246
- await onSynchronized({
247
- action: action,
248
- data: data,
249
- rowIds: entityIds,
250
- }, context);
223
+ await pushToUserIdFn(userId);
224
+ pushed = true;
251
225
  }
252
226
  }
253
227
  }
254
228
  }));
255
- // 到这里说明此oper成功,否则会在内部不停循环重试
256
- // 主动去把oper上的跨事务标志清除,不依赖底层的triggerExecutor
257
- await context.operate('oper', {
258
- id: await (0, uuid_1.generateNewIdAsync)(),
259
- action: 'update',
229
+ }
230
+ // 如果oper一个也不用推送,说明其定义的推送path和对象行的path不匹配(动态指针)
231
+ return pushed;
232
+ }
233
+ /**
234
+ * 为了保证推送的oper序,采用从database中顺序读取所有需要推送的oper来进行推送
235
+ * 每个进程都保证把当前所有的oper顺序处理掉,就不会有乱序的问题,大家通过database上的锁来完成同步
236
+ * @param context
237
+ */
238
+ async trySynchronizeOpers() {
239
+ const context = await this.contextBuilder();
240
+ await context.begin();
241
+ try {
242
+ let dirtyOpers = await context.select('oper', {
260
243
  data: {
261
- [types_1.TriggerDataAttribute]: null,
262
- [types_1.TriggerUuidAttribute]: null,
244
+ id: 1,
263
245
  },
264
246
  filter: {
265
- id
266
- },
267
- }, {});
247
+ [types_1.TriggerDataAttribute]: {
248
+ $exists: true,
249
+ },
250
+ }
251
+ }, { dontCollect: true });
252
+ if (dirtyOpers.length > 0) {
253
+ // 这一步是加锁,保证只有一个进程完成推送,推送者提交前会将$$triggerData$$清零
254
+ const ids = dirtyOpers.map(ele => ele.id);
255
+ dirtyOpers = await context.select('oper', {
256
+ data: {
257
+ id: 1,
258
+ action: 1,
259
+ data: 1,
260
+ targetEntity: 1,
261
+ operatorId: 1,
262
+ [types_1.TriggerDataAttribute]: 1,
263
+ bornAt: 1,
264
+ $$createAt$$: 1,
265
+ $$seq$$: 1,
266
+ filter: 1,
267
+ },
268
+ filter: {
269
+ id: { $in: ids },
270
+ },
271
+ }, { dontCollect: true, forUpdate: true });
272
+ dirtyOpers = dirtyOpers.filter(ele => !!ele[types_1.TriggerDataAttribute]);
273
+ if (dirtyOpers.length > 0) {
274
+ const pushedIds = [];
275
+ const unpushedIds = [];
276
+ await Promise.all(dirtyOpers.map(async (oper) => {
277
+ const result = await this.dispatchOperToChannels(oper, context);
278
+ if (result) {
279
+ pushedIds.push(oper.id);
280
+ }
281
+ else {
282
+ unpushedIds.push(oper.id);
283
+ }
284
+ }));
285
+ if (unpushedIds.length > 0) {
286
+ await context.operate('oper', {
287
+ id: await (0, uuid_1.generateNewIdAsync)(),
288
+ action: 'update',
289
+ data: {
290
+ [types_1.TriggerDataAttribute]: null,
291
+ [types_1.TriggerUuidAttribute]: null,
292
+ },
293
+ filter: {
294
+ id: {
295
+ $in: unpushedIds,
296
+ }
297
+ }
298
+ }, {});
299
+ }
300
+ if (pushedIds.length > 0) {
301
+ await this.startAllChannel(context);
302
+ }
303
+ }
304
+ }
305
+ await context.commit();
306
+ }
307
+ catch (err) {
308
+ await context.rollback();
309
+ console.error(err);
310
+ throw err;
268
311
  }
269
- return 0;
270
312
  }
271
313
  makeCreateOperTrigger() {
272
314
  const { config } = this;
@@ -278,21 +320,24 @@ class Synchronizer {
278
320
  const pushEntities = [];
279
321
  const endpoint2 = (0, path_1.join)(endpoint || 'sync', self.entity);
280
322
  for (const def of pushEntityDefs) {
281
- const { path, relationName, recursive, entity, actions, onSynchronized } = def;
323
+ const { pathToRemoteEntity, pathToSelfEntity, relationName, recursive, entity, actions, onSynchronized } = def;
282
324
  pushEntities.push(entity);
283
325
  const relationName2 = relationName || rnRemote;
284
- const path2 = pathToUser ? `${path}.${pathToUser}` : path;
326
+ const path2 = pathToUser ? `${pathToRemoteEntity}.${pathToUser}` : pathToRemoteEntity;
327
+ (0, assert_1.default)(!recursive);
285
328
  const { projection, getData } = relationName2 ? (0, relationPath_1.destructRelationPath)(this.schema, entity, path2, {
286
329
  relation: {
287
330
  name: relationName,
288
331
  }
289
- }, recursive) : (0, relationPath_1.destructDirectPath)(this.schema, entity, path2, recursive);
332
+ }, recursive) : (0, relationPath_1.destructDirectUserPath)(this.schema, entity, path2);
333
+ const toSelfEntity = (0, relationPath_1.destructDirectPath)(this.schema, entity, pathToSelfEntity);
290
334
  const groupByUsers = (rows) => {
291
335
  const userRowDict = {};
292
336
  rows.forEach((row) => {
293
337
  const goals = getData(row);
294
338
  if (goals) {
295
339
  goals.forEach(({ entity, entityId, userId }) => {
340
+ (0, assert_1.default)(userId);
296
341
  if (userRowDict[userId]) {
297
342
  // 逻辑上来说同一个userId,其关联的entity和entityId必然相同,这个entity/entityId代表了对方
298
343
  (0, assert_1.default)(userRowDict[userId].entity === entity && userRowDict[userId].entityId === entityId);
@@ -310,10 +355,28 @@ class Synchronizer {
310
355
  });
311
356
  return userRowDict;
312
357
  };
358
+ const projectionMerged = (0, lodash_2.merge)(projection, toSelfEntity.projection);
359
+ const groupBySelfEntity = (rows) => {
360
+ const selfEntityIdDict = {};
361
+ for (const row of rows) {
362
+ const selfEntityInfo = toSelfEntity.getData(row, pathToSelfEntity);
363
+ if (selfEntityInfo) {
364
+ const selfEntityIds = selfEntityInfo.map((info) => {
365
+ (0, assert_1.default)(info.entity === this.config.self.entity);
366
+ return info.data.id;
367
+ });
368
+ const uniqSelfEntityIds = (0, lodash_2.uniq)(selfEntityIds);
369
+ (0, assert_1.default)(uniqSelfEntityIds.length === 1, '同一行数据不可能关联在两行selfEntity上');
370
+ selfEntityIdDict[row.id] = uniqSelfEntityIds[0];
371
+ }
372
+ }
373
+ return selfEntityIdDict;
374
+ };
313
375
  if (!this.pushAccessMap[entity]) {
314
376
  this.pushAccessMap[entity] = [{
315
- projection,
377
+ projection: projectionMerged,
316
378
  groupByUsers,
379
+ groupBySelfEntity,
317
380
  getRemotePushInfo: getPushInfo,
318
381
  endpoint: endpoint2,
319
382
  entity,
@@ -325,6 +388,7 @@ class Synchronizer {
325
388
  this.pushAccessMap[entity].push({
326
389
  projection,
327
390
  groupByUsers,
391
+ groupBySelfEntity,
328
392
  getRemotePushInfo: getPushInfo,
329
393
  endpoint: endpoint2,
330
394
  entity,
@@ -349,18 +413,19 @@ class Synchronizer {
349
413
  return pushEntities.includes(data.targetEntity)
350
414
  && !!this.pushAccessMap[targetEntity].find(({ actions }) => !actions || actions.includes(action));
351
415
  },
352
- fn: async ({ ids }, context) => {
416
+ fn: async ({ ids }) => {
353
417
  (0, assert_1.default)(ids.length === 1);
354
- const selfEncryptInfo = await this.config.self.getSelfEncryptInfo(context);
355
- this.synchronizeOpersToRemote(ids[0], context, selfEncryptInfo);
356
- throw new types_1.OakException('consistency on oper will be managed by myself');
418
+ this.trySynchronizeOpers();
419
+ // 内部自主处理triggerData,因此不需要让triggerExecutor处理
420
+ throw new types_1.OakMakeSureByMySelfException();
357
421
  }
358
422
  };
359
423
  return createOperTrigger;
360
424
  }
361
- constructor(config, schema) {
425
+ constructor(config, schema, contextBuilder) {
362
426
  this.config = config;
363
427
  this.schema = schema;
428
+ this.contextBuilder = contextBuilder;
364
429
  }
365
430
  /**
366
431
  * 根据sync的定义,生成对应的 commit triggers
@@ -372,26 +437,10 @@ class Synchronizer {
372
437
  getSyncRoutine() {
373
438
  return {
374
439
  name: 'checkpoint routine for sync',
375
- entity: 'oper',
376
- filter: {
377
- [types_1.TriggerDataAttribute]: {
378
- $exists: true,
379
- }
380
- },
381
- projection: {
382
- id: 1,
383
- [types_1.TriggerDataAttribute]: 1,
384
- },
385
- fn: async (context, data) => {
386
- for (const ele of data) {
387
- const { id, [types_1.TriggerDataAttribute]: triggerData } = ele;
388
- const { cxtStr = '{}' } = triggerData;
389
- await context.initialize(JSON.parse(cxtStr), true);
390
- const selfEncryptInfo = await this.config.self.getSelfEncryptInfo(context);
391
- this.synchronizeOpersToRemote(id, context, selfEncryptInfo);
392
- }
440
+ routine: async () => {
441
+ this.trySynchronizeOpers();
393
442
  return {};
394
- }
443
+ },
395
444
  };
396
445
  }
397
446
  getSelfEndpoint() {
@@ -405,6 +454,7 @@ class Synchronizer {
405
454
  const { [OAK_SYNC_HEADER_ENTITY]: meEntity, [OAK_SYNC_HEADER_ENTITYID]: meEntityId } = headers;
406
455
  console.log('接收到来自远端的sync数据', entity, JSON.stringify(body));
407
456
  const successIds = [];
457
+ const redundantIds = [];
408
458
  let failed;
409
459
  // todo 这里先缓存,不考虑本身同步相关信息的更新
410
460
  if (!this.remotePullInfoMap[entity]) {
@@ -456,12 +506,12 @@ class Synchronizer {
456
506
  }
457
507
  let maxBornAt = this.pullMaxBornAtMap[entityId];
458
508
  const opers = body;
459
- const outdatedOpers = opers.filter(ele => ele.bornAt <= maxBornAt);
460
- const freshOpers = opers.filter(ele => ele.bornAt > maxBornAt);
509
+ const staleOpers = opers.filter(ele => ele.$$seq$$ <= maxBornAt);
510
+ const freshOpers = opers.filter(ele => ele.$$seq$$ > maxBornAt);
461
511
  await Promise.all([
462
512
  // 无法严格保证推送按bornAt,所以一旦还有outdatedOpers,检查其已经被apply
463
513
  (async () => {
464
- const ids = outdatedOpers.map(ele => ele.id);
514
+ const ids = staleOpers.map(ele => ele.id);
465
515
  if (ids.length > 0) {
466
516
  const opersExisted = await context.select('oper', {
467
517
  data: {
@@ -478,13 +528,13 @@ class Synchronizer {
478
528
  // todo 这里如果远端业务逻辑严格,发生乱序应是无关的oper,直接执行就好 by Xc
479
529
  throw new Error(`在sync过程中发现有丢失的oper数据「${missed}」`);
480
530
  }
481
- successIds.push(...ids);
531
+ redundantIds.push(...ids);
482
532
  }
483
533
  })(),
484
534
  (async () => {
485
535
  for (const freshOper of freshOpers) {
486
- // freshOpers是按bornAt序产生的
487
- const { id, targetEntity, action, data, bornAt, filter } = freshOper;
536
+ // freshOpers是按$$seq$$序产生的
537
+ const { id, targetEntity, action, data, $$seq$$, filter } = freshOper;
488
538
  const ids = (0, filter_1.getRelevantIds)(filter);
489
539
  (0, assert_1.default)(ids.length > 0);
490
540
  try {
@@ -503,11 +553,11 @@ class Synchronizer {
503
553
  $in: ids,
504
554
  },
505
555
  },
506
- bornAt: bornAt,
556
+ bornAt: $$seq$$,
507
557
  };
508
558
  await context.operate(targetEntity, operation, {});
509
559
  successIds.push(id);
510
- maxBornAt = bornAt;
560
+ maxBornAt = $$seq$$;
511
561
  }
512
562
  catch (err) {
513
563
  console.error(err);
@@ -525,6 +575,7 @@ class Synchronizer {
525
575
  return {
526
576
  successIds,
527
577
  failed,
578
+ redundantIds,
528
579
  };
529
580
  }
530
581
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oak-backend-base",
3
- "version": "3.4.0",
3
+ "version": "3.4.2",
4
4
  "description": "oak-backend-base",
5
5
  "main": "lib/index",
6
6
  "author": {
@@ -22,8 +22,8 @@
22
22
  "node-schedule": "^2.1.0",
23
23
  "oak-common-aspect": "^2.3.0",
24
24
  "oak-db": "^3.2.0",
25
- "oak-domain": "^4.3.0",
26
- "oak-frontend-base": "^4.3.0",
25
+ "oak-domain": "^4.4.0",
26
+ "oak-frontend-base": "^4.4.0",
27
27
  "socket.io": "^4.7.2",
28
28
  "socket.io-client": "^4.7.2",
29
29
  "uuid": "^8.3.2"