retold-data-service 2.1.5 → 2.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/publish-image.yml +2 -2
- package/BUILDING-AND-PUBLISHING.md +4 -4
- package/CONTRIBUTING.md +1 -1
- package/package.json +10 -10
- package/source/services/comprehension-loader/web/comprehension-loader.js +121 -19
- package/source/services/comprehension-loader/web/comprehension-loader.js.map +1 -1
- package/source/services/comprehension-loader/web/comprehension-loader.min.js +3 -3
- package/source/services/comprehension-loader/web/comprehension-loader.min.js.map +1 -1
- package/source/services/data-cloner/DataCloner-Command-Headless.js +100 -3
- package/source/services/data-cloner/DataCloner-Command-Schema.js +105 -8
- package/source/services/data-cloner/DataCloner-Command-Sync.js +1 -0
- package/source/services/data-cloner/web/data-cloner.js +121 -19
- package/source/services/data-cloner/web/data-cloner.js.map +1 -1
- package/source/services/data-cloner/web/data-cloner.min.js +3 -3
- package/source/services/data-cloner/web/data-cloner.min.js.map +1 -1
- package/tools/smoke-converge-endpoint.js +190 -0
|
@@ -240,6 +240,46 @@ module.exports = (pDataClonerService, pConfig, pCLIOptions, fCallback) =>
|
|
|
240
240
|
});
|
|
241
241
|
};
|
|
242
242
|
|
|
243
|
+
// Step 5b: Converge each deployed table's indexes to the clone index policy.
|
|
244
|
+
//
|
|
245
|
+
// Historically nothing in the headless pipeline touched indices, so headless
|
|
246
|
+
// clones ran with just the clustered primary key — leaving the
|
|
247
|
+
// OngoingEventualConsistency Deleted-filtered range counts clustered-scanning
|
|
248
|
+
// wide rows, which can stall large clones. This step declaratively brings the
|
|
249
|
+
// clone's indexes in line with the desired set (standard operational indexes +
|
|
250
|
+
// any Sync.IndexPolicy.TableIndexes extras): create missing, then drop
|
|
251
|
+
// undeclared per prune scope (default 'managed' — only policy-owned / legacy
|
|
252
|
+
// artifacts, never a hand-authored index). Create-before-drop and
|
|
253
|
+
// no-loss-on-failure keep coverage intact. It must never block the sync, so a
|
|
254
|
+
// failure here is logged and the pipeline proceeds. Opt out with
|
|
255
|
+
// Sync.CreateOperationalIndices=false or Sync.IndexPolicy.Enabled=false.
|
|
256
|
+
let fStep5b_ConvergeIndices = (fNext) =>
|
|
257
|
+
{
|
|
258
|
+
let tmpSync = pConfig.Sync || {};
|
|
259
|
+
let tmpIndexPolicy = (tmpSync.IndexPolicy && typeof(tmpSync.IndexPolicy) === 'object') ? tmpSync.IndexPolicy : {};
|
|
260
|
+
if (tmpSync.CreateOperationalIndices === false || tmpIndexPolicy.Enabled === false)
|
|
261
|
+
{
|
|
262
|
+
tmpFable.log.info('Headless: Index convergence disabled by config; skipping.');
|
|
263
|
+
return fNext();
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
let tmpTables = pConfig.Tables || [];
|
|
267
|
+
tmpFable.log.info(`Headless: Converging indexes on ${tmpTables.length > 0 ? tmpTables.length + ' selected' : 'all'} tables (prune scope: ${tmpIndexPolicy.PruneScope || 'managed'})...`);
|
|
268
|
+
fPost(`${tmpPrefix}/schema/indices/converge`, { Tables: tmpTables, IndexPolicy: tmpIndexPolicy },
|
|
269
|
+
(pError, pData) =>
|
|
270
|
+
{
|
|
271
|
+
if (pError || !pData || !pData.Success)
|
|
272
|
+
{
|
|
273
|
+
tmpFable.log.warn(`Headless: Index convergence reported a problem (continuing to sync): ${pError || (pData && pData.Error) || 'Unknown error'}`);
|
|
274
|
+
}
|
|
275
|
+
else
|
|
276
|
+
{
|
|
277
|
+
tmpFable.log.info(`Headless: ${pData.Message}`);
|
|
278
|
+
}
|
|
279
|
+
return fNext();
|
|
280
|
+
});
|
|
281
|
+
};
|
|
282
|
+
|
|
243
283
|
// Step 6: Start sync
|
|
244
284
|
let fStep6_Sync = (fNext) =>
|
|
245
285
|
{
|
|
@@ -254,6 +294,7 @@ module.exports = (pDataClonerService, pConfig, pCLIOptions, fCallback) =>
|
|
|
254
294
|
DateTimePrecisionMS: tmpSync.DateTimePrecisionMS,
|
|
255
295
|
BackSyncTimeLimit: tmpSync.BackSyncTimeLimit,
|
|
256
296
|
TrueUpPageSize: tmpSync.TrueUpPageSize,
|
|
297
|
+
SyncRecordConcurrency: tmpSync.SyncRecordConcurrency,
|
|
257
298
|
SyncEntityOptions: tmpSync.SyncEntityOptions
|
|
258
299
|
});
|
|
259
300
|
|
|
@@ -268,6 +309,38 @@ module.exports = (pDataClonerService, pConfig, pCLIOptions, fCallback) =>
|
|
|
268
309
|
}
|
|
269
310
|
tmpFable.log.info(`Headless: ${pData.Message}`);
|
|
270
311
|
|
|
312
|
+
// Stall detection. Synced/Total freezes during the (time-budgeted)
|
|
313
|
+
// delete phase, so a healthy delete pass looks identical to a hung
|
|
314
|
+
// one from the poll's vantage point. Only treat it as a stall when
|
|
315
|
+
// NOTHING in the status changes for well beyond one phase budget —
|
|
316
|
+
// default max(2x BackSyncTimeLimit, 20 min) — then exit non-zero so
|
|
317
|
+
// the Docker restart loop can recover (cursor/progress state persists
|
|
318
|
+
// on the mounted volume). Configure via Sync.StallTimeoutMs; set it
|
|
319
|
+
// to 0 to disable.
|
|
320
|
+
let tmpStallConfigured = parseInt(tmpSync.StallTimeoutMs, 10);
|
|
321
|
+
let tmpStallTimeoutMs;
|
|
322
|
+
if (tmpStallConfigured === 0)
|
|
323
|
+
{
|
|
324
|
+
tmpStallTimeoutMs = 0;
|
|
325
|
+
}
|
|
326
|
+
else if (!isNaN(tmpStallConfigured) && tmpStallConfigured > 0)
|
|
327
|
+
{
|
|
328
|
+
tmpStallTimeoutMs = tmpStallConfigured;
|
|
329
|
+
}
|
|
330
|
+
else
|
|
331
|
+
{
|
|
332
|
+
let tmpBudget = parseInt(tmpSync.BackSyncTimeLimit, 10);
|
|
333
|
+
if (isNaN(tmpBudget) || tmpBudget <= 0)
|
|
334
|
+
{
|
|
335
|
+
tmpBudget = 600000;
|
|
336
|
+
}
|
|
337
|
+
tmpStallTimeoutMs = Math.max(2 * tmpBudget, 1200000);
|
|
338
|
+
}
|
|
339
|
+
let fLogStall = (typeof(tmpFable.log.fatal) === 'function') ? tmpFable.log.fatal.bind(tmpFable.log) : tmpFable.log.error.bind(tmpFable.log);
|
|
340
|
+
let tmpLastProgressSignature = null;
|
|
341
|
+
let tmpLastProgressAtMs = Date.now();
|
|
342
|
+
tmpFable.log.info(`Headless: Stall detector ${tmpStallTimeoutMs > 0 ? 'armed at ' + (tmpStallTimeoutMs / 60000).toFixed(1) + ' min of no observable progress' : 'disabled'}.`);
|
|
343
|
+
|
|
271
344
|
// Poll for completion
|
|
272
345
|
let fPoll = () =>
|
|
273
346
|
{
|
|
@@ -291,6 +364,27 @@ module.exports = (pDataClonerService, pConfig, pCLIOptions, fCallback) =>
|
|
|
291
364
|
let tmpA = tmpTables[tmpActive[0]];
|
|
292
365
|
tmpFable.log.info(`Headless: [${tmpDone.length}/${tmpNames.length}] Syncing ${tmpActive[0]}: ${tmpA.Synced}/${tmpA.Total}`);
|
|
293
366
|
}
|
|
367
|
+
|
|
368
|
+
// Advance the stall clock only when the observable status
|
|
369
|
+
// changes (any table's Status or Synced/Total counters).
|
|
370
|
+
let tmpProgressSignature = tmpNames.slice().sort().map((pName) =>
|
|
371
|
+
{
|
|
372
|
+
let tmpT = tmpTables[pName];
|
|
373
|
+
return `${pName}:${tmpT.Status}:${tmpT.Synced}/${tmpT.Total}`;
|
|
374
|
+
}).join('|');
|
|
375
|
+
let tmpNowMs = Date.now();
|
|
376
|
+
if (tmpProgressSignature !== tmpLastProgressSignature)
|
|
377
|
+
{
|
|
378
|
+
tmpLastProgressSignature = tmpProgressSignature;
|
|
379
|
+
tmpLastProgressAtMs = tmpNowMs;
|
|
380
|
+
}
|
|
381
|
+
else if (tmpStallTimeoutMs > 0 && (tmpNowMs - tmpLastProgressAtMs) >= tmpStallTimeoutMs)
|
|
382
|
+
{
|
|
383
|
+
let tmpStalledMin = ((tmpNowMs - tmpLastProgressAtMs) / 60000).toFixed(1);
|
|
384
|
+
fLogStall(`Headless: STALL DETECTED — sync status unchanged for ${tmpStalledMin} min (threshold ${(tmpStallTimeoutMs / 60000).toFixed(1)} min). Active: [${tmpActive.join(', ') || 'none'}], ${tmpDone.length}/${tmpNames.length} tables done. Exiting non-zero so the container restart loop can recover; cursor/progress state persists on the mounted volume.`);
|
|
385
|
+
return process.exit(1);
|
|
386
|
+
}
|
|
387
|
+
|
|
294
388
|
return setTimeout(fPoll, 5000);
|
|
295
389
|
}
|
|
296
390
|
|
|
@@ -347,10 +441,13 @@ module.exports = (pDataClonerService, pConfig, pCLIOptions, fCallback) =>
|
|
|
347
441
|
{
|
|
348
442
|
fStep5_Deploy(() =>
|
|
349
443
|
{
|
|
350
|
-
|
|
444
|
+
fStep5b_ConvergeIndices(() =>
|
|
351
445
|
{
|
|
352
|
-
|
|
353
|
-
|
|
446
|
+
fStep6_Sync(() =>
|
|
447
|
+
{
|
|
448
|
+
tmpFable.log.info('Headless: Pipeline complete.');
|
|
449
|
+
return fCallback();
|
|
450
|
+
});
|
|
354
451
|
});
|
|
355
452
|
});
|
|
356
453
|
});
|
|
@@ -16,6 +16,8 @@ module.exports = (pDataClonerService, pOratorServiceServer) =>
|
|
|
16
16
|
|
|
17
17
|
let libFs = require('fs');
|
|
18
18
|
let _ProviderRegistry = require('./DataCloner-ProviderRegistry.js');
|
|
19
|
+
let libIndexPolicy = require('meadow-integration/source/services/clone/Meadow-Service-IndexPolicy.js');
|
|
20
|
+
let libIndexConvergence = require('meadow-integration/source/services/clone/Meadow-Service-IndexConvergence.js');
|
|
19
21
|
|
|
20
22
|
// POST /clone/schema/fetch
|
|
21
23
|
// Accepts either:
|
|
@@ -889,7 +891,7 @@ module.exports = (pDataClonerService, pOratorServiceServer) =>
|
|
|
889
891
|
let tmpTableNames = tmpFilterTables || Object.keys(tmpModelTables);
|
|
890
892
|
let tmpCreatedIndices = [];
|
|
891
893
|
|
|
892
|
-
tmpFable.log.info(`Data Cloner: Creating missing
|
|
894
|
+
tmpFable.log.info(`Data Cloner: Creating missing operational indices (GUID + sync) for ${tmpTableNames.length} tables...`);
|
|
893
895
|
|
|
894
896
|
tmpFable.Utility.eachLimit(tmpTableNames, 1,
|
|
895
897
|
(pTableName, fNextTable) =>
|
|
@@ -903,10 +905,15 @@ module.exports = (pDataClonerService, pOratorServiceServer) =>
|
|
|
903
905
|
// Generate all index statements for this table (provider-specific)
|
|
904
906
|
let tmpAllStatements = tmpSchemaProvider.generateCreateIndexStatements(tmpTableSchema);
|
|
905
907
|
|
|
906
|
-
// Filter to
|
|
907
|
-
|
|
908
|
+
// Filter to the clone's operational indices: GUID uniqueness
|
|
909
|
+
// (AK_M_GUID*) plus the standard sync indices (IX_M_SYNC*) that
|
|
910
|
+
// keep the OngoingEventualConsistency range-count / delete walks
|
|
911
|
+
// seekable. Application ForeignKey / Indexed-column indices are
|
|
912
|
+
// intentionally left out of the clone.
|
|
913
|
+
let tmpCloneIndexStatements = tmpAllStatements.filter((pStmt) => pStmt.Name &&
|
|
914
|
+
(pStmt.Name.indexOf('AK_M_GUID') === 0 || pStmt.Name.indexOf('IX_M_SYNC') === 0));
|
|
908
915
|
|
|
909
|
-
if (
|
|
916
|
+
if (tmpCloneIndexStatements.length < 1)
|
|
910
917
|
{
|
|
911
918
|
return fNextTable();
|
|
912
919
|
}
|
|
@@ -914,12 +921,12 @@ module.exports = (pDataClonerService, pOratorServiceServer) =>
|
|
|
914
921
|
// createIndex is idempotent on all providers — safe to call even if index exists
|
|
915
922
|
let fCreateNext = (pIndex) =>
|
|
916
923
|
{
|
|
917
|
-
if (pIndex >=
|
|
924
|
+
if (pIndex >= tmpCloneIndexStatements.length)
|
|
918
925
|
{
|
|
919
926
|
return fNextTable();
|
|
920
927
|
}
|
|
921
928
|
|
|
922
|
-
let tmpStmt =
|
|
929
|
+
let tmpStmt = tmpCloneIndexStatements[pIndex];
|
|
923
930
|
tmpSchemaProvider.createIndex(tmpStmt,
|
|
924
931
|
(pCreateError) =>
|
|
925
932
|
{
|
|
@@ -946,8 +953,8 @@ module.exports = (pDataClonerService, pOratorServiceServer) =>
|
|
|
946
953
|
() =>
|
|
947
954
|
{
|
|
948
955
|
let tmpMessage = tmpCreatedIndices.length > 0
|
|
949
|
-
? `${tmpCreatedIndices.length}
|
|
950
|
-
: 'No new
|
|
956
|
+
? `${tmpCreatedIndices.length} operational index(es) created.`
|
|
957
|
+
: 'No new operational indices were needed.';
|
|
951
958
|
|
|
952
959
|
tmpFable.log.info(`Data Cloner: ${tmpMessage}`);
|
|
953
960
|
|
|
@@ -960,4 +967,94 @@ module.exports = (pDataClonerService, pOratorServiceServer) =>
|
|
|
960
967
|
return fNext();
|
|
961
968
|
});
|
|
962
969
|
});
|
|
970
|
+
|
|
971
|
+
// POST /clone/schema/indices/converge — Converge each deployed table's indexes
|
|
972
|
+
// to the clone index policy (create missing, then drop undeclared per prune
|
|
973
|
+
// scope). This is the authoritative, declarative index mechanism: the desired
|
|
974
|
+
// set = standard operational (Deleted composite + non-unique GUID) + any
|
|
975
|
+
// caller-declared per-table extras (body.IndexPolicy.TableIndexes).
|
|
976
|
+
pOratorServiceServer.post(`${tmpPrefix}/schema/indices/converge`,
|
|
977
|
+
(pRequest, pResponse, fNext) =>
|
|
978
|
+
{
|
|
979
|
+
if (!tmpCloneState.DeployedModelObject)
|
|
980
|
+
{
|
|
981
|
+
pResponse.send(400, { Success: false, Error: 'No schema deployed. Deploy tables first.' });
|
|
982
|
+
return fNext();
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
let tmpProviderName = tmpCloneState.ConnectionProvider;
|
|
986
|
+
let tmpProviderRegistryEntry = _ProviderRegistry[tmpProviderName];
|
|
987
|
+
let tmpActiveProvider = tmpProviderRegistryEntry ? tmpFable[tmpProviderRegistryEntry.serviceName] : null;
|
|
988
|
+
|
|
989
|
+
if (!tmpActiveProvider || !tmpActiveProvider.connected)
|
|
990
|
+
{
|
|
991
|
+
pResponse.send(400, { Success: false, Error: 'No connected provider available.' });
|
|
992
|
+
return fNext();
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
if (typeof(tmpActiveProvider.introspectTableIndices) !== 'function'
|
|
996
|
+
|| typeof(tmpActiveProvider.generateCreateIndexStatements) !== 'function'
|
|
997
|
+
|| typeof(tmpActiveProvider.createIndex) !== 'function'
|
|
998
|
+
|| typeof(tmpActiveProvider.dropIndex) !== 'function')
|
|
999
|
+
{
|
|
1000
|
+
pResponse.send(400, { Success: false, Error: `Provider ${tmpProviderName} does not support index convergence.` });
|
|
1001
|
+
return fNext();
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
let tmpBody = pRequest.body || {};
|
|
1005
|
+
let tmpIndexConfig = (tmpBody.IndexPolicy && typeof(tmpBody.IndexPolicy) === 'object') ? tmpBody.IndexPolicy : {};
|
|
1006
|
+
let tmpPruneScope = tmpIndexConfig.PruneScope || libIndexConvergence.PRUNE_MANAGED;
|
|
1007
|
+
let tmpFilterTables = Array.isArray(tmpBody.Tables) && tmpBody.Tables.length > 0 ? tmpBody.Tables : null;
|
|
1008
|
+
|
|
1009
|
+
let tmpModelTables = tmpCloneState.DeployedModelObject.Tables || {};
|
|
1010
|
+
let tmpTableNames = tmpFilterTables || Object.keys(tmpModelTables);
|
|
1011
|
+
let tmpResults = [];
|
|
1012
|
+
|
|
1013
|
+
tmpFable.log.info(`Data Cloner: Converging indexes for ${tmpTableNames.length} tables (prune scope: ${tmpPruneScope})...`);
|
|
1014
|
+
|
|
1015
|
+
tmpFable.Utility.eachLimit(tmpTableNames, 1,
|
|
1016
|
+
(pTableName, fNextTable) =>
|
|
1017
|
+
{
|
|
1018
|
+
let tmpTableSchema = tmpModelTables[pTableName];
|
|
1019
|
+
if (!tmpTableSchema)
|
|
1020
|
+
{
|
|
1021
|
+
return fNextTable();
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
let tmpDesired = libIndexPolicy.resolveDesiredIndexes(tmpTableSchema, tmpIndexConfig);
|
|
1025
|
+
libIndexConvergence.convergeTableIndexes(tmpActiveProvider, tmpTableSchema, tmpDesired,
|
|
1026
|
+
{ PruneScope: tmpPruneScope, log: tmpFable.log },
|
|
1027
|
+
(pConvergeError, pResult) =>
|
|
1028
|
+
{
|
|
1029
|
+
if (pConvergeError)
|
|
1030
|
+
{
|
|
1031
|
+
tmpFable.log.warn(`Data Cloner: Index convergence for ${pTableName} failed: ${pConvergeError}`);
|
|
1032
|
+
}
|
|
1033
|
+
else if (pResult && (pResult.created.length > 0 || pResult.dropped.length > 0 || pResult.skipped.length > 0))
|
|
1034
|
+
{
|
|
1035
|
+
tmpFable.log.info(`Data Cloner: ${pTableName} — created [${pResult.created.join(', ')}], dropped [${pResult.dropped.join(', ')}]${pResult.skipped.length > 0 ? `, skipped [${pResult.skipped.join(', ')}]` : ''}`);
|
|
1036
|
+
tmpResults.push(pResult);
|
|
1037
|
+
}
|
|
1038
|
+
return fNextTable();
|
|
1039
|
+
});
|
|
1040
|
+
},
|
|
1041
|
+
() =>
|
|
1042
|
+
{
|
|
1043
|
+
let tmpCreated = tmpResults.reduce((pSum, pR) => { return pSum + pR.created.length; }, 0);
|
|
1044
|
+
let tmpDropped = tmpResults.reduce((pSum, pR) => { return pSum + pR.dropped.length; }, 0);
|
|
1045
|
+
let tmpMessage = (tmpCreated > 0 || tmpDropped > 0)
|
|
1046
|
+
? `Index convergence: ${tmpCreated} created, ${tmpDropped} dropped across ${tmpResults.length} table(s).`
|
|
1047
|
+
: 'Index convergence: all tables already match the desired index set.';
|
|
1048
|
+
|
|
1049
|
+
tmpFable.log.info(`Data Cloner: ${tmpMessage}`);
|
|
1050
|
+
|
|
1051
|
+
pResponse.send(200,
|
|
1052
|
+
{
|
|
1053
|
+
Success: true,
|
|
1054
|
+
Results: tmpResults,
|
|
1055
|
+
Message: tmpMessage
|
|
1056
|
+
});
|
|
1057
|
+
return fNext();
|
|
1058
|
+
});
|
|
1059
|
+
});
|
|
963
1060
|
};
|
|
@@ -20,6 +20,7 @@ const _RuntimeSyncProperties = {
|
|
|
20
20
|
MaxRecordsPerEntity: (pVal) => { let tmpN = parseInt(pVal, 10); return (!isNaN(tmpN) && tmpN > 0) ? tmpN : null; },
|
|
21
21
|
DateTimePrecisionMS: (pVal) => { let tmpN = parseInt(pVal, 10); return !isNaN(tmpN) ? tmpN : null; },
|
|
22
22
|
TrueUpPageSize: (pVal) => { let tmpN = parseInt(pVal, 10); return (!isNaN(tmpN) && tmpN > 0) ? tmpN : null; },
|
|
23
|
+
SyncRecordConcurrency: (pVal) => { let tmpN = parseInt(pVal, 10); return (!isNaN(tmpN) && tmpN > 0) ? tmpN : null; },
|
|
23
24
|
UseAdvancedIDPagination: (pVal) => !!pVal,
|
|
24
25
|
SyncDeletedRecords: (pVal) => !!pVal
|
|
25
26
|
};
|