@alexify/migronaut 2.2.0 → 2.4.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/CHANGELOG.md +190 -0
- package/README.md +41 -3
- package/bullmq.d.ts +484 -8
- package/index.d.ts +1264 -9
- package/migronaut.schema.json +93 -1
- package/package.json +9 -2
- package/src/bullmq/background-processor.js +541 -0
- package/src/bullmq/index.js +12 -0
- package/src/bullmq/jobs.js +254 -7
- package/src/bullmq/processor.js +348 -21
- package/src/bullmq/producer.js +185 -13
- package/src/bullmq/service.js +484 -45
- package/src/cli/commands/background.js +500 -0
- package/src/cli/commands/create.js +6 -0
- package/src/cli/exit-codes.js +6 -0
- package/src/cli/index.js +2 -0
- package/src/core/audit.js +11 -1
- package/src/core/background-audit.js +139 -0
- package/src/core/background-drift.js +126 -0
- package/src/core/background-dry-run.js +375 -0
- package/src/core/background-engine.js +849 -0
- package/src/core/background-kit.js +432 -0
- package/src/core/background-partition.js +298 -0
- package/src/core/background-runner.js +305 -0
- package/src/core/background-sandbox.js +701 -0
- package/src/core/background-shard.js +542 -0
- package/src/core/background-spec.js +597 -0
- package/src/core/background-store.js +951 -0
- package/src/core/background-throttle.js +269 -0
- package/src/core/background-watch-plan.js +164 -0
- package/src/core/background-watch-store.js +78 -0
- package/src/core/background-watch.js +610 -0
- package/src/core/background.js +1127 -0
- package/src/core/bson-peer.js +23 -0
- package/src/core/changelog.js +32 -0
- package/src/core/collections.js +78 -8
- package/src/core/config.js +102 -12
- package/src/core/converge-plan.js +86 -7
- package/src/core/converge.js +88 -0
- package/src/core/lock.js +48 -21
- package/src/core/migration-logger.js +279 -0
- package/src/core/migrator.js +1027 -22
- package/src/core/options.js +36 -0
- package/src/core/run-recorder.js +6 -1
- package/src/core/run.js +26 -12
- package/src/core/runner.js +34 -8
- package/src/core/server-info.js +9 -2
- package/src/core/shard-info.js +76 -0
- package/src/core/versioning-spec.js +181 -0
- package/src/errors/index.js +88 -0
- package/src/index.js +16 -0
- package/src/utils/error.js +11 -2
- package/src/utils/job-ref.js +44 -0
- package/src/utils/loader.js +77 -9
- package/src/utils/migration-name.js +33 -1
- package/src/utils/redact.js +140 -3
- package/src/utils/telemetry.js +110 -0
- package/src/utils/template.js +62 -1
- package/src/versioning/config.js +155 -0
- package/src/versioning/document.js +326 -0
- package/src/versioning/index.js +50 -0
- package/src/versioning/internal.js +279 -0
- package/src/versioning/mongoose.js +151 -0
- package/src/versioning/occ.js +318 -0
- package/src/versioning/registry.js +187 -0
- package/src/versioning/upcaster.js +213 -0
- package/versioning.d.ts +666 -0
- package/versioning.js +1 -0
package/src/bullmq/producer.js
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
|
-
const {
|
|
1
|
+
const {
|
|
2
|
+
BackgroundPendingError,
|
|
3
|
+
ConfigInvalidError,
|
|
4
|
+
MigrationBlockedError,
|
|
5
|
+
NotAppliedError,
|
|
6
|
+
} = require('../errors/index.js');
|
|
2
7
|
const { actorIssue, pickActor } = require('../utils/actor.js');
|
|
3
8
|
const { mapLimit } = require('../utils/concurrency.js');
|
|
4
9
|
const { assertId, randomId } = require('../utils/id.js');
|
|
5
10
|
const { assertMigrationName } = require('../utils/migration-name.js');
|
|
6
11
|
const {
|
|
12
|
+
BACKGROUND_FORBIDDEN_JOB_OPTIONS,
|
|
7
13
|
FORBIDDEN_JOB_OPTIONS,
|
|
8
14
|
JOB_NAMES,
|
|
15
|
+
buildBackgroundJob,
|
|
9
16
|
buildConvergeJob,
|
|
10
17
|
buildMigrationJob,
|
|
11
|
-
|
|
18
|
+
isObjectLike,
|
|
12
19
|
migrationJobOptions,
|
|
13
20
|
} = require('./jobs.js');
|
|
14
21
|
const { waitForGroup } = require('./wait.js');
|
|
@@ -24,7 +31,7 @@ const LOOKUP_CONCURRENCY = 16;
|
|
|
24
31
|
*/
|
|
25
32
|
function assertJobOptions(jobOptions) {
|
|
26
33
|
if (jobOptions === undefined) return;
|
|
27
|
-
if (!
|
|
34
|
+
if (!isObjectLike(jobOptions)) {
|
|
28
35
|
throw new ConfigInvalidError('jobOptions must be an object', { jobOptions: typeof jobOptions });
|
|
29
36
|
}
|
|
30
37
|
for (const key of FORBIDDEN_JOB_OPTIONS) {
|
|
@@ -38,6 +45,27 @@ function assertJobOptions(jobOptions) {
|
|
|
38
45
|
}
|
|
39
46
|
}
|
|
40
47
|
|
|
48
|
+
/**
|
|
49
|
+
* A background queue's `jobOptions`: the same passthrough, minus what decides
|
|
50
|
+
* how its coordinators and lanes are retried, ordered and woken.
|
|
51
|
+
*/
|
|
52
|
+
function assertBackgroundJobOptions(jobOptions) {
|
|
53
|
+
if (jobOptions === undefined) return;
|
|
54
|
+
if (!isObjectLike(jobOptions)) {
|
|
55
|
+
throw new ConfigInvalidError('background jobOptions must be an object', {
|
|
56
|
+
jobOptions: typeof jobOptions,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
for (const key of BACKGROUND_FORBIDDEN_JOB_OPTIONS) {
|
|
60
|
+
if (jobOptions[key] !== undefined) {
|
|
61
|
+
throw new ConfigInvalidError(
|
|
62
|
+
`background jobOptions.${key} is not configurable — coordinators and lanes set their own`,
|
|
63
|
+
{ key },
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
41
69
|
/** Validate the `requestedBy` / `reason` of an enqueue call, and return them */
|
|
42
70
|
function actorOf(options) {
|
|
43
71
|
for (const key of ['requestedBy', 'reason']) {
|
|
@@ -124,16 +152,38 @@ async function planUpJobs(kit, options = {}) {
|
|
|
124
152
|
const migrations = [];
|
|
125
153
|
/** The version of each file the plan was made from — a worker refuses any other */
|
|
126
154
|
const checksums = new Map();
|
|
155
|
+
/**
|
|
156
|
+
* The first migration that requires a background migration not completed
|
|
157
|
+
* yet — the plan ends before it. (A background file that requires one is
|
|
158
|
+
* not held: it registers as blocked, and starts once it is unblocked.)
|
|
159
|
+
*/
|
|
160
|
+
let waiting;
|
|
127
161
|
for (const row of rows) {
|
|
128
|
-
if (row.status
|
|
129
|
-
|
|
130
|
-
|
|
162
|
+
if (row.status === 'applied' && !force) continue;
|
|
163
|
+
if (row.kind !== 'background' && Array.isArray(row.waitsFor) && row.waitsFor.length > 0) {
|
|
164
|
+
waiting = { migration: row.file, waitsFor: row.waitsFor };
|
|
165
|
+
break;
|
|
131
166
|
}
|
|
167
|
+
migrations.push(row.file);
|
|
168
|
+
if (typeof row.checksum === 'string') checksums.set(row.file, row.checksum);
|
|
169
|
+
}
|
|
170
|
+
if (waiting !== undefined && filename !== undefined) {
|
|
171
|
+
// Asked for by name: say why now, rather than enqueue a job that can only fail.
|
|
172
|
+
throw new BackgroundPendingError(
|
|
173
|
+
`${waiting.migration} requires background migration(s) that have not completed: ` +
|
|
174
|
+
waiting.waitsFor.join(', '),
|
|
175
|
+
{
|
|
176
|
+
migration: waiting.migration,
|
|
177
|
+
waitsFor: waiting.waitsFor.map((migration) => ({ migration })),
|
|
178
|
+
},
|
|
179
|
+
);
|
|
132
180
|
}
|
|
133
181
|
const groupId = await newGroupId(kit);
|
|
182
|
+
const tail = waiting !== undefined ? { waiting } : {};
|
|
134
183
|
if (migrations.length === 0) {
|
|
135
|
-
const plan = { groupId, direction: JOB_NAMES.UP, batch: null, migrations, jobs: [] };
|
|
136
|
-
|
|
184
|
+
const plan = { groupId, direction: JOB_NAMES.UP, batch: null, migrations, jobs: [], ...tail };
|
|
185
|
+
// A group cut short of the head never converges — nor does it with nothing to run.
|
|
186
|
+
if (waiting === undefined && converge && !(await kit.converge({ dryRun: true })).inSync) {
|
|
137
187
|
plan.converge = buildConvergeJob({ groupId, ordered, jobOptions, ...actor });
|
|
138
188
|
}
|
|
139
189
|
return plan;
|
|
@@ -158,8 +208,8 @@ async function planUpJobs(kit, options = {}) {
|
|
|
158
208
|
opts: migrationJobOptions(jobOptions, JOB_NAMES.UP, migration, { force }),
|
|
159
209
|
});
|
|
160
210
|
}
|
|
161
|
-
const plan = { groupId, direction: JOB_NAMES.UP, batch, migrations, jobs };
|
|
162
|
-
if (converge) {
|
|
211
|
+
const plan = { groupId, direction: JOB_NAMES.UP, batch, migrations, jobs, ...tail };
|
|
212
|
+
if (converge && waiting === undefined) {
|
|
163
213
|
plan.converge = buildConvergeJob({
|
|
164
214
|
groupId,
|
|
165
215
|
ordered,
|
|
@@ -335,12 +385,22 @@ async function enqueueGroup(queue, kit, plan, { queueEvents, getQueueEvents } =
|
|
|
335
385
|
);
|
|
336
386
|
}
|
|
337
387
|
|
|
388
|
+
if (plan.waiting !== undefined) {
|
|
389
|
+
kit.logger.info(
|
|
390
|
+
`⧗ ${plan.waiting.migration} waits for background migration(s): ` +
|
|
391
|
+
`${plan.waiting.waitsFor.join(', ')} — not enqueued yet`,
|
|
392
|
+
{ groupId, migration: plan.waiting.migration, waitsFor: plan.waiting.waitsFor.length },
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
|
|
338
396
|
return {
|
|
339
397
|
groupId,
|
|
340
398
|
direction,
|
|
341
399
|
batch,
|
|
342
|
-
// "No migration to run" — a converge-only group is still up to date
|
|
343
|
-
|
|
400
|
+
// "No migration to run" — a converge-only group is still up to date; one
|
|
401
|
+
// cut short by a background migration is not.
|
|
402
|
+
upToDate: jobs.length === 0 && plan.waiting === undefined,
|
|
403
|
+
...(plan.waiting !== undefined ? { waiting: plan.waiting } : {}),
|
|
344
404
|
jobs,
|
|
345
405
|
deduplicated,
|
|
346
406
|
converge,
|
|
@@ -359,7 +419,7 @@ async function enqueueGroup(queue, kit, plan, { queueEvents, getQueueEvents } =
|
|
|
359
419
|
* pending — the declared state describes the newest schema.
|
|
360
420
|
*/
|
|
361
421
|
async function enqueueConverge(queue, kit, options = {}, internals = {}) {
|
|
362
|
-
if (!
|
|
422
|
+
if (!isObjectLike(options)) {
|
|
363
423
|
throw new ConfigInvalidError('enqueueConverge options must be an object');
|
|
364
424
|
}
|
|
365
425
|
const { ordered, jobOptions, queueEvents } = options;
|
|
@@ -408,6 +468,115 @@ async function enqueueUp(queue, kit, options = {}, internals = {}) {
|
|
|
408
468
|
});
|
|
409
469
|
}
|
|
410
470
|
|
|
471
|
+
/** How long a running background migration may show no sign of life before a takeover */
|
|
472
|
+
const DEFAULT_STALL_MS = 15 * 60_000;
|
|
473
|
+
|
|
474
|
+
/** The latest sign of life of a background migration: a checkpoint, a coordinator step, its start */
|
|
475
|
+
function lastActivity(status) {
|
|
476
|
+
let latest = 0;
|
|
477
|
+
for (const at of [
|
|
478
|
+
status.lastProgressAt,
|
|
479
|
+
status.coordinator?.at,
|
|
480
|
+
status.startedAt,
|
|
481
|
+
status.registeredAt,
|
|
482
|
+
]) {
|
|
483
|
+
const time = at instanceof Date ? at.getTime() : 0;
|
|
484
|
+
if (time > latest) latest = time;
|
|
485
|
+
}
|
|
486
|
+
return latest;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Enqueue the coordinator of one background migration — or, with no
|
|
491
|
+
* `migration`, of every one with work to do (unblocking those whose requires
|
|
492
|
+
* are met on the way). Safe to call from every pod, as often as you like: a
|
|
493
|
+
* coordinator chain that is alive absorbs the add. A background migration
|
|
494
|
+
* nothing has moved for `stallMs` (no live lease, no checkpoint, no
|
|
495
|
+
* coordinator step) also gets a takeover coordinator — one per round, however
|
|
496
|
+
* many pods ask — whose newer round retires the stuck one.
|
|
497
|
+
*/
|
|
498
|
+
/** Every option enqueueBackground takes — a typo (`stalMs`) is refused, not a silent default */
|
|
499
|
+
const ENQUEUE_BACKGROUND_KEYS = new Set([
|
|
500
|
+
'migration',
|
|
501
|
+
'jobOptions',
|
|
502
|
+
'stallMs',
|
|
503
|
+
'requestedBy',
|
|
504
|
+
'reason',
|
|
505
|
+
]);
|
|
506
|
+
|
|
507
|
+
async function enqueueBackground(queue, kit, options = {}) {
|
|
508
|
+
if (!isObjectLike(options)) {
|
|
509
|
+
throw new ConfigInvalidError('enqueueBackground options must be an object');
|
|
510
|
+
}
|
|
511
|
+
for (const key of Object.keys(options)) {
|
|
512
|
+
if (!ENQUEUE_BACKGROUND_KEYS.has(key)) {
|
|
513
|
+
throw new ConfigInvalidError(`enqueueBackground: "${key}" is not an option`, { key });
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
const { migration, jobOptions, stallMs = DEFAULT_STALL_MS } = options;
|
|
517
|
+
assertQueue(queue);
|
|
518
|
+
assertBackgroundJobOptions(jobOptions);
|
|
519
|
+
if (!Number.isSafeInteger(stallMs) || stallMs < 1000) {
|
|
520
|
+
throw new ConfigInvalidError('stallMs must be an integer of at least 1000', { stallMs });
|
|
521
|
+
}
|
|
522
|
+
const actor = actorOf(options);
|
|
523
|
+
const targets = [];
|
|
524
|
+
if (migration !== undefined) {
|
|
525
|
+
assertMigrationName(migration);
|
|
526
|
+
const status = await kit.backgroundStatus(migration);
|
|
527
|
+
if (status === null) {
|
|
528
|
+
throw new NotAppliedError(
|
|
529
|
+
`Background migration ${migration} is not registered — run up first`,
|
|
530
|
+
{ migration },
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
targets.push(status);
|
|
534
|
+
} else {
|
|
535
|
+
// One read for all of them: the runnable list carries what a stall is told by.
|
|
536
|
+
targets.push(...(await kit.runnableBackground()));
|
|
537
|
+
}
|
|
538
|
+
const specs = [];
|
|
539
|
+
const now = Date.now();
|
|
540
|
+
for (const status of targets) {
|
|
541
|
+
specs.push(buildBackgroundJob({ migration: status.migration, jobOptions, ...actor }));
|
|
542
|
+
const stalled =
|
|
543
|
+
status.status === 'running' &&
|
|
544
|
+
status.liveLeases === 0 &&
|
|
545
|
+
now - lastActivity(status) > stallMs;
|
|
546
|
+
if (stalled) {
|
|
547
|
+
// An operational event: nothing has moved it for stallMs.
|
|
548
|
+
kit.logger.warn(
|
|
549
|
+
`⚠ Background migration ${status.migration} looks stalled (nothing moved it for ` +
|
|
550
|
+
`${Math.round((now - lastActivity(status)) / 1000)}s) — a takeover coordinator is queued`,
|
|
551
|
+
{
|
|
552
|
+
background: status.migration,
|
|
553
|
+
round: status.coordinator?.round ?? 0,
|
|
554
|
+
idleMs: now - lastActivity(status),
|
|
555
|
+
},
|
|
556
|
+
);
|
|
557
|
+
specs.push(
|
|
558
|
+
buildBackgroundJob({
|
|
559
|
+
migration: status.migration,
|
|
560
|
+
takeoverOf: status.coordinator?.round ?? 0,
|
|
561
|
+
jobOptions,
|
|
562
|
+
...actor,
|
|
563
|
+
}),
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
if (specs.length === 0) return { jobs: [] };
|
|
568
|
+
const added = await queue.addBulk(specs);
|
|
569
|
+
const jobs = [];
|
|
570
|
+
for (const [index, job] of added.entries()) {
|
|
571
|
+
jobs.push({
|
|
572
|
+
migration: specs[index].data.migration,
|
|
573
|
+
id: String(job.id),
|
|
574
|
+
...(specs[index].data.takeover ? { takeover: true } : {}),
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
return { jobs };
|
|
578
|
+
}
|
|
579
|
+
|
|
411
580
|
/** Enqueue a rollback (last batch, a `batch`, `steps`, back `to`, or one `filename`) */
|
|
412
581
|
async function enqueueDown(queue, kit, options = {}, internals = {}) {
|
|
413
582
|
const { queueEvents, ...planOptions } = options;
|
|
@@ -418,7 +587,10 @@ async function enqueueDown(queue, kit, options = {}, internals = {}) {
|
|
|
418
587
|
}
|
|
419
588
|
|
|
420
589
|
module.exports = {
|
|
590
|
+
DEFAULT_STALL_MS,
|
|
591
|
+
assertBackgroundJobOptions,
|
|
421
592
|
assertJobOptions,
|
|
593
|
+
enqueueBackground,
|
|
422
594
|
enqueueConverge,
|
|
423
595
|
enqueueDown,
|
|
424
596
|
enqueueUp,
|