mongodash 2.6.0 → 2.8.0
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/README.md +45 -0
- package/dist/lib/ConcurrentRunner.js +47 -2
- package/dist/lib/ConcurrentRunner.js.map +1 -1
- package/dist/lib/createContinuousLock.js +23 -6
- package/dist/lib/createContinuousLock.js.map +1 -1
- package/dist/lib/cronTasks.js +119 -64
- package/dist/lib/cronTasks.js.map +1 -1
- package/dist/lib/index.js +11 -6
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/reactiveTasks/LeaderElector.js +21 -3
- package/dist/lib/reactiveTasks/LeaderElector.js.map +1 -1
- package/dist/lib/reactiveTasks/MetricsCollector.js +118 -39
- package/dist/lib/reactiveTasks/MetricsCollector.js.map +1 -1
- package/dist/lib/reactiveTasks/ReactiveTaskPlanner.js +66 -31
- package/dist/lib/reactiveTasks/ReactiveTaskPlanner.js.map +1 -1
- package/dist/lib/reactiveTasks/ReactiveTaskRepository.js +19 -1
- package/dist/lib/reactiveTasks/ReactiveTaskRepository.js.map +1 -1
- package/dist/lib/reactiveTasks/ReactiveTaskTypes.js +7 -1
- package/dist/lib/reactiveTasks/ReactiveTaskTypes.js.map +1 -1
- package/dist/lib/reactiveTasks/ReactiveTaskWorker.js +80 -5
- package/dist/lib/reactiveTasks/ReactiveTaskWorker.js.map +1 -1
- package/dist/lib/reactiveTasks/index.js +20 -13
- package/dist/lib/reactiveTasks/index.js.map +1 -1
- package/dist/lib/task-management/OperationalTaskController.js +1 -1
- package/dist/lib/task-management/OperationalTaskController.js.map +1 -1
- package/dist/lib/testing/assertNoReactiveTaskErrors.js +16 -12
- package/dist/lib/testing/assertNoReactiveTaskErrors.js.map +1 -1
- package/dist/lib/testing/index.js +2 -0
- package/dist/lib/testing/index.js.map +1 -1
- package/dist/lib/testing/resolveWhitelistFilter.js +48 -0
- package/dist/lib/testing/resolveWhitelistFilter.js.map +1 -0
- package/dist/lib/testing/waitUntilReactiveTasksIdle.js +17 -46
- package/dist/lib/testing/waitUntilReactiveTasksIdle.js.map +1 -1
- package/dist/types/ConcurrentRunner.d.ts +16 -0
- package/dist/types/createContinuousLock.d.ts +17 -1
- package/dist/types/cronTasks.d.ts +17 -2
- package/dist/types/index.d.ts +2 -2
- package/dist/types/reactiveTasks/LeaderElector.d.ts +15 -1
- package/dist/types/reactiveTasks/MetricsCollector.d.ts +19 -8
- package/dist/types/reactiveTasks/ReactiveTaskPlanner.d.ts +11 -0
- package/dist/types/reactiveTasks/ReactiveTaskRepository.d.ts +10 -1
- package/dist/types/reactiveTasks/ReactiveTaskTypes.d.ts +19 -0
- package/dist/types/reactiveTasks/index.d.ts +8 -2
- package/dist/types/testing/assertNoReactiveTaskErrors.d.ts +4 -4
- package/dist/types/testing/index.d.ts +2 -0
- package/dist/types/testing/resolveWhitelistFilter.d.ts +35 -0
- package/dist/types/testing/waitUntilReactiveTasksIdle.d.ts +7 -13
- package/docs/.vitepress/config.mts +9 -1
- package/docs/cron-tasks.md +130 -1
- package/docs/error-handling.md +156 -0
- package/docs/reactive-tasks/guides.md +1 -1
- package/docs/reactive-tasks/index.md +2 -2
- package/docs/reactive-tasks/monitoring.md +7 -0
- package/docs/reactive-tasks/testing.md +187 -0
- package/docs/testing.md +60 -94
- package/package.json +36 -24
- package/docs/.vitepress/cache/deps/_metadata.json +0 -31
- package/docs/.vitepress/cache/deps/chunk-LE5NDSFD.js +0 -12824
- package/docs/.vitepress/cache/deps/chunk-LE5NDSFD.js.map +0 -7
- package/docs/.vitepress/cache/deps/package.json +0 -3
- package/docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js +0 -4505
- package/docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js.map +0 -7
- package/docs/.vitepress/cache/deps/vitepress___@vueuse_core.js +0 -9731
- package/docs/.vitepress/cache/deps/vitepress___@vueuse_core.js.map +0 -7
- package/docs/.vitepress/cache/deps/vue.js +0 -347
- package/docs/.vitepress/cache/deps/vue.js.map +0 -7
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.assertNoReactiveTaskErrors = assertNoReactiveTaskErrors;
|
|
4
4
|
const reactiveTasks_1 = require("../reactiveTasks");
|
|
5
|
+
const resolveWhitelistFilter_1 = require("./resolveWhitelistFilter");
|
|
5
6
|
/**
|
|
6
7
|
* Asserts that no reactive tasks have failed during the test run.
|
|
7
8
|
* Checks the 'executionHistory' and 'lastError' of tasks in all registered collections.
|
|
@@ -13,20 +14,23 @@ async function assertNoReactiveTaskErrors(options) {
|
|
|
13
14
|
registry = schedulerToUse.getRegistry();
|
|
14
15
|
const entries = registry.getAllEntries();
|
|
15
16
|
const errorsFound = [];
|
|
17
|
+
const hasWhitelist = options.whitelist && options.whitelist.length > 0;
|
|
16
18
|
for (const entry of entries) {
|
|
19
|
+
// If whitelist is active, check if this collection is relevant
|
|
20
|
+
let whitelistFilter = null;
|
|
21
|
+
if (hasWhitelist) {
|
|
22
|
+
const resolution = await (0, resolveWhitelistFilter_1.resolveWhitelistFilter)(options.whitelist, entry.sourceCollection);
|
|
23
|
+
if (resolution === 'skip')
|
|
24
|
+
continue;
|
|
25
|
+
if (resolution !== 'matchAll') {
|
|
26
|
+
whitelistFilter = resolution;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
17
29
|
// Build independent query for each collection
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
$or: [
|
|
21
|
-
{ 'executionHistory.status': 'failed', 'executionHistory.at': { $gte: options.since } },
|
|
22
|
-
// Also check lastError if it happened recently (though executionHistory covers history)
|
|
23
|
-
// We rely on executionHistory for the time-based check.
|
|
24
|
-
],
|
|
30
|
+
const baseQuery = {
|
|
31
|
+
$or: [{ 'executionHistory.status': 'failed', 'executionHistory.at': { $gte: options.since } }],
|
|
25
32
|
};
|
|
26
|
-
|
|
27
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
28
|
-
query.sourceDocId = { $in: options.sourceDocIds };
|
|
29
|
-
}
|
|
33
|
+
const query = whitelistFilter ? { $and: [baseQuery, whitelistFilter] } : baseQuery;
|
|
30
34
|
const tasksWithHistory = await entry.tasksCollection.find(query).toArray();
|
|
31
35
|
for (const taskRecord of tasksWithHistory) {
|
|
32
36
|
if (!taskRecord.executionHistory)
|
|
@@ -39,7 +43,7 @@ async function assertNoReactiveTaskErrors(options) {
|
|
|
39
43
|
if (item.status !== 'failed')
|
|
40
44
|
continue;
|
|
41
45
|
const errorMessage = item.error || 'Unknown error';
|
|
42
|
-
// 3. Check Whitelist
|
|
46
|
+
// 3. Check Whitelist (excludeErrors)
|
|
43
47
|
let isExcluded = false;
|
|
44
48
|
if (options.excludeErrors) {
|
|
45
49
|
for (const pattern of options.excludeErrors) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"assertNoReactiveTaskErrors.js","sourceRoot":"","sources":["../../../src/testing/assertNoReactiveTaskErrors.ts"],"names":[],"mappings":";;AAmCA,
|
|
1
|
+
{"version":3,"file":"assertNoReactiveTaskErrors.js","sourceRoot":"","sources":["../../../src/testing/assertNoReactiveTaskErrors.ts"],"names":[],"mappings":"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"}
|
|
@@ -16,5 +16,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
__exportStar(require("./assertNoReactiveTaskErrors"), exports);
|
|
18
18
|
__exportStar(require("./configureForTesting"), exports);
|
|
19
|
+
__exportStar(require("./resolveWhitelistFilter"), exports);
|
|
20
|
+
__exportStar(require("./waitUntil"), exports);
|
|
19
21
|
__exportStar(require("./waitUntilReactiveTasksIdle"), exports);
|
|
20
22
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/testing/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,+DAA6C;AAC7C,wDAAsC;AACtC,+DAA6C"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/testing/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,+DAA6C;AAC7C,wDAAsC;AACtC,2DAAyC;AACzC,8CAA4B;AAC5B,+DAA6C"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveWhitelistFilter = resolveWhitelistFilter;
|
|
4
|
+
/**
|
|
5
|
+
* Build the `Filter<ReactiveTaskRecord>` for a single registry entry based on
|
|
6
|
+
* the provided whitelist rules. Extracted from `waitUntilReactiveTasksIdle` /
|
|
7
|
+
* `assertNoReactiveTaskErrors` so the two utilities cannot drift.
|
|
8
|
+
*/
|
|
9
|
+
async function resolveWhitelistFilter(whitelist, sourceCollection) {
|
|
10
|
+
const rules = whitelist.filter((rule) => rule.collection === sourceCollection.collectionName);
|
|
11
|
+
if (rules.length === 0) {
|
|
12
|
+
return 'skip';
|
|
13
|
+
}
|
|
14
|
+
const criteria = [];
|
|
15
|
+
for (const rule of rules) {
|
|
16
|
+
let ruleIds = null;
|
|
17
|
+
if (rule.filter) {
|
|
18
|
+
const matchingDocs = (await sourceCollection.find(rule.filter, { projection: { _id: 1 } }).toArray());
|
|
19
|
+
ruleIds = matchingDocs.map((d) => d._id);
|
|
20
|
+
}
|
|
21
|
+
if (ruleIds === null && !rule.task) {
|
|
22
|
+
// Rule covers every document in this collection and every task.
|
|
23
|
+
return 'matchAll';
|
|
24
|
+
}
|
|
25
|
+
// A filter-scoped rule that matched zero source documents can never
|
|
26
|
+
// contribute tasks of its own - drop it even when a task is also
|
|
27
|
+
// specified (the AND of "task=X" and "sourceDocId IN []" is still
|
|
28
|
+
// empty, so emitting that filter just produces useless `$in: []` queries).
|
|
29
|
+
if (ruleIds !== null && ruleIds.length === 0) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
const ruleCriteria = {};
|
|
33
|
+
if (rule.task) {
|
|
34
|
+
ruleCriteria.task = rule.task;
|
|
35
|
+
}
|
|
36
|
+
if (ruleIds !== null) {
|
|
37
|
+
ruleCriteria.sourceDocId = { $in: ruleIds };
|
|
38
|
+
}
|
|
39
|
+
criteria.push(ruleCriteria);
|
|
40
|
+
}
|
|
41
|
+
if (criteria.length === 0) {
|
|
42
|
+
// Rules matched this collection but every rule resolved to an empty
|
|
43
|
+
// document set - nothing to wait for / check.
|
|
44
|
+
return 'skip';
|
|
45
|
+
}
|
|
46
|
+
return { $or: criteria };
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=resolveWhitelistFilter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolveWhitelistFilter.js","sourceRoot":"","sources":["../../../src/testing/resolveWhitelistFilter.ts"],"names":[],"mappings":";;AAqCA,wDAiDC;AAtDD;;;;GAIG;AACI,KAAK,UAAU,sBAAsB,CACxC,SAA0B,EAC1B,gBAAuE;IAEvE,MAAM,KAAK,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU,KAAK,gBAAgB,CAAC,cAAc,CAAC,CAAC;IAC9F,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACrB,OAAO,MAAM,CAAC;IAClB,CAAC;IAED,MAAM,QAAQ,GAAsC,EAAE,CAAC;IAEvD,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACvB,IAAI,OAAO,GAAqB,IAAI,CAAC;QAErC,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YACd,MAAM,YAAY,GAAG,CAAC,MAAM,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,UAAU,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,EAAE,CAAe,CAAC;YACpH,OAAO,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;QAC7C,CAAC;QAED,IAAI,OAAO,KAAK,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;YACjC,gEAAgE;YAChE,OAAO,UAAU,CAAC;QACtB,CAAC;QAED,oEAAoE;QACpE,iEAAiE;QACjE,kEAAkE;QAClE,2EAA2E;QAC3E,IAAI,OAAO,KAAK,IAAI,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC3C,SAAS;QACb,CAAC;QAED,MAAM,YAAY,GAA+B,EAAE,CAAC;QACpD,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;YACZ,YAAY,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;QAClC,CAAC;QACD,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;YACnB,YAAY,CAAC,WAAW,GAAG,EAAE,GAAG,EAAE,OAA8C,EAAE,CAAC;QACvF,CAAC;QACD,QAAQ,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;IAChC,CAAC;IAED,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxB,oEAAoE;QACpE,8CAA8C;QAC9C,OAAO,MAAM,CAAC;IAClB,CAAC;IAED,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,CAAC;AAC7B,CAAC"}
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.waitUntilReactiveTasksIdle = waitUntilReactiveTasksIdle;
|
|
4
4
|
const _debug = require("debug");
|
|
5
5
|
const reactiveTasks_1 = require("../reactiveTasks");
|
|
6
|
+
const resolveWhitelistFilter_1 = require("./resolveWhitelistFilter");
|
|
6
7
|
const waitUntil_1 = require("./waitUntil");
|
|
7
8
|
const debug = _debug('mongodash:testing');
|
|
8
9
|
async function waitUntilReactiveTasksIdle(customOptions = {}) {
|
|
@@ -13,16 +14,22 @@ async function waitUntilReactiveTasksIdle(customOptions = {}) {
|
|
|
13
14
|
const planner = reactiveTasks_1._scheduler.taskPlannerInstance;
|
|
14
15
|
const runner = reactiveTasks_1._scheduler.concurrentRunnerInstance;
|
|
15
16
|
const registry = reactiveTasks_1._scheduler.getRegistry();
|
|
16
|
-
// --- 1. Global Checks
|
|
17
|
-
//
|
|
17
|
+
// --- 1. Global Checks ---
|
|
18
|
+
// The planner buffer check is kept in whitelist mode too: change-stream
|
|
19
|
+
// events for *our* collections land there before task records exist,
|
|
20
|
+
// and skipping the check altogether would let us return idle before
|
|
21
|
+
// the events have been turned into rows the DB check below can see.
|
|
22
|
+
// The worker check is only applied globally - in whitelist mode
|
|
23
|
+
// other tests' workers must not block us.
|
|
18
24
|
if (planner && !planner.isEmpty) {
|
|
19
25
|
debug('Planner not empty');
|
|
20
26
|
return false;
|
|
21
27
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
28
|
+
if (!hasWhitelist) {
|
|
29
|
+
if (runner && runner.activeWorkers > 0) {
|
|
30
|
+
debug(`Active workers: ${runner.activeWorkers}`);
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
26
33
|
}
|
|
27
34
|
// --- 2. Check Database ---
|
|
28
35
|
const entries = registry.getAllEntries();
|
|
@@ -31,47 +38,11 @@ async function waitUntilReactiveTasksIdle(customOptions = {}) {
|
|
|
31
38
|
// If whitelisting is active, we only check tasks that match the whitelist
|
|
32
39
|
let whitelistFilter = null;
|
|
33
40
|
if (hasWhitelist) {
|
|
34
|
-
const
|
|
35
|
-
if (
|
|
41
|
+
const resolution = await (0, resolveWhitelistFilter_1.resolveWhitelistFilter)(customOptions.whitelist, entry.sourceCollection);
|
|
42
|
+
if (resolution === 'skip')
|
|
36
43
|
continue;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
let matchAll = false;
|
|
40
|
-
for (const rule of rules) {
|
|
41
|
-
let ruleIds = null;
|
|
42
|
-
if (rule.filter) {
|
|
43
|
-
// If we have a filter, we need to find which docs match it.
|
|
44
|
-
// We can't filter tasks directly by source properties efficiently without joining,
|
|
45
|
-
// so we find the matching source docs first.
|
|
46
|
-
const matchingDocs = (await entry.sourceCollection.find(rule.filter, { projection: { _id: 1 } }).toArray());
|
|
47
|
-
ruleIds = matchingDocs.map((d) => d._id);
|
|
48
|
-
}
|
|
49
|
-
if (ruleIds === null && !rule.task) {
|
|
50
|
-
// One rule validates 'all', so we wait for everything in this collection
|
|
51
|
-
matchAll = true;
|
|
52
|
-
break;
|
|
53
|
-
}
|
|
54
|
-
if (rule.task) {
|
|
55
|
-
criteria.push({ task: rule.task });
|
|
56
|
-
}
|
|
57
|
-
if (ruleIds !== null) {
|
|
58
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
59
|
-
criteria.push({ sourceDocId: { $in: ruleIds } });
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
if (!matchAll) {
|
|
63
|
-
if (criteria.length > 0) {
|
|
64
|
-
whitelistFilter = { $or: criteria };
|
|
65
|
-
}
|
|
66
|
-
else {
|
|
67
|
-
// Whitelist is active, but we have rules that result in effectively "nothing"
|
|
68
|
-
// (e.g. filter returned no docs).
|
|
69
|
-
// If we have NO criteria and NO matchAll, it implies we wait for nothing on this collection?
|
|
70
|
-
// Or should we treat it as blocking?
|
|
71
|
-
// If filter didn't match any doc, then we effectively wait for nothing for that rule.
|
|
72
|
-
// If ALL rules resulted in nothing, we continue to next entry.
|
|
73
|
-
continue;
|
|
74
|
-
}
|
|
44
|
+
if (resolution !== 'matchAll') {
|
|
45
|
+
whitelistFilter = resolution;
|
|
75
46
|
}
|
|
76
47
|
}
|
|
77
48
|
const stableThresholdMs = (options.timeoutMs || 0) + (options.stabilityDurationMs || 0) + 100;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"waitUntilReactiveTasksIdle.js","sourceRoot":"","sources":["../../../src/testing/waitUntilReactiveTasksIdle.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"waitUntilReactiveTasksIdle.js","sourceRoot":"","sources":["../../../src/testing/waitUntilReactiveTasksIdle.ts"],"names":[],"mappings":";;AA+BA,gEA0EC;AAzGD,gCAAgC;AAEhC,oDAAkE;AAClE,qEAAiF;AACjF,2CAA0D;AAE1D,MAAM,KAAK,GAAG,MAAM,CAAC,mBAAmB,CAAC,CAAC;AAyBnC,KAAK,UAAU,0BAA0B,CAAC,gBAAmD,EAAE;IAClG,MAAM,OAAO,mBACT,SAAS,EAAE,KAAK,EAChB,cAAc,EAAE,EAAE,EAClB,mBAAmB,EAAE,GAAG,IACrB,aAAa,CACnB,CAAC;IAEF,MAAM,YAAY,GAAG,aAAa,CAAC,SAAS,IAAI,aAAa,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,CAAC;IAEnF,MAAM,IAAA,qBAAS,EAAC,KAAK,IAAI,EAAE;QACvB,6BAA6B;QAC7B,MAAM,OAAO,GAAG,0BAAU,CAAC,mBAAmB,CAAC;QAC/C,MAAM,MAAM,GAAG,0BAAU,CAAC,wBAAwB,CAAC;QACnD,MAAM,QAAQ,GAAG,0BAAU,CAAC,WAAW,EAAE,CAAC;QAE1C,2BAA2B;QAC3B,wEAAwE;QACxE,qEAAqE;QACrE,oEAAoE;QACpE,oEAAoE;QACpE,gEAAgE;QAChE,0CAA0C;QAC1C,IAAI,OAAO,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;YAC9B,KAAK,CAAC,mBAAmB,CAAC,CAAC;YAC3B,OAAO,KAAK,CAAC;QACjB,CAAC;QACD,IAAI,CAAC,YAAY,EAAE,CAAC;YAChB,IAAI,MAAM,IAAI,MAAM,CAAC,aAAa,GAAG,CAAC,EAAE,CAAC;gBACrC,KAAK,CAAC,mBAAmB,MAAM,CAAC,aAAa,EAAE,CAAC,CAAC;gBACjD,OAAO,KAAK,CAAC;YACjB,CAAC;QACL,CAAC;QAED,4BAA4B;QAC5B,MAAM,OAAO,GAAG,QAAQ,CAAC,aAAa,EAAE,CAAC;QAEzC,wEAAwE;QACxE,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;YAC1B,0EAA0E;YAC1E,IAAI,eAAe,GAAsC,IAAI,CAAC;YAE9D,IAAI,YAAY,EAAE,CAAC;gBACf,MAAM,UAAU,GAAG,MAAM,IAAA,+CAAsB,EAAC,aAAa,CAAC,SAAU,EAAE,KAAK,CAAC,gBAAgB,CAAC,CAAC;gBAClG,IAAI,UAAU,KAAK,MAAM;oBAAE,SAAS;gBACpC,IAAI,UAAU,KAAK,UAAU,EAAE,CAAC;oBAC5B,eAAe,GAAG,UAAU,CAAC;gBACjC,CAAC;YACL,CAAC;YAED,MAAM,iBAAiB,GAAG,CAAC,OAAO,CAAC,SAAS,IAAI,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,mBAAmB,IAAI,CAAC,CAAC,GAAG,GAAG,CAAC;YAE9F,MAAM,SAAS,GAA+B;gBAC1C,GAAG,EAAE;oBACD,EAAE,MAAM,EAAE,EAAE,GAAG,EAAE,CAAC,YAAY,EAAE,kBAAkB,CAAC,EAAE,EAAE;oBACvD;wBACI,MAAM,EAAE,SAAS;wBACjB,GAAG,EAAE,CAAC,EAAE,SAAS,EAAE,EAAE,IAAI,EAAE,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,iBAAiB,CAAC,EAAE,EAAE,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;qBAChG;iBACJ;aACJ,CAAC;YAEF,MAAM,KAAK,GAA+B,eAAe,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,SAAS,EAAE,eAAe,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;YAE/G,MAAM,KAAK,GAAG,MAAM,KAAK,CAAC,eAAe,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC;YAEhE,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;gBACZ,KAAK,CAAC,cAAc,KAAK,CAAC,eAAe,CAAC,cAAc,QAAQ,KAAK,eAAe,CAAC,CAAC;gBACtF,OAAO,KAAK,CAAC;YACjB,CAAC;QACL,CAAC;QAED,OAAO,IAAI,CAAC;IAChB,CAAC,EAAE,OAAO,CAAC,CAAC;AAChB,CAAC"}
|
|
@@ -23,6 +23,22 @@ export declare class ConcurrentRunner {
|
|
|
23
23
|
start(tryRunATask: TryRunATaskCallback): void;
|
|
24
24
|
stop(): Promise<void>;
|
|
25
25
|
speedUp(sourceName: string): void;
|
|
26
|
+
/**
|
|
27
|
+
* Override the back-off schedule so the next poll for `sourceName`
|
|
28
|
+
* happens at approximately `runAt` (a millisecond epoch timestamp).
|
|
29
|
+
* Intended for callers that already know when their next unit of work
|
|
30
|
+
* is due - e.g. cron scheduling an hour out - to skip wasted polls.
|
|
31
|
+
*
|
|
32
|
+
* - `runAt` must be a finite number; non-finite values are ignored.
|
|
33
|
+
* - If `state.nextRunAt` is already at or before `now`, the write is
|
|
34
|
+
* skipped: something more urgent (usually {@link speedUp}) has
|
|
35
|
+
* already signalled an immediate poll and we must not push it
|
|
36
|
+
* back out. The worker picks up the signal on its next iteration.
|
|
37
|
+
* - Otherwise `nextRunAt` is overwritten with `runAt` (even if `runAt`
|
|
38
|
+
* is in the past - that behaves like {@link speedUp}).
|
|
39
|
+
* - Back-off is reset so a subsequent wake-up fires at `minPollMs`.
|
|
40
|
+
*/
|
|
41
|
+
setNextRunAt(sourceName: string, runAt: number): void;
|
|
26
42
|
updateAllSources(options: Partial<SourceOptions>): void;
|
|
27
43
|
private runWorker;
|
|
28
44
|
private prolongNextRun;
|
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
import { Collection, ObjectId } from 'mongodb';
|
|
2
2
|
type StopContinuousLock = () => Promise<void>;
|
|
3
|
+
export interface CreateContinuousLockOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Initial value of the lock property written by whoever acquired the lock.
|
|
6
|
+
* When provided, every renewal becomes a compare-and-swap: the update only
|
|
7
|
+
* succeeds if the lock still carries the previously-written value. If another
|
|
8
|
+
* actor has taken over the lock in the meantime, `onLockLost` is invoked and
|
|
9
|
+
* no further renewals are attempted. This prevents a slow renewal from
|
|
10
|
+
* accidentally extending a lock that has already been stolen.
|
|
11
|
+
*/
|
|
12
|
+
expectedInitialValue?: unknown;
|
|
13
|
+
/**
|
|
14
|
+
* Invoked once when CAS detects we no longer own the lock. Only fires when
|
|
15
|
+
* `expectedInitialValue` is set. Renewals stop after this callback.
|
|
16
|
+
*/
|
|
17
|
+
onLockLost?: () => void;
|
|
18
|
+
}
|
|
3
19
|
export declare function createContinuousLock<DocumentType extends {
|
|
4
20
|
_id: string | ObjectId;
|
|
5
|
-
}>(collection: Collection<DocumentType>, documentId: DocumentType['_id'], lockProperty: keyof DocumentType, lockTime: number): StopContinuousLock;
|
|
21
|
+
}>(collection: Collection<DocumentType>, documentId: DocumentType['_id'], lockProperty: keyof DocumentType, lockTime: number, options?: CreateContinuousLockOptions): StopContinuousLock;
|
|
6
22
|
export {};
|
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
import { CronExpressionOptions } from 'cron-parser';
|
|
2
2
|
export interface InitOptions {
|
|
3
3
|
runCronTasks: boolean;
|
|
4
|
+
/**
|
|
5
|
+
* Maximum number of cron tasks this instance will execute in parallel.
|
|
6
|
+
*
|
|
7
|
+
* The default of `1` preserves the historical behaviour: one task is
|
|
8
|
+
* processed at a time per instance. Raise it when you have many
|
|
9
|
+
* independent cron tasks and want to avoid head-of-line blocking (a
|
|
10
|
+
* long-running task delaying unrelated ones).
|
|
11
|
+
*
|
|
12
|
+
* Tasks with the same id are always serialised via the per-task lock
|
|
13
|
+
* (`lockedTill`), so raising this does not cause a single task to run
|
|
14
|
+
* twice in parallel.
|
|
15
|
+
*/
|
|
16
|
+
cronTaskConcurrency: number;
|
|
4
17
|
cronExpressionParserOptions: CronExpressionOptions;
|
|
5
18
|
cronTaskCaller: CronTaskCaller;
|
|
6
19
|
cronTaskFilter: CronTaskFilter;
|
|
@@ -64,8 +77,10 @@ export declare function cronTask(taskId: TaskId, interval: Interval, task: TaskF
|
|
|
64
77
|
*/
|
|
65
78
|
export declare function getCronTasksList(query?: CronTaskQuery): Promise<CronPagedResult<CronTaskRecord>>;
|
|
66
79
|
/**
|
|
67
|
-
*
|
|
68
|
-
*
|
|
80
|
+
* @deprecated Alias for {@link scheduleCronTaskImmediately}. Prefer that name for
|
|
81
|
+
* clarity - it describes exactly what happens (the task is scheduled to run on
|
|
82
|
+
* the next polling tick, not necessarily this very millisecond). This alias will
|
|
83
|
+
* be removed in a future major version.
|
|
69
84
|
*/
|
|
70
85
|
export declare function triggerCronTask(taskId: TaskId): Promise<void>;
|
|
71
86
|
/**
|
package/dist/types/index.d.ts
CHANGED
|
@@ -5,12 +5,12 @@ import { InitOptions as GetMongoClientInitOptions } from './getMongoClient';
|
|
|
5
5
|
import { OnError } from './OnError';
|
|
6
6
|
import { OnInfo } from './OnInfo';
|
|
7
7
|
import { InitOptions as ReactiveTasksInitOptions } from './reactiveTasks';
|
|
8
|
-
export { CODE_CRON_TASK_FAILED, CODE_CRON_TASK_FINISHED, CODE_CRON_TASK_SCHEDULED, CODE_CRON_TASK_STARTED, CronPagedResult, cronTask, CronTaskQuery, CronTaskRecord, CronTaskStatus, getCronTasksList, Interval, runCronTask, scheduleCronTaskImmediately, startCronTasks, stopCronTasks, TaskFunction, TaskId, triggerCronTask, } from './cronTasks';
|
|
8
|
+
export { CODE_CRON_TASK_FAILED, CODE_CRON_TASK_FINISHED, CODE_CRON_TASK_SCHEDULED, CODE_CRON_TASK_STARTED, CronPagedResult, cronTask, CronTaskQuery, CronTaskRecord, CronTaskStatus, getCronTasksList, getRegisteredCronTaskIds, Interval, runCronTask, scheduleCronTaskImmediately, startCronTasks, stopCronTasks, TaskFunction, TaskId, triggerCronTask, } from './cronTasks';
|
|
9
9
|
export { getCollection } from './getCollection';
|
|
10
10
|
export { getMongoClient } from './getMongoClient';
|
|
11
11
|
export { OnError } from './OnError';
|
|
12
12
|
export { processInBatches, ProcessInBatchesOptions, ProcessInBatchesResult } from './processInBatches';
|
|
13
|
-
export { CODE_REACTIVE_TASK_FAILED, CODE_REACTIVE_TASK_FINISHED, CODE_REACTIVE_TASK_LEADER_LOCK_LOST, CODE_REACTIVE_TASK_PLANNER_RECONCILIATION_FINISHED, CODE_REACTIVE_TASK_PLANNER_RECONCILIATION_STARTED, CODE_REACTIVE_TASK_PLANNER_STARTED, CODE_REACTIVE_TASK_PLANNER_STOPPED, CODE_REACTIVE_TASK_PLANNER_STREAM_ERROR, CODE_REACTIVE_TASK_STARTED, countReactiveTasks, getPrometheusMetrics, getReactiveTasks, reactiveTask, ReactiveTask, ReactiveTaskHandler, retryReactiveTasks, startReactiveTasks, stopReactiveTasks, TaskConditionFailedError, _scheduler, } from './reactiveTasks';
|
|
13
|
+
export { CODE_REACTIVE_TASK_CLEANUP, CODE_REACTIVE_TASK_FAILED, CODE_REACTIVE_TASK_FINISHED, CODE_REACTIVE_TASK_INITIALIZED, CODE_REACTIVE_TASK_LEADER_LOCK_LOST, CODE_REACTIVE_TASK_LOCK_LOST, CODE_REACTIVE_TASK_PLANNER_RECONCILIATION_FINISHED, CODE_REACTIVE_TASK_PLANNER_RECONCILIATION_STARTED, CODE_REACTIVE_TASK_PLANNER_STARTED, CODE_REACTIVE_TASK_PLANNER_STOPPED, CODE_REACTIVE_TASK_PLANNER_STREAM_ERROR, CODE_REACTIVE_TASK_STARTED, countReactiveTasks, getPrometheusMetrics, getReactiveTasks, PagedResult, PaginationOptions, reactiveTask, ReactiveTask, ReactiveTaskHandler, ReactiveTaskQuery, ReactiveTaskRecord, ReactiveTaskStatus, retryReactiveTasks, startReactiveTasks, stopReactiveTasks, TaskConditionFailedError, _scheduler, } from './reactiveTasks';
|
|
14
14
|
export { OperationalTaskController, serveDashboard } from './task-management';
|
|
15
15
|
export * from './testing';
|
|
16
16
|
export { isLockAlreadyAcquiredError, LockAlreadyAcquiredError, withLock, WithLockOptions } from './withLock';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { GlobalsCollection } from '../globalsCollection';
|
|
2
|
-
import { OnInfo } from '../OnInfo';
|
|
3
2
|
import { OnError } from '../OnError';
|
|
3
|
+
import { OnInfo } from '../OnInfo';
|
|
4
4
|
export interface LeaderElectorCallbacks {
|
|
5
5
|
onBecomeLeader: () => Promise<void>;
|
|
6
6
|
onLoseLeader: () => Promise<void>;
|
|
@@ -35,6 +35,20 @@ export declare class LeaderElector {
|
|
|
35
35
|
get isLeader(): boolean;
|
|
36
36
|
start(): Promise<void>;
|
|
37
37
|
stop(): Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* Give up leadership locally. The DB lock is NOT released - the next
|
|
40
|
+
* heartbeat will likely re-acquire it (unless another instance raced
|
|
41
|
+
* in). onLoseLeader is fired asynchronously so callers (e.g. the
|
|
42
|
+
* scheduler wiring this to a flush-failure path) get a clean
|
|
43
|
+
* planner.stop() before the next heartbeat restarts it, rather than
|
|
44
|
+
* starting a new planner on top of a live one.
|
|
45
|
+
*
|
|
46
|
+
* Note: the follow-up onBecomeLeader that fires after a forced loss
|
|
47
|
+
* looks identical to a real leader election and will increment
|
|
48
|
+
* reactive_tasks_leader_elections_total; see the event codes
|
|
49
|
+
* CODE_REACTIVE_TASK_PLANNER_STREAM_ERROR and the flush-failure
|
|
50
|
+
* counter to disambiguate "real" flapping from restart-driven ones.
|
|
51
|
+
*/
|
|
38
52
|
forceLoseLeader(): void;
|
|
39
53
|
private runLeaderElectionLoop;
|
|
40
54
|
private tryAcquireLock;
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import type { Registry } from 'prom-client';
|
|
2
2
|
import { GlobalsCollection } from '../globalsCollection';
|
|
3
3
|
import { OnError } from '../OnError';
|
|
4
|
-
import { OnInfo } from '../OnInfo';
|
|
5
4
|
import { LeaderElector } from './LeaderElector';
|
|
6
|
-
import { ReactiveTaskPlanner } from './ReactiveTaskPlanner';
|
|
7
5
|
import { ReactiveTaskRegistry } from './ReactiveTaskRegistry';
|
|
8
6
|
import { ReactiveTaskSchedulerOptions } from './ReactiveTaskTypes';
|
|
9
7
|
/**
|
|
@@ -15,7 +13,12 @@ import { ReactiveTaskSchedulerOptions } from './ReactiveTaskTypes';
|
|
|
15
13
|
* - **local**: Returns metrics from THIS instance only.
|
|
16
14
|
* Use when Prometheus scrapes each pod individually (K8s Pod Monitors).
|
|
17
15
|
*
|
|
18
|
-
* Global stats (queue depth, lag
|
|
16
|
+
* Global stats (queue depth, lag, change-stream lag, reconciliation timestamp)
|
|
17
|
+
* are computed by the Leader. In `cluster` mode the Leader also pushes these
|
|
18
|
+
* values into the registry document on each push interval so a scrape that
|
|
19
|
+
* lands on a Follower still returns a complete metrics view - bounded-stale
|
|
20
|
+
* at `GLOBAL_STATS_STALE_MULTIPLIER * pushIntervalMs` (2x by default,
|
|
21
|
+
* i.e. a single missed push is tolerated).
|
|
19
22
|
*/
|
|
20
23
|
export declare class MetricsCollector {
|
|
21
24
|
private enabled;
|
|
@@ -24,17 +27,19 @@ export declare class MetricsCollector {
|
|
|
24
27
|
private readonly registry;
|
|
25
28
|
private readonly globalsCollection;
|
|
26
29
|
private readonly leaderElector;
|
|
27
|
-
private readonly onInfo;
|
|
28
30
|
private readonly onError;
|
|
29
31
|
private promClientModule;
|
|
30
32
|
private localPromRegistry?;
|
|
31
33
|
private globalStatsRegistry?;
|
|
32
34
|
private metricDuration?;
|
|
33
35
|
private metricRetries?;
|
|
36
|
+
private metricLeaderElections?;
|
|
37
|
+
private metricLockLost?;
|
|
38
|
+
private metricStreamErrors?;
|
|
39
|
+
private metricFlushFailures?;
|
|
34
40
|
private pushInterval?;
|
|
35
41
|
private queueMetricsPromise;
|
|
36
|
-
|
|
37
|
-
constructor(instanceId: string, registry: ReactiveTaskRegistry, globalsCollection: GlobalsCollection, leaderElector: LeaderElector, options: ReactiveTaskSchedulerOptions['monitoring'], onInfo?: OnInfo, onError?: OnError);
|
|
42
|
+
constructor(instanceId: string, registry: ReactiveTaskRegistry, globalsCollection: GlobalsCollection, leaderElector: LeaderElector, options: ReactiveTaskSchedulerOptions['monitoring'], onError?: OnError);
|
|
38
43
|
private initPrometheus;
|
|
39
44
|
private initLocalMetrics;
|
|
40
45
|
private initGlobalStatsMetrics;
|
|
@@ -42,11 +47,17 @@ export declare class MetricsCollector {
|
|
|
42
47
|
stop(): void;
|
|
43
48
|
recordTaskExecution(task: string, status: 'success' | 'failed', durationMs: number): void;
|
|
44
49
|
recordRetry(task: string): void;
|
|
50
|
+
recordLeaderElection(): void;
|
|
51
|
+
recordLockLost(task: string): void;
|
|
52
|
+
recordStreamError(): void;
|
|
53
|
+
recordFlushFailure(): void;
|
|
45
54
|
getPrometheusMetrics(): Promise<Registry | null>;
|
|
46
55
|
/**
|
|
47
56
|
* Returns aggregated metrics from ALL instances.
|
|
48
57
|
* Fetches other instances' metrics from DB and merges with fresh local metrics.
|
|
49
|
-
*
|
|
58
|
+
* Global stats: leader computes fresh; followers read the leader's last
|
|
59
|
+
* push from the registry doc (so a scrape that hits a follower still
|
|
60
|
+
* returns queue depth / lag / reconciliation / stream-lag gauges).
|
|
50
61
|
*/
|
|
51
62
|
private getClusterMetrics;
|
|
52
63
|
/**
|
|
@@ -54,7 +65,7 @@ export declare class MetricsCollector {
|
|
|
54
65
|
* Leader also includes global stats.
|
|
55
66
|
*/
|
|
56
67
|
private getLocalMetrics;
|
|
57
|
-
private
|
|
68
|
+
private fetchClusterStateFromDb;
|
|
58
69
|
private getLocalMetricsAsJson;
|
|
59
70
|
private getGlobalStatsAsJson;
|
|
60
71
|
private triggerGlobalStatsCollection;
|
|
@@ -6,6 +6,15 @@ import { ReactiveTaskRegistry } from './ReactiveTaskRegistry';
|
|
|
6
6
|
export interface PlannerCallbacks {
|
|
7
7
|
onStreamError: () => void;
|
|
8
8
|
onTaskPlanned: (tasksCollectionName: string, debounceMs: number) => void;
|
|
9
|
+
/** Fired when a batch flush fails. Records the metric and should trigger a planner restart. */
|
|
10
|
+
onFlushFailure?: () => void;
|
|
11
|
+
/**
|
|
12
|
+
* Fired when the planner needs to restart due to a flush failure (distinct from a
|
|
13
|
+
* real change-stream error). Callers should trigger a leader-election cycle here
|
|
14
|
+
* instead of reacting to `onStreamError`, so flush failures don't pollute the
|
|
15
|
+
* stream-error metric.
|
|
16
|
+
*/
|
|
17
|
+
onRequestRestart?: () => void;
|
|
9
18
|
}
|
|
10
19
|
/**
|
|
11
20
|
* Responsible for listening to MongoDB Change Stream events and planning tasks.
|
|
@@ -31,6 +40,7 @@ export declare class ReactiveTaskPlanner {
|
|
|
31
40
|
private batchFlushTimer;
|
|
32
41
|
private batchFirstEventTime;
|
|
33
42
|
private isFlushing;
|
|
43
|
+
private lastFlushFailed;
|
|
34
44
|
private metaDocId;
|
|
35
45
|
private lastClusterTime;
|
|
36
46
|
private ops;
|
|
@@ -59,6 +69,7 @@ export declare class ReactiveTaskPlanner {
|
|
|
59
69
|
private groupEventsByCollection;
|
|
60
70
|
private processDeletions;
|
|
61
71
|
private executeUpsertOperations;
|
|
72
|
+
private throwOnAnyRejection;
|
|
62
73
|
private handleStreamError;
|
|
63
74
|
private checkEvolutionStrategies;
|
|
64
75
|
private checkTriggerEvolution;
|
|
@@ -24,11 +24,20 @@ export declare class ReactiveTaskRepository<T extends Document> {
|
|
|
24
24
|
findAndLockNextTask(taskDefs: ReactiveTaskInternal<T>[], options: {
|
|
25
25
|
visibilityTimeoutMs: number;
|
|
26
26
|
}): Promise<ReactiveTaskRecord<T> | null>;
|
|
27
|
+
/**
|
|
28
|
+
* Finalize a task record (success or failure). Returns `true` when the
|
|
29
|
+
* update matched the record, `false` when it did not - which in
|
|
30
|
+
* practice means another worker has since re-claimed the task (its
|
|
31
|
+
* startedAt no longer matches) and this call was a no-op.
|
|
32
|
+
*
|
|
33
|
+
* Callers that care about the distinction (e.g. to suppress success /
|
|
34
|
+
* failure metrics for a stolen task) should inspect the return value.
|
|
35
|
+
*/
|
|
27
36
|
finalizeTask(taskRecord: ReactiveTaskRecord<T>, strategy: ReactiveTaskRetryStrategy, error?: Error, debounceMs?: number, executionStats?: {
|
|
28
37
|
durationMs: number;
|
|
29
38
|
}, executionHistoryLimit?: number, options?: {
|
|
30
39
|
session?: import('mongodb').ClientSession;
|
|
31
|
-
}): Promise<
|
|
40
|
+
}): Promise<boolean>;
|
|
32
41
|
deferTask(taskRecord: ReactiveTaskRecord<T>, delay: number | Date): Promise<void>;
|
|
33
42
|
executeBulkWrite(operations: Parameters<Collection<ReactiveTaskRecord<T>>['bulkWrite']>[0], options?: CompatibleBulkWriteOptions): Promise<void>;
|
|
34
43
|
findTasks(filter: Filter<ReactiveTaskRecord<T>>, options?: {
|
|
@@ -103,6 +103,19 @@ export interface RegistryDocument {
|
|
|
103
103
|
lastSeen: Date | string;
|
|
104
104
|
metrics: unknown;
|
|
105
105
|
}>;
|
|
106
|
+
/**
|
|
107
|
+
* Cluster-wide stats (queue depth, global lag, change-stream lag,
|
|
108
|
+
* last reconciliation). Written only by the current Leader on each
|
|
109
|
+
* push; non-leaders read it so that a follower-served scrape in
|
|
110
|
+
* `cluster` mode can return a complete metrics view. A single field
|
|
111
|
+
* (not per-instance) prevents double-counting across leader
|
|
112
|
+
* transitions - the next leader overwrites on its first push.
|
|
113
|
+
*/
|
|
114
|
+
globalStats?: {
|
|
115
|
+
updatedAt: Date | string;
|
|
116
|
+
leaderId: string;
|
|
117
|
+
metrics: unknown;
|
|
118
|
+
};
|
|
106
119
|
}
|
|
107
120
|
/**
|
|
108
121
|
* Error thrown when `getDocument` fails because the document no longer matches the filter
|
|
@@ -336,6 +349,7 @@ export interface ReactiveTaskCaller {
|
|
|
336
349
|
export declare const CODE_REACTIVE_TASK_STARTED = "reactiveTaskStarted";
|
|
337
350
|
export declare const CODE_REACTIVE_TASK_FINISHED = "reactiveTaskFinished";
|
|
338
351
|
export declare const CODE_REACTIVE_TASK_FAILED = "reactiveTaskFailed";
|
|
352
|
+
export declare const CODE_REACTIVE_TASK_LOCK_LOST = "reactiveTaskLockLost";
|
|
339
353
|
export declare const CODE_REACTIVE_TASK_PLANNER_STARTED = "reactiveTaskPlannerStarted";
|
|
340
354
|
export declare const CODE_REACTIVE_TASK_PLANNER_STOPPED = "reactiveTaskPlannerStopped";
|
|
341
355
|
export declare const CODE_REACTIVE_TASK_PLANNER_RECONCILIATION_STARTED = "reactiveTaskPlannerReconciliationStarted";
|
|
@@ -345,6 +359,11 @@ export declare const CODE_REACTIVE_TASK_LEADER_LOCK_LOST = "reactiveTaskLeaderLo
|
|
|
345
359
|
export declare const CODE_REACTIVE_TASK_INITIALIZED = "reactiveTaskInitialized";
|
|
346
360
|
export declare const CODE_REACTIVE_TASK_CLEANUP = "reactiveTaskCleanup";
|
|
347
361
|
export declare const CODE_MANUAL_TRIGGER = "manualTrigger";
|
|
362
|
+
/**
|
|
363
|
+
* @internal
|
|
364
|
+
* Document id used by the planner for its meta document. Exposed for the
|
|
365
|
+
* dashboard and advanced tooling - not part of the public API contract.
|
|
366
|
+
*/
|
|
348
367
|
export declare const REACTIVE_TASK_META_DOC_ID = "_mongodash_planner_meta";
|
|
349
368
|
/**
|
|
350
369
|
* Filter for querying tasks.
|
|
@@ -6,7 +6,13 @@ import { ReactiveTaskManager } from './ReactiveTaskManager';
|
|
|
6
6
|
import { ReactiveTaskPlanner } from './ReactiveTaskPlanner';
|
|
7
7
|
import { ReactiveTaskRegistry } from './ReactiveTaskRegistry';
|
|
8
8
|
import { PagedResult, PaginationOptions, ReactiveTask, ReactiveTaskQuery, ReactiveTaskRecord, ReactiveTaskSchedulerOptions } from './ReactiveTaskTypes';
|
|
9
|
-
export { CODE_REACTIVE_TASK_CLEANUP, CODE_REACTIVE_TASK_FAILED, CODE_REACTIVE_TASK_FINISHED, CODE_REACTIVE_TASK_INITIALIZED, CODE_REACTIVE_TASK_LEADER_LOCK_LOST, CODE_REACTIVE_TASK_PLANNER_RECONCILIATION_FINISHED, CODE_REACTIVE_TASK_PLANNER_RECONCILIATION_STARTED, CODE_REACTIVE_TASK_PLANNER_STARTED, CODE_REACTIVE_TASK_PLANNER_STOPPED, CODE_REACTIVE_TASK_PLANNER_STREAM_ERROR, CODE_REACTIVE_TASK_STARTED, PagedResult, PaginationOptions, ReactiveTask, ReactiveTaskCaller, ReactiveTaskFilter, ReactiveTaskHandler, ReactiveTaskQuery, ReactiveTaskRecord, ReactiveTaskSchedulerOptions, ReactiveTaskStatus, REACTIVE_TASK_META_DOC_ID, TaskConditionFailedError, } from './ReactiveTaskTypes';
|
|
9
|
+
export { CODE_REACTIVE_TASK_CLEANUP, CODE_REACTIVE_TASK_FAILED, CODE_REACTIVE_TASK_FINISHED, CODE_REACTIVE_TASK_INITIALIZED, CODE_REACTIVE_TASK_LEADER_LOCK_LOST, CODE_REACTIVE_TASK_LOCK_LOST, CODE_REACTIVE_TASK_PLANNER_RECONCILIATION_FINISHED, CODE_REACTIVE_TASK_PLANNER_RECONCILIATION_STARTED, CODE_REACTIVE_TASK_PLANNER_STARTED, CODE_REACTIVE_TASK_PLANNER_STOPPED, CODE_REACTIVE_TASK_PLANNER_STREAM_ERROR, CODE_REACTIVE_TASK_STARTED, PagedResult, PaginationOptions, ReactiveTask, ReactiveTaskCaller, ReactiveTaskFilter, ReactiveTaskHandler, ReactiveTaskQuery, ReactiveTaskRecord, ReactiveTaskSchedulerOptions, ReactiveTaskStatus, REACTIVE_TASK_META_DOC_ID, TaskConditionFailedError, } from './ReactiveTaskTypes';
|
|
10
|
+
/**
|
|
11
|
+
* @internal
|
|
12
|
+
* Exported only for the built-in OperationalTaskController / dashboard bridge
|
|
13
|
+
* and for advanced testing. Not part of the public API contract: fields and
|
|
14
|
+
* methods on the scheduler instance can change between minor versions.
|
|
15
|
+
*/
|
|
10
16
|
export { scheduler as _scheduler };
|
|
11
17
|
export type InitOptions = {
|
|
12
18
|
globalsCollection: GlobalsCollection;
|
|
@@ -54,7 +60,7 @@ export declare class ReactiveTaskScheduler {
|
|
|
54
60
|
debounce?: number;
|
|
55
61
|
}): void;
|
|
56
62
|
get forceDebounce(): number | string | undefined;
|
|
57
|
-
addTask(taskDef: ReactiveTask<
|
|
63
|
+
addTask<T extends Document>(taskDef: ReactiveTask<T>): Promise<void>;
|
|
58
64
|
/**
|
|
59
65
|
* Starts the entire system - leader election and workers.
|
|
60
66
|
*/
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ReactiveTaskScheduler } from '../reactiveTasks';
|
|
2
|
+
import { WhitelistRule } from './resolveWhitelistFilter';
|
|
2
3
|
export interface AssertNoReactiveTaskErrorsOptions {
|
|
3
4
|
/**
|
|
4
5
|
* Check for errors occurring after this time.
|
|
@@ -6,11 +7,10 @@ export interface AssertNoReactiveTaskErrorsOptions {
|
|
|
6
7
|
*/
|
|
7
8
|
since: Date;
|
|
8
9
|
/**
|
|
9
|
-
* Optional: Check only tasks related to
|
|
10
|
-
*
|
|
11
|
-
* Supports generic ID types (ObjectId, string, number).
|
|
10
|
+
* Optional: Check only tasks related to specific entities.
|
|
11
|
+
* If provided, errors in collections/tasks not matching the whitelist are ignored.
|
|
12
12
|
*/
|
|
13
|
-
|
|
13
|
+
whitelist?: WhitelistRule[];
|
|
14
14
|
/**
|
|
15
15
|
* Optional: Whitelist specific errors.
|
|
16
16
|
* If a string is provided, exact match is required.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Collection, Document, Filter } from 'mongodb';
|
|
2
|
+
import { ReactiveTaskRecord } from '../reactiveTasks';
|
|
3
|
+
/**
|
|
4
|
+
* A single rule used by the testing utilities to scope checks to a set of
|
|
5
|
+
* source documents.
|
|
6
|
+
*/
|
|
7
|
+
export interface WhitelistRule {
|
|
8
|
+
collection: string;
|
|
9
|
+
/**
|
|
10
|
+
* Filter to find relevant source documents. When omitted every document
|
|
11
|
+
* in the collection is considered.
|
|
12
|
+
*/
|
|
13
|
+
filter?: Filter<Document>;
|
|
14
|
+
/**
|
|
15
|
+
* Optional: restrict to a specific reactive task name.
|
|
16
|
+
*/
|
|
17
|
+
task?: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Resolution outcome for a whitelist against one registry entry.
|
|
21
|
+
*
|
|
22
|
+
* - `'skip'`: the whitelist has rules, but none apply to this collection or
|
|
23
|
+
* the source filters matched zero documents. Callers should skip this
|
|
24
|
+
* entry entirely.
|
|
25
|
+
* - `'matchAll'`: at least one rule for this collection wants the full
|
|
26
|
+
* collection. Callers should apply no extra filter.
|
|
27
|
+
* - An object: the caller should AND this filter with its base query.
|
|
28
|
+
*/
|
|
29
|
+
export type WhitelistResolution = 'skip' | 'matchAll' | Filter<ReactiveTaskRecord>;
|
|
30
|
+
/**
|
|
31
|
+
* Build the `Filter<ReactiveTaskRecord>` for a single registry entry based on
|
|
32
|
+
* the provided whitelist rules. Extracted from `waitUntilReactiveTasksIdle` /
|
|
33
|
+
* `assertNoReactiveTaskErrors` so the two utilities cannot drift.
|
|
34
|
+
*/
|
|
35
|
+
export declare function resolveWhitelistFilter(whitelist: WhitelistRule[], sourceCollection: Pick<Collection<Document>, 'collectionName' | 'find'>): Promise<WhitelistResolution>;
|