meadow-integration 1.0.23 → 1.0.25
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/docs/_cover.md +1 -1
- package/docs/_version.json +7 -0
- package/docs/css/docuserve.css +277 -23
- package/docs/index.html +2 -2
- package/docs/retold-catalog.json +40 -1
- package/docs/retold-keyword-index.json +6150 -5279
- package/package.json +3 -2
- package/source/Meadow-Service-Integration-Adapter.js +5 -8
- package/source/services/clone/Meadow-Service-Sync-Entity-ComparisonOnly.js +435 -0
- package/source/services/clone/Meadow-Service-Sync-Entity-OngoingEventualConsistency.js +353 -0
- package/source/services/clone/Meadow-Service-Sync-Entity-TrueUp.js +199 -0
- package/source/services/clone/Meadow-Service-Sync.js +61 -6
- package/test/Meadow-Integration-Adapter_test.js +431 -48
- package/test/Meadow-Integration-ComprehensionPush_test.js +102 -8
- package/test/Meadow-Integration-NewStrategies_test.js +1265 -0
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
const libMeadowSyncEntityOngoing = require('./Meadow-Service-Sync-Entity-Ongoing.js');
|
|
2
|
+
|
|
3
|
+
class MeadowSyncEntityOngoingEventualConsistency extends libMeadowSyncEntityOngoing
|
|
4
|
+
{
|
|
5
|
+
constructor(pFable, pOptions, pServiceHash)
|
|
6
|
+
{
|
|
7
|
+
super(pFable, pOptions, pServiceHash);
|
|
8
|
+
|
|
9
|
+
this.serviceType = 'MeadowSyncEntityOngoingEventualConsistency';
|
|
10
|
+
|
|
11
|
+
// Milliseconds devoted to backwards bisection of existing records before
|
|
12
|
+
// moving on to pull new records. Default 30 seconds.
|
|
13
|
+
this.BackSyncTimeLimit = (typeof(this.options.BackSyncTimeLimit) === 'number')
|
|
14
|
+
? this.options.BackSyncTimeLimit
|
|
15
|
+
: 30000;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Bisect an ID range with a time budget. Checks the budget at the start of
|
|
19
|
+
// each recursive call and stops when time is exhausted. Otherwise identical
|
|
20
|
+
// to the inherited _bisectRange, except it subdivides upper-half-first so
|
|
21
|
+
// that the most recently created records (high IDs) are prioritized.
|
|
22
|
+
_bisectRangeWithTimeBudget(pMinID, pMaxID, pDepth, pStartTime, pTimeLimit, fCallback)
|
|
23
|
+
{
|
|
24
|
+
// Time budget check
|
|
25
|
+
if (Date.now() - pStartTime >= pTimeLimit)
|
|
26
|
+
{
|
|
27
|
+
return fCallback();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Global record cap check
|
|
31
|
+
if (this.MaxRecordsPerEntity > 0 && this._totalSyncedThisSync >= this.MaxRecordsPerEntity)
|
|
32
|
+
{
|
|
33
|
+
return fCallback();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const tmpRangeSize = pMaxID - pMinID + 1;
|
|
37
|
+
const tmpIDCol = this.DefaultIdentifier;
|
|
38
|
+
const tmpRangeFilter = `FBV~${tmpIDCol}~GE~${pMinID}~FBV~${tmpIDCol}~LE~${pMaxID}`;
|
|
39
|
+
|
|
40
|
+
this._getLocalCount(pMinID, pMaxID,
|
|
41
|
+
(pLocalCountError, pLocalCount) =>
|
|
42
|
+
{
|
|
43
|
+
if (pLocalCountError)
|
|
44
|
+
{
|
|
45
|
+
this.fable.log.warn(`${this.EntitySchema.TableName}: bisect local count error for range ${pMinID}-${pMaxID}: ${pLocalCountError}`);
|
|
46
|
+
return fCallback();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
this._getServerCount(tmpRangeFilter,
|
|
50
|
+
(pServerCountError, pServerCount) =>
|
|
51
|
+
{
|
|
52
|
+
if (pServerCountError)
|
|
53
|
+
{
|
|
54
|
+
this.fable.log.warn(`${this.EntitySchema.TableName}: bisect server count error for range ${pMinID}-${pMaxID}: ${pServerCountError}`);
|
|
55
|
+
return fCallback();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (pLocalCount === pServerCount)
|
|
59
|
+
{
|
|
60
|
+
if (!this._hasUpdateDate)
|
|
61
|
+
{
|
|
62
|
+
this._incrementProgress(pServerCount);
|
|
63
|
+
return fCallback();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
this._getLocalMaxUpdateDate(pMinID, pMaxID,
|
|
67
|
+
(pLocalMaxErr, pLocalMaxDate) =>
|
|
68
|
+
{
|
|
69
|
+
if (pLocalMaxErr || !pLocalMaxDate)
|
|
70
|
+
{
|
|
71
|
+
return fCallback();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const tmpMaxDateFilter = `${tmpRangeFilter}~FSF~UpdateDate~DESC~DESC`;
|
|
75
|
+
this._getServerRecords(tmpMaxDateFilter, 0, 1,
|
|
76
|
+
(pServerMaxErr, pServerMaxRecords) =>
|
|
77
|
+
{
|
|
78
|
+
if (pServerMaxErr || !pServerMaxRecords || pServerMaxRecords.length < 1)
|
|
79
|
+
{
|
|
80
|
+
return fCallback();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const tmpServerMaxDate = pServerMaxRecords[0].UpdateDate;
|
|
84
|
+
const tmpMaxDateDiff = Math.abs(this._normalizeDateUTC(pLocalMaxDate).diff(this._normalizeDateUTC(tmpServerMaxDate)));
|
|
85
|
+
|
|
86
|
+
if (tmpMaxDateDiff <= this.DateTimePrecisionMS)
|
|
87
|
+
{
|
|
88
|
+
this._incrementProgress(pServerCount);
|
|
89
|
+
return fCallback();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: date mismatch in range ${pMinID}-${pMaxID} (local max: ${pLocalMaxDate}, server max: ${tmpServerMaxDate})`);
|
|
93
|
+
if (tmpRangeSize <= this.BisectMinRangeSize)
|
|
94
|
+
{
|
|
95
|
+
return this._pullRangeFromServer(pMinID, pMaxID, fCallback);
|
|
96
|
+
}
|
|
97
|
+
return this._subdivideRangeReversed(pMinID, pMaxID, pDepth, pStartTime, pTimeLimit, fCallback);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Counts differ
|
|
104
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: count mismatch in range ${pMinID}-${pMaxID} (local: ${pLocalCount}, server: ${pServerCount})`);
|
|
105
|
+
|
|
106
|
+
if (tmpRangeSize <= this.BisectMinRangeSize)
|
|
107
|
+
{
|
|
108
|
+
return this._pullRangeFromServer(pMinID, pMaxID, fCallback);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return this._subdivideRangeReversed(pMinID, pMaxID, pDepth, pStartTime, pTimeLimit, fCallback);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Split a range in half and bisect the UPPER half first so that recently
|
|
117
|
+
// created/modified records (high IDs) are checked before older records.
|
|
118
|
+
_subdivideRangeReversed(pMinID, pMaxID, pDepth, pStartTime, pTimeLimit, fCallback)
|
|
119
|
+
{
|
|
120
|
+
const tmpMidID = Math.floor((pMinID + pMaxID) / 2);
|
|
121
|
+
|
|
122
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: subdividing range ${pMinID}-${pMaxID} at ID ${tmpMidID} (depth ${pDepth}, upper half first)`);
|
|
123
|
+
|
|
124
|
+
// Upper half first (reversed from standard bisection)
|
|
125
|
+
this._bisectRangeWithTimeBudget(tmpMidID + 1, pMaxID, pDepth + 1, pStartTime, pTimeLimit,
|
|
126
|
+
() =>
|
|
127
|
+
{
|
|
128
|
+
// Then lower half (if time remains -- checked at entry of next call)
|
|
129
|
+
this._bisectRangeWithTimeBudget(pMinID, tmpMidID, pDepth + 1, pStartTime, pTimeLimit, fCallback);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
_syncInternal(fCallback)
|
|
134
|
+
{
|
|
135
|
+
this.operation.createTimeStamp('EntityOngoingEventualConsistencySync');
|
|
136
|
+
|
|
137
|
+
this._totalSyncedThisSync = 0;
|
|
138
|
+
this._recordsCreated = 0;
|
|
139
|
+
this._recordsUpdated = 0;
|
|
140
|
+
|
|
141
|
+
const tmpSyncState = (
|
|
142
|
+
{
|
|
143
|
+
Local: { MaxIDEntity: -1, RecordCount: 0 },
|
|
144
|
+
Server: { MaxIDEntity: -1, RecordCount: 0 },
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
this._hasUpdateDate = false;
|
|
148
|
+
this._hasDeletedColumn = false;
|
|
149
|
+
|
|
150
|
+
if (this.EntitySchema && this.EntitySchema.MeadowSchema && Array.isArray(this.EntitySchema.MeadowSchema.Schema))
|
|
151
|
+
{
|
|
152
|
+
for (let i = 0; i < this.EntitySchema.MeadowSchema.Schema.length; i++)
|
|
153
|
+
{
|
|
154
|
+
const tmpColumn = this.EntitySchema.MeadowSchema.Schema[i];
|
|
155
|
+
if (tmpColumn.Column == 'UpdateDate')
|
|
156
|
+
{
|
|
157
|
+
this._hasUpdateDate = true;
|
|
158
|
+
}
|
|
159
|
+
if (tmpColumn.Type == 'Deleted' || tmpColumn.Column == 'Deleted')
|
|
160
|
+
{
|
|
161
|
+
this._hasDeletedColumn = true;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
this.fable.log.info(`Syncing with ONGOING EVENTUAL CONSISTENCY STRATEGY entity ${this.EntitySchema.TableName} (BackSyncTimeLimit: ${this.BackSyncTimeLimit}ms, UpdateDate: ${this._hasUpdateDate}, Deleted: ${this._hasDeletedColumn})...`);
|
|
167
|
+
|
|
168
|
+
this.fable.Utility.waterfall(
|
|
169
|
+
[
|
|
170
|
+
// ---- Stage 1: Gather local stats ----
|
|
171
|
+
(fStageComplete) =>
|
|
172
|
+
{
|
|
173
|
+
const tmpQuery = this.Meadow.query;
|
|
174
|
+
tmpQuery.setSort({ Column: this.DefaultIdentifier, Direction: 'Descending' });
|
|
175
|
+
tmpQuery.setCap(1);
|
|
176
|
+
if (!this._hasDeletedColumn)
|
|
177
|
+
{
|
|
178
|
+
tmpQuery.setDisableDeleteTracking(true);
|
|
179
|
+
}
|
|
180
|
+
this.Meadow.doRead(tmpQuery,
|
|
181
|
+
(pReadError, pQuery, pRecord) =>
|
|
182
|
+
{
|
|
183
|
+
if (pReadError)
|
|
184
|
+
{
|
|
185
|
+
this.fable.log.error(`Error reading local max entity ID ${this.EntitySchema.TableName}: ${pReadError}`);
|
|
186
|
+
return fStageComplete(`Error reading local max entity ID ${this.EntitySchema.TableName}: ${pReadError}`);
|
|
187
|
+
}
|
|
188
|
+
if (pRecord)
|
|
189
|
+
{
|
|
190
|
+
tmpSyncState.Local.MaxIDEntity = pRecord[this.DefaultIdentifier];
|
|
191
|
+
this.fable.log.info(`Found local max entity ID ${this.EntitySchema.TableName}: ${tmpSyncState.Local.MaxIDEntity}`);
|
|
192
|
+
}
|
|
193
|
+
else
|
|
194
|
+
{
|
|
195
|
+
this.fable.log.info(`No local records for ${this.EntitySchema.TableName}.`);
|
|
196
|
+
}
|
|
197
|
+
return fStageComplete();
|
|
198
|
+
});
|
|
199
|
+
},
|
|
200
|
+
(fStageComplete) =>
|
|
201
|
+
{
|
|
202
|
+
// Local count
|
|
203
|
+
const tmpQuery = this.Meadow.query;
|
|
204
|
+
if (!this._hasDeletedColumn)
|
|
205
|
+
{
|
|
206
|
+
tmpQuery.setDisableDeleteTracking(true);
|
|
207
|
+
}
|
|
208
|
+
this.Meadow.doCount(tmpQuery,
|
|
209
|
+
(pCountError, pQuery, pCount) =>
|
|
210
|
+
{
|
|
211
|
+
if (pCountError)
|
|
212
|
+
{
|
|
213
|
+
this.fable.log.error(`Error getting local count of ${this.EntitySchema.TableName}: ${pCountError}`);
|
|
214
|
+
return fStageComplete(`Error getting local count of ${this.EntitySchema.TableName}: ${pCountError}`);
|
|
215
|
+
}
|
|
216
|
+
tmpSyncState.Local.RecordCount = pCount;
|
|
217
|
+
this.fable.log.info(`Local count ${this.EntitySchema.TableName}: ${tmpSyncState.Local.RecordCount}`);
|
|
218
|
+
return fStageComplete();
|
|
219
|
+
});
|
|
220
|
+
},
|
|
221
|
+
|
|
222
|
+
// ---- Stage 2: Gather server stats ----
|
|
223
|
+
(fStageComplete) =>
|
|
224
|
+
{
|
|
225
|
+
this.fable.MeadowCloneRestClient.getJSON(`${this.EntitySchema.TableName}/Max/${this.DefaultIdentifier}`,
|
|
226
|
+
(pError, pResponse, pBody) =>
|
|
227
|
+
{
|
|
228
|
+
if (pError)
|
|
229
|
+
{
|
|
230
|
+
this.fable.log.warn(`Could not get server max entity ID for ${this.EntitySchema.TableName} (${pError}); continuing sync.`);
|
|
231
|
+
return fStageComplete();
|
|
232
|
+
}
|
|
233
|
+
if (pBody && pBody.hasOwnProperty(this.DefaultIdentifier))
|
|
234
|
+
{
|
|
235
|
+
tmpSyncState.Server.MaxIDEntity = pBody[this.DefaultIdentifier];
|
|
236
|
+
this.fable.log.info(`Found server max entity ID ${this.EntitySchema.TableName}: ${tmpSyncState.Server.MaxIDEntity}`);
|
|
237
|
+
}
|
|
238
|
+
return fStageComplete();
|
|
239
|
+
});
|
|
240
|
+
},
|
|
241
|
+
(fStageComplete) =>
|
|
242
|
+
{
|
|
243
|
+
this._getServerCount(null,
|
|
244
|
+
(pError, pCount) =>
|
|
245
|
+
{
|
|
246
|
+
if (pError)
|
|
247
|
+
{
|
|
248
|
+
this.fable.log.warn(`Could not get server count for ${this.EntitySchema.TableName} (${pError}); estimating from max ID.`);
|
|
249
|
+
tmpSyncState.Server.RecordCount = tmpSyncState.Server.MaxIDEntity > 0 ? tmpSyncState.Server.MaxIDEntity : 0;
|
|
250
|
+
return fStageComplete();
|
|
251
|
+
}
|
|
252
|
+
tmpSyncState.Server.RecordCount = pCount;
|
|
253
|
+
this.fable.log.info(`Server count ${this.EntitySchema.TableName}: ${tmpSyncState.Server.RecordCount}`);
|
|
254
|
+
return fStageComplete();
|
|
255
|
+
});
|
|
256
|
+
},
|
|
257
|
+
|
|
258
|
+
// Create progress tracker
|
|
259
|
+
(fStageComplete) =>
|
|
260
|
+
{
|
|
261
|
+
let tmpTrackerTotal = (this.MaxRecordsPerEntity > 0)
|
|
262
|
+
? Math.min(tmpSyncState.Server.RecordCount, this.MaxRecordsPerEntity)
|
|
263
|
+
: tmpSyncState.Server.RecordCount;
|
|
264
|
+
this.operation.createProgressTracker(tmpTrackerTotal, `FullSync-${this.EntitySchema.TableName}`);
|
|
265
|
+
return fStageComplete();
|
|
266
|
+
},
|
|
267
|
+
|
|
268
|
+
// ---- Stage 3: Time-budgeted backwards bisection ----
|
|
269
|
+
(fStageComplete) =>
|
|
270
|
+
{
|
|
271
|
+
if (tmpSyncState.Local.MaxIDEntity < 1)
|
|
272
|
+
{
|
|
273
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: no local records; skipping backwards bisection.`);
|
|
274
|
+
return fStageComplete();
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const tmpBackSyncStartTime = Date.now();
|
|
278
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: starting backwards bisection with ${this.BackSyncTimeLimit}ms time budget (ID range 1-${tmpSyncState.Local.MaxIDEntity})...`);
|
|
279
|
+
|
|
280
|
+
this._bisectRangeWithTimeBudget(1, tmpSyncState.Local.MaxIDEntity, 0, tmpBackSyncStartTime, this.BackSyncTimeLimit,
|
|
281
|
+
() =>
|
|
282
|
+
{
|
|
283
|
+
const tmpElapsed = Date.now() - tmpBackSyncStartTime;
|
|
284
|
+
const tmpExhausted = tmpElapsed >= this.BackSyncTimeLimit;
|
|
285
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: backwards bisection ${tmpExhausted ? 'time budget exhausted' : 'complete'} after ${tmpElapsed}ms.`);
|
|
286
|
+
return fStageComplete();
|
|
287
|
+
});
|
|
288
|
+
},
|
|
289
|
+
|
|
290
|
+
// ---- Stage 4: Pull new records by ID ----
|
|
291
|
+
(fStageComplete) =>
|
|
292
|
+
{
|
|
293
|
+
if (tmpSyncState.Server.MaxIDEntity <= tmpSyncState.Local.MaxIDEntity)
|
|
294
|
+
{
|
|
295
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: no new records by ID (server max ${tmpSyncState.Server.MaxIDEntity} <= local max ${tmpSyncState.Local.MaxIDEntity}).`);
|
|
296
|
+
return fStageComplete();
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const tmpIDCol = this.DefaultIdentifier;
|
|
300
|
+
const tmpFilter = `FBV~${tmpIDCol}~GT~${tmpSyncState.Local.MaxIDEntity}~FSF~${tmpIDCol}~ASC~ASC`;
|
|
301
|
+
const tmpEstimated = tmpSyncState.Server.MaxIDEntity - tmpSyncState.Local.MaxIDEntity;
|
|
302
|
+
|
|
303
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: pulling new records with ID > ${tmpSyncState.Local.MaxIDEntity} (~${tmpEstimated} estimated)...`);
|
|
304
|
+
|
|
305
|
+
this._pullServerRecords(tmpFilter, tmpEstimated,
|
|
306
|
+
(pError, pSyncedCount) =>
|
|
307
|
+
{
|
|
308
|
+
if (pError)
|
|
309
|
+
{
|
|
310
|
+
this.fable.log.warn(`${this.EntitySchema.TableName}: error pulling new records by ID: ${pError}`);
|
|
311
|
+
}
|
|
312
|
+
else
|
|
313
|
+
{
|
|
314
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: pulled ${pSyncedCount} new records by ID.`);
|
|
315
|
+
}
|
|
316
|
+
return fStageComplete();
|
|
317
|
+
});
|
|
318
|
+
},
|
|
319
|
+
],
|
|
320
|
+
(pError) =>
|
|
321
|
+
{
|
|
322
|
+
if (pError)
|
|
323
|
+
{
|
|
324
|
+
this.fable.log.error(`Error performing ongoing eventual consistency sync ${this.EntitySchema.TableName}: ${pError}`, { Error: pError });
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
let tmpTracker = this.operation.progressTrackers[`FullSync-${this.EntitySchema.TableName}`];
|
|
328
|
+
if (tmpTracker)
|
|
329
|
+
{
|
|
330
|
+
tmpTracker.CurrentCount = tmpTracker.TotalCount;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: ongoing eventual consistency sync complete.`);
|
|
334
|
+
|
|
335
|
+
this.syncResults = {
|
|
336
|
+
Created: this._recordsCreated,
|
|
337
|
+
Updated: this._recordsUpdated,
|
|
338
|
+
Deleted: 0,
|
|
339
|
+
ServerRecordCount: tmpSyncState.Server.RecordCount,
|
|
340
|
+
LocalRecordCount: tmpSyncState.Local.RecordCount
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
if (this.SyncDeletedRecords)
|
|
344
|
+
{
|
|
345
|
+
return this.syncDeletedRecords(() => { return fCallback(); });
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return fCallback();
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
module.exports = MeadowSyncEntityOngoingEventualConsistency;
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
const libMeadowSyncEntityOngoing = require('./Meadow-Service-Sync-Entity-Ongoing.js');
|
|
2
|
+
|
|
3
|
+
class MeadowSyncEntityTrueUp extends libMeadowSyncEntityOngoing
|
|
4
|
+
{
|
|
5
|
+
constructor(pFable, pOptions, pServiceHash)
|
|
6
|
+
{
|
|
7
|
+
super(pFable, pOptions, pServiceHash);
|
|
8
|
+
|
|
9
|
+
this.serviceType = 'MeadowSyncEntityTrueUp';
|
|
10
|
+
|
|
11
|
+
// Page size for the linear keyset-paginated walk. Larger than the normal
|
|
12
|
+
// PageSize because there is no bisection overhead -- we just walk every record.
|
|
13
|
+
this.TrueUpPageSize = (typeof(this.options.TrueUpPageSize) === 'number')
|
|
14
|
+
? this.options.TrueUpPageSize
|
|
15
|
+
: 500;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
_syncInternal(fCallback)
|
|
19
|
+
{
|
|
20
|
+
this.operation.createTimeStamp('EntityTrueUpSync');
|
|
21
|
+
|
|
22
|
+
this._totalSyncedThisSync = 0;
|
|
23
|
+
this._recordsCreated = 0;
|
|
24
|
+
this._recordsUpdated = 0;
|
|
25
|
+
|
|
26
|
+
const tmpSyncState = (
|
|
27
|
+
{
|
|
28
|
+
Server: { MaxIDEntity: -1, RecordCount: 0 },
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
this._hasDeletedColumn = false;
|
|
32
|
+
|
|
33
|
+
if (this.EntitySchema && this.EntitySchema.MeadowSchema && Array.isArray(this.EntitySchema.MeadowSchema.Schema))
|
|
34
|
+
{
|
|
35
|
+
for (let i = 0; i < this.EntitySchema.MeadowSchema.Schema.length; i++)
|
|
36
|
+
{
|
|
37
|
+
const tmpColumn = this.EntitySchema.MeadowSchema.Schema[i];
|
|
38
|
+
if (tmpColumn.Type == 'Deleted' || tmpColumn.Column == 'Deleted')
|
|
39
|
+
{
|
|
40
|
+
this._hasDeletedColumn = true;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
this.fable.log.info(`Syncing with TRUE-UP STRATEGY entity ${this.EntitySchema.TableName} (TrueUpPageSize: ${this.TrueUpPageSize})...`);
|
|
46
|
+
|
|
47
|
+
this.fable.Utility.waterfall(
|
|
48
|
+
[
|
|
49
|
+
// ---- Stage 1: Gather server stats ----
|
|
50
|
+
(fStageComplete) =>
|
|
51
|
+
{
|
|
52
|
+
this.fable.MeadowCloneRestClient.getJSON(`${this.EntitySchema.TableName}/Max/${this.DefaultIdentifier}`,
|
|
53
|
+
(pError, pResponse, pBody) =>
|
|
54
|
+
{
|
|
55
|
+
if (pError)
|
|
56
|
+
{
|
|
57
|
+
this.fable.log.warn(`Could not get server max entity ID for ${this.EntitySchema.TableName} (${pError}); continuing sync.`);
|
|
58
|
+
return fStageComplete();
|
|
59
|
+
}
|
|
60
|
+
if (pBody && pBody.hasOwnProperty(this.DefaultIdentifier))
|
|
61
|
+
{
|
|
62
|
+
tmpSyncState.Server.MaxIDEntity = pBody[this.DefaultIdentifier];
|
|
63
|
+
this.fable.log.info(`Found server max entity ID ${this.EntitySchema.TableName}: ${tmpSyncState.Server.MaxIDEntity}`);
|
|
64
|
+
}
|
|
65
|
+
return fStageComplete();
|
|
66
|
+
});
|
|
67
|
+
},
|
|
68
|
+
(fStageComplete) =>
|
|
69
|
+
{
|
|
70
|
+
this._getServerCount(null,
|
|
71
|
+
(pError, pCount) =>
|
|
72
|
+
{
|
|
73
|
+
if (pError)
|
|
74
|
+
{
|
|
75
|
+
this.fable.log.warn(`Could not get server count for ${this.EntitySchema.TableName} (${pError}); estimating from max ID.`);
|
|
76
|
+
tmpSyncState.Server.RecordCount = tmpSyncState.Server.MaxIDEntity > 0 ? tmpSyncState.Server.MaxIDEntity : 0;
|
|
77
|
+
return fStageComplete();
|
|
78
|
+
}
|
|
79
|
+
tmpSyncState.Server.RecordCount = pCount;
|
|
80
|
+
this.fable.log.info(`Server count ${this.EntitySchema.TableName}: ${tmpSyncState.Server.RecordCount}`);
|
|
81
|
+
return fStageComplete();
|
|
82
|
+
});
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
// Create progress tracker
|
|
86
|
+
(fStageComplete) =>
|
|
87
|
+
{
|
|
88
|
+
let tmpTrackerTotal = (this.MaxRecordsPerEntity > 0)
|
|
89
|
+
? Math.min(tmpSyncState.Server.RecordCount, this.MaxRecordsPerEntity)
|
|
90
|
+
: tmpSyncState.Server.RecordCount;
|
|
91
|
+
this.operation.createProgressTracker(tmpTrackerTotal, `FullSync-${this.EntitySchema.TableName}`);
|
|
92
|
+
return fStageComplete();
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
// ---- Stage 2: Linear keyset-paginated walk ----
|
|
96
|
+
(fStageComplete) =>
|
|
97
|
+
{
|
|
98
|
+
if (tmpSyncState.Server.MaxIDEntity < 1)
|
|
99
|
+
{
|
|
100
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: no records on server; nothing to true-up.`);
|
|
101
|
+
return fStageComplete();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const tmpIDCol = this.DefaultIdentifier;
|
|
105
|
+
let tmpCursorID = 0;
|
|
106
|
+
let tmpTotalProcessed = 0;
|
|
107
|
+
|
|
108
|
+
const fFetchPage = () =>
|
|
109
|
+
{
|
|
110
|
+
// Global record cap check
|
|
111
|
+
if (this.MaxRecordsPerEntity > 0 && this._totalSyncedThisSync >= this.MaxRecordsPerEntity)
|
|
112
|
+
{
|
|
113
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: global record cap reached (${this._totalSyncedThisSync}/${this.MaxRecordsPerEntity}); stopping true-up.`);
|
|
114
|
+
return fStageComplete();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const tmpFilter = `FBV~${tmpIDCol}~GT~${tmpCursorID}~FSF~${tmpIDCol}~ASC~ASC`;
|
|
118
|
+
|
|
119
|
+
this._getServerRecords(tmpFilter, 0, this.TrueUpPageSize,
|
|
120
|
+
(pError, pRecords) =>
|
|
121
|
+
{
|
|
122
|
+
if (pError)
|
|
123
|
+
{
|
|
124
|
+
this.fable.log.error(`Error fetching ${this.EntitySchema.TableName} true-up page at cursor ${tmpCursorID}: ${pError}`);
|
|
125
|
+
return fStageComplete(pError);
|
|
126
|
+
}
|
|
127
|
+
if (!pRecords || pRecords.length < 1)
|
|
128
|
+
{
|
|
129
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: true-up walk complete (${tmpTotalProcessed} records processed).`);
|
|
130
|
+
return fStageComplete();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
this.fable.Utility.eachLimit(pRecords, 5,
|
|
134
|
+
(pRecord, fRecordDone) =>
|
|
135
|
+
{
|
|
136
|
+
this._upsertRecord(pRecord,
|
|
137
|
+
() =>
|
|
138
|
+
{
|
|
139
|
+
tmpTotalProcessed++;
|
|
140
|
+
this._totalSyncedThisSync++;
|
|
141
|
+
return setImmediate(fRecordDone);
|
|
142
|
+
});
|
|
143
|
+
},
|
|
144
|
+
(pUpsertError) =>
|
|
145
|
+
{
|
|
146
|
+
this._incrementProgress(pRecords.length);
|
|
147
|
+
|
|
148
|
+
// Advance cursor to max ID seen in this page
|
|
149
|
+
tmpCursorID = pRecords[pRecords.length - 1][tmpIDCol];
|
|
150
|
+
|
|
151
|
+
if (pRecords.length < this.TrueUpPageSize)
|
|
152
|
+
{
|
|
153
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: true-up walk complete (${tmpTotalProcessed} records processed).`);
|
|
154
|
+
return fStageComplete();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: true-up progress ${tmpTotalProcessed} of ~${tmpSyncState.Server.RecordCount} records (cursor at ID ${tmpCursorID})...`);
|
|
158
|
+
return setImmediate(fFetchPage);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
fFetchPage();
|
|
164
|
+
},
|
|
165
|
+
],
|
|
166
|
+
(pError) =>
|
|
167
|
+
{
|
|
168
|
+
if (pError)
|
|
169
|
+
{
|
|
170
|
+
this.fable.log.error(`Error performing true-up sync ${this.EntitySchema.TableName}: ${pError}`, { Error: pError });
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
let tmpTracker = this.operation.progressTrackers[`FullSync-${this.EntitySchema.TableName}`];
|
|
174
|
+
if (tmpTracker)
|
|
175
|
+
{
|
|
176
|
+
tmpTracker.CurrentCount = tmpTracker.TotalCount;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: true-up sync complete.`);
|
|
180
|
+
|
|
181
|
+
this.syncResults = {
|
|
182
|
+
Created: this._recordsCreated,
|
|
183
|
+
Updated: this._recordsUpdated,
|
|
184
|
+
Deleted: 0,
|
|
185
|
+
ServerRecordCount: tmpSyncState.Server.RecordCount,
|
|
186
|
+
LocalRecordCount: 0
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
if (this.SyncDeletedRecords)
|
|
190
|
+
{
|
|
191
|
+
return this.syncDeletedRecords(() => { return fCallback(); });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return fCallback();
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
module.exports = MeadowSyncEntityTrueUp;
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
const libFableServiceProviderBase = require('fable-serviceproviderbase');
|
|
2
2
|
const libMeadowSyncEntityInitial = require('./Meadow-Service-Sync-Entity-Initial.js');
|
|
3
3
|
const libMeadowSyncEntityOngoing = require('./Meadow-Service-Sync-Entity-Ongoing.js');
|
|
4
|
+
const libMeadowSyncEntityOngoingEventualConsistency = require('./Meadow-Service-Sync-Entity-OngoingEventualConsistency.js');
|
|
5
|
+
const libMeadowSyncEntityTrueUp = require('./Meadow-Service-Sync-Entity-TrueUp.js');
|
|
6
|
+
const libMeadowSyncEntityComparisonOnly = require('./Meadow-Service-Sync-Entity-ComparisonOnly.js');
|
|
4
7
|
|
|
5
8
|
class MeadowSync extends libFableServiceProviderBase
|
|
6
9
|
{
|
|
@@ -20,6 +23,21 @@ class MeadowSync extends libFableServiceProviderBase
|
|
|
20
23
|
this.fable.ServiceManager.addServiceType('MeadowSyncEntityOngoing', libMeadowSyncEntityOngoing);
|
|
21
24
|
}
|
|
22
25
|
|
|
26
|
+
if (!this.fable.ServiceManager.servicesMap.hasOwnProperty('MeadowSyncEntityOngoingEventualConsistency'))
|
|
27
|
+
{
|
|
28
|
+
this.fable.ServiceManager.addServiceType('MeadowSyncEntityOngoingEventualConsistency', libMeadowSyncEntityOngoingEventualConsistency);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!this.fable.ServiceManager.servicesMap.hasOwnProperty('MeadowSyncEntityTrueUp'))
|
|
32
|
+
{
|
|
33
|
+
this.fable.ServiceManager.addServiceType('MeadowSyncEntityTrueUp', libMeadowSyncEntityTrueUp);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!this.fable.ServiceManager.servicesMap.hasOwnProperty('MeadowSyncEntityComparisonOnly'))
|
|
37
|
+
{
|
|
38
|
+
this.fable.ServiceManager.addServiceType('MeadowSyncEntityComparisonOnly', libMeadowSyncEntityComparisonOnly);
|
|
39
|
+
}
|
|
40
|
+
|
|
23
41
|
// If this is empty, we will sync everything in the loaded Schema.
|
|
24
42
|
// Otherwise, we will go through this list and sync them in this order.
|
|
25
43
|
this.SyncEntityList = [];
|
|
@@ -103,6 +121,28 @@ class MeadowSync extends libFableServiceProviderBase
|
|
|
103
121
|
this.SyncDeletedRecordsQueryString = this.options.SyncDeletedRecordsQueryString;
|
|
104
122
|
}
|
|
105
123
|
|
|
124
|
+
// Milliseconds devoted to backwards bisection in OngoingEventualConsistency mode.
|
|
125
|
+
this.BackSyncTimeLimit = 30000;
|
|
126
|
+
if (this.fable.ProgramConfiguration.hasOwnProperty('BackSyncTimeLimit'))
|
|
127
|
+
{
|
|
128
|
+
this.BackSyncTimeLimit = parseInt(this.fable.ProgramConfiguration.BackSyncTimeLimit, 10) || 30000;
|
|
129
|
+
}
|
|
130
|
+
else if (this.options.hasOwnProperty('BackSyncTimeLimit'))
|
|
131
|
+
{
|
|
132
|
+
this.BackSyncTimeLimit = parseInt(this.options.BackSyncTimeLimit, 10) || 30000;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Page size for the linear keyset-paginated walk in TrueUp mode.
|
|
136
|
+
this.TrueUpPageSize = 500;
|
|
137
|
+
if (this.fable.ProgramConfiguration.hasOwnProperty('TrueUpPageSize'))
|
|
138
|
+
{
|
|
139
|
+
this.TrueUpPageSize = parseInt(this.fable.ProgramConfiguration.TrueUpPageSize, 10) || 500;
|
|
140
|
+
}
|
|
141
|
+
else if (this.options.hasOwnProperty('TrueUpPageSize'))
|
|
142
|
+
{
|
|
143
|
+
this.TrueUpPageSize = parseInt(this.options.TrueUpPageSize, 10) || 500;
|
|
144
|
+
}
|
|
145
|
+
|
|
106
146
|
this.MeadowSchema = false;
|
|
107
147
|
this.MeadowSchemaTableList = false;
|
|
108
148
|
|
|
@@ -142,6 +182,8 @@ class MeadowSync extends libFableServiceProviderBase
|
|
|
142
182
|
MaxRecordsPerEntity: this.MaxRecordsPerEntity,
|
|
143
183
|
DateTimePrecisionMS: this.DateTimePrecisionMS,
|
|
144
184
|
UseAdvancedIDPagination: this.UseAdvancedIDPagination,
|
|
185
|
+
BackSyncTimeLimit: this.BackSyncTimeLimit,
|
|
186
|
+
TrueUpPageSize: this.TrueUpPageSize,
|
|
145
187
|
};
|
|
146
188
|
|
|
147
189
|
// Apply per-entity option overrides if configured
|
|
@@ -151,16 +193,29 @@ class MeadowSync extends libFableServiceProviderBase
|
|
|
151
193
|
}
|
|
152
194
|
|
|
153
195
|
let tmpSyncEntity;
|
|
196
|
+
let tmpServiceTypeName;
|
|
154
197
|
|
|
155
|
-
|
|
198
|
+
switch (this.SyncMode)
|
|
156
199
|
{
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
200
|
+
case 'Ongoing':
|
|
201
|
+
tmpServiceTypeName = 'MeadowSyncEntityOngoing';
|
|
202
|
+
break;
|
|
203
|
+
case 'OngoingEventualConsistency':
|
|
204
|
+
tmpServiceTypeName = 'MeadowSyncEntityOngoingEventualConsistency';
|
|
205
|
+
break;
|
|
206
|
+
case 'TrueUp':
|
|
207
|
+
tmpServiceTypeName = 'MeadowSyncEntityTrueUp';
|
|
208
|
+
break;
|
|
209
|
+
case 'ComparisonOnly':
|
|
210
|
+
tmpServiceTypeName = 'MeadowSyncEntityComparisonOnly';
|
|
211
|
+
break;
|
|
212
|
+
default:
|
|
213
|
+
tmpServiceTypeName = 'MeadowSyncEntityInitial';
|
|
214
|
+
break;
|
|
162
215
|
}
|
|
163
216
|
|
|
217
|
+
tmpSyncEntity = this.fable.serviceManager.instantiateServiceProvider(tmpServiceTypeName, tmpSyncEntityOptions, `SyncEntity-${tmpEntitySchema.TableName}`);
|
|
218
|
+
|
|
164
219
|
this.MeadowSyncEntities[tmpEntitySchema.TableName] = tmpSyncEntity;
|
|
165
220
|
|
|
166
221
|
return tmpSyncEntity.initialize((pInitError) =>
|