@alexify/migronaut 2.1.0 → 2.2.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 +116 -0
- package/README.md +35 -8
- package/bullmq.d.ts +16 -1
- package/index.d.ts +266 -13
- package/migronaut.schema.json +57 -1
- package/package.json +1 -1
- package/src/bullmq/processor.js +25 -1
- package/src/bullmq/producer.js +17 -14
- package/src/cli/commands/converge.js +38 -10
- package/src/cli/table.js +68 -9
- package/src/core/audit.js +88 -3
- package/src/core/collections.js +50 -26
- package/src/core/config.js +31 -1
- package/src/core/converge-plan.js +260 -57
- package/src/core/converge-search-run.js +440 -0
- package/src/core/converge-search.js +404 -0
- package/src/core/converge.js +347 -190
- package/src/core/index-spec.js +27 -16
- package/src/core/lock.js +50 -12
- package/src/core/migrator.js +47 -14
- package/src/core/options.js +16 -1
- package/src/core/search-index-spec.js +758 -0
- package/src/core/server-info.js +63 -0
- package/src/errors/index.js +9 -5
- package/src/utils/canonical.js +34 -1
- package/src/utils/telemetry.js +18 -1
- package/src/utils/template.js +7 -0
package/index.d.ts
CHANGED
|
@@ -292,6 +292,30 @@ export interface MigronautConfig {
|
|
|
292
292
|
* Default: false
|
|
293
293
|
*/
|
|
294
294
|
convergeAfterUp?: boolean;
|
|
295
|
+
/**
|
|
296
|
+
* What converge does with declared search indexes on a server without
|
|
297
|
+
* Atlas Search: `'fail'` refuses the run before anything is written,
|
|
298
|
+
* `'skip'` converges everything else and reports them as `skip` rows.
|
|
299
|
+
* Default: 'fail'
|
|
300
|
+
* @experimental New in 2.2
|
|
301
|
+
*/
|
|
302
|
+
onSearchUnavailable?: 'fail' | 'skip';
|
|
303
|
+
/**
|
|
304
|
+
* Hold every converge — the after-up one included — until each declared
|
|
305
|
+
* search index is queryable with its declared definition. Search indexes
|
|
306
|
+
* build in the background, so without it a new one is not queryable yet when
|
|
307
|
+
* converge returns. An index that FAILED or went STALE before the run, its
|
|
308
|
+
* definition unchanged, does not hold it — it is warned about instead. The
|
|
309
|
+
* migration lock is released while it waits. Default: false
|
|
310
|
+
* @experimental New in 2.2
|
|
311
|
+
*/
|
|
312
|
+
waitForSearchIndexes?: boolean;
|
|
313
|
+
/**
|
|
314
|
+
* How long `waitForSearchIndexes` waits before the converge fails with
|
|
315
|
+
* `phase: 'wait'` (the server goes on building). Default: 600000 (10 minutes)
|
|
316
|
+
* @experimental New in 2.2
|
|
317
|
+
*/
|
|
318
|
+
searchIndexWaitTimeoutMs?: number;
|
|
295
319
|
/** Mongoose instance — required only if your migrations use Mongoose models */
|
|
296
320
|
mongoose?: MongooseLike;
|
|
297
321
|
hooks?: MigrationHooks;
|
|
@@ -393,9 +417,77 @@ export interface IndexDefinition {
|
|
|
393
417
|
export type ValidationLevel = 'off' | 'strict' | 'moderate';
|
|
394
418
|
export type ValidationAction = 'error' | 'warn' | 'errorAndLog';
|
|
395
419
|
|
|
420
|
+
/** The two kinds of Atlas search index */
|
|
421
|
+
export type SearchIndexType = 'search' | 'vectorSearch';
|
|
422
|
+
|
|
423
|
+
/** `mappings` of an Atlas Search definition */
|
|
424
|
+
export interface SearchIndexMappings {
|
|
425
|
+
/** Default: false */
|
|
426
|
+
dynamic?: boolean | { typeSet: string };
|
|
427
|
+
fields?: Record<string, unknown>;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* An Atlas Search index definition, as Atlas defines it. Compared whole, with
|
|
432
|
+
* the documented defaults filled in; anything Atlas adds can be declared too.
|
|
433
|
+
*/
|
|
434
|
+
export interface SearchDefinition {
|
|
435
|
+
mappings: SearchIndexMappings;
|
|
436
|
+
/** Default: 'lucene.standard' */
|
|
437
|
+
analyzer?: string;
|
|
438
|
+
/** Default: the analyzer */
|
|
439
|
+
searchAnalyzer?: string;
|
|
440
|
+
analyzers?: Array<Record<string, unknown>>;
|
|
441
|
+
synonyms?: Array<Record<string, unknown>>;
|
|
442
|
+
/** Default: false */
|
|
443
|
+
storedSource?: boolean | { include?: string[]; exclude?: string[] };
|
|
444
|
+
/** Default: 1 */
|
|
445
|
+
numPartitions?: number;
|
|
446
|
+
[option: string]: unknown;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/** One field of a Vector Search definition */
|
|
450
|
+
export interface VectorSearchField {
|
|
451
|
+
/** `'autoEmbed'` is Atlas's automated embedding (in preview); one index holds vector or autoEmbed fields, not both */
|
|
452
|
+
type: 'vector' | 'filter' | 'autoEmbed' | (string & {});
|
|
453
|
+
path: string;
|
|
454
|
+
numDimensions?: number;
|
|
455
|
+
similarity?: 'euclidean' | 'cosine' | 'dotProduct';
|
|
456
|
+
/** Default: 'none' ('scalar' for autoEmbed) */
|
|
457
|
+
quantization?: string;
|
|
458
|
+
/** Default: 'hnsw' */
|
|
459
|
+
indexingMethod?: 'hnsw' | 'flat';
|
|
460
|
+
/** Default: { maxEdges: 16, numEdgeCandidates: 100 } */
|
|
461
|
+
hnswOptions?: { maxEdges?: number; numEdgeCandidates?: number };
|
|
462
|
+
/** autoEmbed: the embedding model */
|
|
463
|
+
model?: string;
|
|
464
|
+
/** autoEmbed: 'text' */
|
|
465
|
+
modality?: string;
|
|
466
|
+
[option: string]: unknown;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/** A Vector Search index definition */
|
|
470
|
+
export interface VectorSearchDefinition {
|
|
471
|
+
fields: VectorSearchField[];
|
|
472
|
+
[option: string]: unknown;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* One declared Atlas Search or Vector Search index. The name defaults to
|
|
477
|
+
* `'default'` and the type to `'search'`, as on the server. A change of type
|
|
478
|
+
* — or of an autoEmbed field's path, model, size, quantization or modality —
|
|
479
|
+
* cannot be made in place: converge refuses it, and the way is a new index
|
|
480
|
+
* under a new name (converge, then remove the old declaration and converge
|
|
481
|
+
* with prune).
|
|
482
|
+
* @experimental New in 2.2 — the shape may still change in a minor release (named in the CHANGELOG).
|
|
483
|
+
*/
|
|
484
|
+
export type SearchIndexDefinition =
|
|
485
|
+
| { name?: string; type?: 'search'; definition: SearchDefinition }
|
|
486
|
+
| { name?: string; type: 'vectorSearch'; definition: VectorSearchDefinition };
|
|
487
|
+
|
|
396
488
|
/**
|
|
397
489
|
* A declared collection: the end state `converge()` keeps it in. Leave
|
|
398
|
-
* `indexes` or `validator` out to leave that part unmanaged.
|
|
490
|
+
* `indexes`, `searchIndexes` or `validator` out to leave that part unmanaged.
|
|
399
491
|
* @experimental New in 2.1 — the shape may still change in a minor release (named in the CHANGELOG).
|
|
400
492
|
*/
|
|
401
493
|
export interface CollectionDefinition {
|
|
@@ -405,13 +497,23 @@ export interface CollectionDefinition {
|
|
|
405
497
|
* unless `prune` is on.
|
|
406
498
|
*/
|
|
407
499
|
indexes?: IndexDefinition[];
|
|
500
|
+
/**
|
|
501
|
+
* Atlas Search and Vector Search indexes (Atlas, an Atlas CLI local
|
|
502
|
+
* deployment, or MongoDB 8.3+ with mongot). Undeclared live ones are kept
|
|
503
|
+
* unless `prune` is on; leave the key out and they are not managed at all.
|
|
504
|
+
* @experimental New in 2.2
|
|
505
|
+
*/
|
|
506
|
+
searchIndexes?: SearchIndexDefinition[];
|
|
408
507
|
/** A query or `{ $jsonSchema }` document; `null` (or `{}`) for no validator */
|
|
409
508
|
validator?: Record<string, unknown> | null;
|
|
410
509
|
/** Default: 'strict'. Only with a validator */
|
|
411
510
|
validationLevel?: ValidationLevel;
|
|
412
511
|
/** Default: 'error'. Only with a validator */
|
|
413
512
|
validationAction?: ValidationAction;
|
|
414
|
-
/**
|
|
513
|
+
/**
|
|
514
|
+
* Drop live indexes (and search indexes, when `searchIndexes` is declared)
|
|
515
|
+
* this definition does not declare. Default: the call's `prune`, else false
|
|
516
|
+
*/
|
|
415
517
|
prune?: boolean;
|
|
416
518
|
}
|
|
417
519
|
|
|
@@ -443,20 +545,33 @@ export interface ConvergeOptions {
|
|
|
443
545
|
* CLI: `--rebuild-unique`.
|
|
444
546
|
*/
|
|
445
547
|
rebuildUnique?: boolean;
|
|
548
|
+
/**
|
|
549
|
+
* Hold the run until every declared search index serves its
|
|
550
|
+
* declaration — failing on a FAILED build of an index this run created or
|
|
551
|
+
* changed, or after `searchIndexWaitTimeoutMs`. The migration lock is released
|
|
552
|
+
* when the wait starts — it only reads. Overrides the config's `waitForSearchIndexes`;
|
|
553
|
+
* not with `dryRun`. CLI: `--wait-search` / `--no-wait-search`.
|
|
554
|
+
* @experimental New in 2.2
|
|
555
|
+
*/
|
|
556
|
+
waitForSearchIndexes?: boolean;
|
|
446
557
|
/** Who asked for this converge — recorded in the converge history */
|
|
447
558
|
requestedBy?: string;
|
|
448
559
|
/** Why — recorded in the converge history */
|
|
449
560
|
reason?: string;
|
|
450
561
|
}
|
|
451
562
|
|
|
452
|
-
export type ConvergeTarget = 'collection' | 'validator' | 'index';
|
|
563
|
+
export type ConvergeTarget = 'collection' | 'validator' | 'index' | 'searchIndex';
|
|
453
564
|
|
|
454
565
|
/**
|
|
455
566
|
* What converge does to one target. `keep` is an undeclared index left alone
|
|
456
567
|
* (prune off); `conflict` refuses the run — an undeclared index covers the
|
|
457
568
|
* declared one's key under another name, a unique index would be rebuilt
|
|
458
|
-
* without {@link ConvergeOptions.rebuildUnique},
|
|
459
|
-
*
|
|
569
|
+
* without {@link ConvergeOptions.rebuildUnique}, the collection is a view or a
|
|
570
|
+
* time-series collection, a search index would need a change no update can
|
|
571
|
+
* make (its type, an autoEmbed field's model or size), or the server has no
|
|
572
|
+
* Atlas Search; `skip` is a declared search index left alone on a server
|
|
573
|
+
* without Search (`onSearchUnavailable: 'skip'`). A search index is never
|
|
574
|
+
* `recreate`d: `modify` updates it in place.
|
|
460
575
|
* @experimental New in 2.1 — the shape may still change in a minor release (named in the CHANGELOG).
|
|
461
576
|
*/
|
|
462
577
|
export type ConvergeActionKind =
|
|
@@ -466,7 +581,37 @@ export type ConvergeActionKind =
|
|
|
466
581
|
| 'drop'
|
|
467
582
|
| 'keep'
|
|
468
583
|
| 'unchanged'
|
|
469
|
-
| 'conflict'
|
|
584
|
+
| 'conflict'
|
|
585
|
+
| 'skip';
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* The status `$listSearchIndexes` reports for a search index — `'UNKNOWN'`
|
|
589
|
+
* when the server reports none
|
|
590
|
+
*/
|
|
591
|
+
export type SearchIndexStatus =
|
|
592
|
+
| 'PENDING'
|
|
593
|
+
| 'BUILDING'
|
|
594
|
+
| 'READY'
|
|
595
|
+
| 'FAILED'
|
|
596
|
+
| 'STALE'
|
|
597
|
+
| 'DELETING'
|
|
598
|
+
| 'DOES_NOT_EXIST'
|
|
599
|
+
| 'UNKNOWN'
|
|
600
|
+
| (string & {});
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Where the server is with a search index: it builds in the background, so a
|
|
604
|
+
* created or updated one is not queryable (with its new definition) at once
|
|
605
|
+
* @experimental New in 2.2
|
|
606
|
+
*/
|
|
607
|
+
export interface SearchIndexBuild {
|
|
608
|
+
status: SearchIndexStatus;
|
|
609
|
+
queryable: boolean;
|
|
610
|
+
/** The server's message — why a build FAILED, typically */
|
|
611
|
+
message?: string;
|
|
612
|
+
/** A newer definition is being built next to the one served */
|
|
613
|
+
updating?: true;
|
|
614
|
+
}
|
|
470
615
|
|
|
471
616
|
/**
|
|
472
617
|
* `planned` in a dry run; otherwise `applied`, `failed`, or `skipped` — no
|
|
@@ -497,6 +642,21 @@ export interface ConvergeAction {
|
|
|
497
642
|
from?: Record<string, unknown>;
|
|
498
643
|
/** What the row puts there — the declared index or validator — on rows that create or change it */
|
|
499
644
|
to?: Record<string, unknown>;
|
|
645
|
+
/**
|
|
646
|
+
* A search index row's build state on the server — as read before the run,
|
|
647
|
+
* and after it for a row the run applied
|
|
648
|
+
* @experimental New in 2.2
|
|
649
|
+
*/
|
|
650
|
+
build?: SearchIndexBuild;
|
|
651
|
+
/**
|
|
652
|
+
* On a search index row: the options the server reports that the
|
|
653
|
+
* declaration does not set and migronaut knows no default for
|
|
654
|
+
* (`mappings.fields.title.similarity`) — left out of the comparison, so a
|
|
655
|
+
* new server default does not make every converge update the index.
|
|
656
|
+
* Declare one to manage it.
|
|
657
|
+
* @experimental New in 2.2
|
|
658
|
+
*/
|
|
659
|
+
ignored?: string[];
|
|
500
660
|
}
|
|
501
661
|
|
|
502
662
|
/**
|
|
@@ -532,6 +692,12 @@ export interface ConvergeHistoryEntry {
|
|
|
532
692
|
/** The rows that changed, failed or refused the run — each with its collection and `from` / `to` */
|
|
533
693
|
actions: Array<ConvergeAction & { collection: string }>;
|
|
534
694
|
unstable?: ConvergeUnstable[];
|
|
695
|
+
/**
|
|
696
|
+
* What the run saw of Atlas Search, and how a wait for its builds ended —
|
|
697
|
+
* when a definition declares `searchIndexes`
|
|
698
|
+
* @experimental New in 2.2
|
|
699
|
+
*/
|
|
700
|
+
search?: ConvergeSearchSummary;
|
|
535
701
|
}
|
|
536
702
|
|
|
537
703
|
/**
|
|
@@ -546,6 +712,42 @@ export interface ConvergeUnstable {
|
|
|
546
712
|
reason?: string;
|
|
547
713
|
}
|
|
548
714
|
|
|
715
|
+
/**
|
|
716
|
+
* A declared search index that exists but does not serve its declaration yet
|
|
717
|
+
* @experimental New in 2.2
|
|
718
|
+
*/
|
|
719
|
+
export interface SearchIndexNotReady extends SearchIndexBuild {
|
|
720
|
+
collection: string;
|
|
721
|
+
name: string;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* What a converge saw of Atlas Search — present when a definition declares
|
|
726
|
+
* `searchIndexes`
|
|
727
|
+
* @experimental New in 2.2
|
|
728
|
+
*/
|
|
729
|
+
export interface ConvergeSearchSummary {
|
|
730
|
+
/** Whether the server has Atlas Search */
|
|
731
|
+
available: boolean;
|
|
732
|
+
/**
|
|
733
|
+
* How that was told: `'listed'` (the server listed search indexes),
|
|
734
|
+
* `'parameter'` (its search index manager setting), `'error'` (it refused
|
|
735
|
+
* a search command), `'version'` (older than 6.0, not asked) or
|
|
736
|
+
* `'assumed'` (it would not say — a refusal at apply time reports it)
|
|
737
|
+
*/
|
|
738
|
+
evidence?: 'listed' | 'parameter' | 'error' | 'version' | 'assumed';
|
|
739
|
+
/**
|
|
740
|
+
* Declared search indexes still building, updating, stale or failed. Does
|
|
741
|
+
* not count against `inSync`: a build is the server's work, not a difference.
|
|
742
|
+
*/
|
|
743
|
+
notReady: SearchIndexNotReady[];
|
|
744
|
+
/** How a wait for the builds (`waitForSearchIndexes`) ended, when there was one */
|
|
745
|
+
wait?: { outcome: ConvergeWaitOutcome; waitedMs: number };
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/** How a wait for search index builds ended */
|
|
749
|
+
export type ConvergeWaitOutcome = 'ready' | 'failed' | 'timeout' | 'unreadable' | 'aborted';
|
|
750
|
+
|
|
549
751
|
/**
|
|
550
752
|
* Outcome of {@link MigratorKit.converge}
|
|
551
753
|
* @experimental New in 2.1 — the shape may still change in a minor release (named in the CHANGELOG).
|
|
@@ -556,11 +758,15 @@ export interface ConvergeResult {
|
|
|
556
758
|
changed: number;
|
|
557
759
|
/**
|
|
558
760
|
* True when the database matches the declarations: nothing left to do and
|
|
559
|
-
* no conflict. Undeclared indexes kept with prune off
|
|
761
|
+
* no conflict. Undeclared indexes kept with prune off, search indexes
|
|
762
|
+
* skipped on a server without Search, and search index builds still under
|
|
763
|
+
* way do not count against it.
|
|
560
764
|
*/
|
|
561
765
|
inSync: boolean;
|
|
562
766
|
collections: CollectionConvergeResult[];
|
|
563
767
|
unstable?: ConvergeUnstable[];
|
|
768
|
+
/** @experimental New in 2.2 */
|
|
769
|
+
search?: ConvergeSearchSummary;
|
|
564
770
|
}
|
|
565
771
|
|
|
566
772
|
// ─── Logger ───────────────────────────────────────────────────────────────────
|
|
@@ -1014,6 +1220,12 @@ export interface LockEvent extends MigronautEventBase {
|
|
|
1014
1220
|
ttlMs?: number;
|
|
1015
1221
|
/** How long acquisition took in ms (on `lock:acquired`) */
|
|
1016
1222
|
acquireMs?: number;
|
|
1223
|
+
/**
|
|
1224
|
+
* True on a `lock:released` that came before the run ended: a converge gave
|
|
1225
|
+
* the lock up to wait for search index builds, which only reads.
|
|
1226
|
+
* @experimental New in 2.2
|
|
1227
|
+
*/
|
|
1228
|
+
early?: true;
|
|
1017
1229
|
}
|
|
1018
1230
|
|
|
1019
1231
|
/** Who started a converge: the `converge` call itself, or a bulk `up` (`convergeAfterUp`) */
|
|
@@ -1053,6 +1265,26 @@ export interface ConvergeActionEvent extends MigronautEventBase {
|
|
|
1053
1265
|
/**
|
|
1054
1266
|
* @experimental New in 2.1 — the shape may still change in a minor release (named in the CHANGELOG).
|
|
1055
1267
|
*/
|
|
1268
|
+
/**
|
|
1269
|
+
* A converge's wait for search index builds (`waitForSearchIndexes`):
|
|
1270
|
+
* `started` once, `progress` every 30 seconds, then how it ended — one of
|
|
1271
|
+
* {@link ConvergeWaitOutcome}.
|
|
1272
|
+
* @experimental New in 2.2
|
|
1273
|
+
*/
|
|
1274
|
+
export interface ConvergeWaitEvent extends MigronautEventBase {
|
|
1275
|
+
status: 'started' | 'progress' | ConvergeWaitOutcome;
|
|
1276
|
+
/** How many search indexes the wait is for */
|
|
1277
|
+
searchIndexes: number;
|
|
1278
|
+
/** `started` only — whether the migration lock was released for the wait */
|
|
1279
|
+
lockReleased?: boolean;
|
|
1280
|
+
/** `started` only — the budget (`searchIndexWaitTimeoutMs`) */
|
|
1281
|
+
timeoutMs?: number;
|
|
1282
|
+
/** Every status but `started` */
|
|
1283
|
+
waitedMs?: number;
|
|
1284
|
+
/** `failed` and `timeout` — the indexes that did not get there */
|
|
1285
|
+
notReady?: SearchIndexNotReady[];
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1056
1288
|
export interface ConvergeEndEvent extends MigronautEventBase {
|
|
1057
1289
|
trigger: ConvergeTrigger;
|
|
1058
1290
|
success: boolean;
|
|
@@ -1085,12 +1317,17 @@ export interface MigronautEvents {
|
|
|
1085
1317
|
'lock:lost': (event: LockEvent) => void;
|
|
1086
1318
|
'converge:start': (event: ConvergeStartEvent) => void;
|
|
1087
1319
|
'converge:action': (event: ConvergeActionEvent) => void;
|
|
1320
|
+
'converge:wait': (event: ConvergeWaitEvent) => void;
|
|
1088
1321
|
'converge:end': (event: ConvergeEndEvent) => void;
|
|
1089
1322
|
}
|
|
1090
1323
|
|
|
1091
1324
|
/** One check performed by {@link MigratorKit.audit} */
|
|
1092
1325
|
export interface AuditCheck {
|
|
1093
|
-
/**
|
|
1326
|
+
/**
|
|
1327
|
+
* e.g. 'config', 'connection', 'transactions', 'indexes', 'lock', 'checksums',
|
|
1328
|
+
* 'pending', 'ordering', 'runtime' — and 'search' when declared collections
|
|
1329
|
+
* hold search indexes
|
|
1330
|
+
*/
|
|
1094
1331
|
name: string;
|
|
1095
1332
|
status: 'pass' | 'warn' | 'fail';
|
|
1096
1333
|
detail: string;
|
|
@@ -1620,11 +1857,27 @@ export class QueueJobFailedError extends MigronautError {
|
|
|
1620
1857
|
|
|
1621
1858
|
/**
|
|
1622
1859
|
* Thrown by {@link MigratorKit.converge} when the database cannot be brought
|
|
1623
|
-
* to the declared state. `context.phase` is
|
|
1624
|
-
* (`context.conflicts` lists why
|
|
1625
|
-
*
|
|
1626
|
-
*
|
|
1627
|
-
*
|
|
1860
|
+
* to the declared state. `context.phase` is:
|
|
1861
|
+
* - `'plan'` for a refused plan (`context.conflicts` lists why, with a `hint`
|
|
1862
|
+
* when Atlas Search is missing; nothing was written) — or a search index
|
|
1863
|
+
* list that could not be read before the first write (`collection`,
|
|
1864
|
+
* `target: 'searchIndex'`, `cause`, `mongoCode`, `hint`);
|
|
1865
|
+
* - `'replan'` when a collection changed while the run was under way
|
|
1866
|
+
* (`collection`, `introduced`: the new conflicts or drops; nothing of that
|
|
1867
|
+
* collection was written) — or its search index list could not be read
|
|
1868
|
+
* again (as for `'plan'`);
|
|
1869
|
+
* - `'apply'` for a failed step (`collection`, `target`, `name`, `action`,
|
|
1870
|
+
* `cause`, and `mongoCode`, `hint` and — after a failed rebuild — `restored`
|
|
1871
|
+
* when they apply) — or a search index list that could not be read to check
|
|
1872
|
+
* the steps just applied (as for `'plan'`);
|
|
1873
|
+
* - `'wait'` when `waitForSearchIndexes` gave up: `reason` is `'failed'` (the
|
|
1874
|
+
* build of a search index this run created or changed FAILED) or `'timeout'`,
|
|
1875
|
+
* with `notReady` the indexes not serving their declaration, `waitedMs`,
|
|
1876
|
+
* `timeoutMs` — or `'unreadable'`: a search index list that could not be
|
|
1877
|
+
* read (as for `'plan'`), after up to three network or failover blips in a
|
|
1878
|
+
* row. Everything was applied — only the builds were not finished.
|
|
1879
|
+
*
|
|
1880
|
+
* `context.converge` is the {@link ConvergeResult} so far.
|
|
1628
1881
|
*/
|
|
1629
1882
|
export class ConvergeFailedError extends MigronautError {
|
|
1630
1883
|
constructor(message: string, context?: Record<string, unknown>, options?: MigronautErrorOptions);
|
package/migronaut.schema.json
CHANGED
|
@@ -163,6 +163,25 @@
|
|
|
163
163
|
"type": "boolean",
|
|
164
164
|
"default": false,
|
|
165
165
|
"description": "End every bulk `up` (no file, no --to) by converging the declared collections under the same lock"
|
|
166
|
+
},
|
|
167
|
+
"onSearchUnavailable": {
|
|
168
|
+
"enum": [
|
|
169
|
+
"fail",
|
|
170
|
+
"skip"
|
|
171
|
+
],
|
|
172
|
+
"default": "fail",
|
|
173
|
+
"description": "What converge does with declared search indexes on a server without Atlas Search: refuse the run (fail), or converge everything else and skip them (skip)"
|
|
174
|
+
},
|
|
175
|
+
"waitForSearchIndexes": {
|
|
176
|
+
"type": "boolean",
|
|
177
|
+
"default": false,
|
|
178
|
+
"description": "Hold converge until every declared search index is queryable (search indexes build in the background)"
|
|
179
|
+
},
|
|
180
|
+
"searchIndexWaitTimeoutMs": {
|
|
181
|
+
"type": "integer",
|
|
182
|
+
"minimum": 1,
|
|
183
|
+
"default": 600000,
|
|
184
|
+
"description": "How long waitForSearchIndexes waits, in milliseconds, before the converge fails (the build goes on)"
|
|
166
185
|
}
|
|
167
186
|
},
|
|
168
187
|
"definitions": {
|
|
@@ -182,6 +201,11 @@
|
|
|
182
201
|
"required": [
|
|
183
202
|
"validator"
|
|
184
203
|
]
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
"required": [
|
|
207
|
+
"searchIndexes"
|
|
208
|
+
]
|
|
185
209
|
}
|
|
186
210
|
],
|
|
187
211
|
"properties": {
|
|
@@ -198,6 +222,13 @@
|
|
|
198
222
|
},
|
|
199
223
|
"description": "Every index besides _id. Omit to leave the collection's indexes unmanaged"
|
|
200
224
|
},
|
|
225
|
+
"searchIndexes": {
|
|
226
|
+
"type": "array",
|
|
227
|
+
"items": {
|
|
228
|
+
"$ref": "#/definitions/searchIndex"
|
|
229
|
+
},
|
|
230
|
+
"description": "Atlas Search and Vector Search indexes. Omit to leave the collection's search indexes unmanaged (Atlas, an Atlas CLI local deployment, or MongoDB 8.3+ with mongot)"
|
|
231
|
+
},
|
|
201
232
|
"validator": {
|
|
202
233
|
"oneOf": [
|
|
203
234
|
{
|
|
@@ -227,7 +258,7 @@
|
|
|
227
258
|
},
|
|
228
259
|
"prune": {
|
|
229
260
|
"type": "boolean",
|
|
230
|
-
"description": "Drop indexes this definition does not declare
|
|
261
|
+
"description": "Drop indexes (and search indexes, when declared) this definition does not declare — otherwise they are kept and reported"
|
|
231
262
|
}
|
|
232
263
|
}
|
|
233
264
|
},
|
|
@@ -320,6 +351,31 @@
|
|
|
320
351
|
"description": "Accepted and ignored — a no-op since MongoDB 4.2"
|
|
321
352
|
}
|
|
322
353
|
}
|
|
354
|
+
},
|
|
355
|
+
"searchIndex": {
|
|
356
|
+
"type": "object",
|
|
357
|
+
"required": [
|
|
358
|
+
"definition"
|
|
359
|
+
],
|
|
360
|
+
"additionalProperties": false,
|
|
361
|
+
"properties": {
|
|
362
|
+
"name": {
|
|
363
|
+
"type": "string",
|
|
364
|
+
"minLength": 1,
|
|
365
|
+
"description": "Index name. Defaults to \"default\", as on the server"
|
|
366
|
+
},
|
|
367
|
+
"type": {
|
|
368
|
+
"enum": [
|
|
369
|
+
"search",
|
|
370
|
+
"vectorSearch"
|
|
371
|
+
],
|
|
372
|
+
"description": "Defaults to 'search'. Cannot change in place — declare a new index under a new name instead"
|
|
373
|
+
},
|
|
374
|
+
"definition": {
|
|
375
|
+
"type": "object",
|
|
376
|
+
"description": "The index definition, as Atlas defines it: { mappings, analyzer, … } for search, { fields: [...] } for vectorSearch"
|
|
377
|
+
}
|
|
378
|
+
}
|
|
323
379
|
}
|
|
324
380
|
}
|
|
325
381
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alexify/migronaut",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Elegant, fast, fully-typed, zero-dependency MongoDB migrations for Node.js — adopts an existing migrate-mongo changelog in one command",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Alex Dolid <dolid.sasha@gmail.com>",
|
package/src/bullmq/processor.js
CHANGED
|
@@ -253,12 +253,35 @@ function createMigrationProcessor(options = {}) {
|
|
|
253
253
|
},
|
|
254
254
|
'converge:action': (event) => {
|
|
255
255
|
if (!current) return;
|
|
256
|
-
const target =
|
|
256
|
+
const target =
|
|
257
|
+
event.target === 'index'
|
|
258
|
+
? `index ${event.name}`
|
|
259
|
+
: event.target === 'searchIndex'
|
|
260
|
+
? `search index ${event.name}`
|
|
261
|
+
: event.target;
|
|
257
262
|
const what = `${event.action} ${target} on ${event.collection}`;
|
|
258
263
|
if (event.status === 'started') log(current, `… ${what}`);
|
|
259
264
|
else if (event.status === 'applied') log(current, `✔ ${what} [${event.durationMs ?? 0}ms]`);
|
|
260
265
|
else log(current, `✖ ${what}: ${event.error ?? 'failed'}`);
|
|
261
266
|
},
|
|
267
|
+
'converge:wait': (event) => {
|
|
268
|
+
if (!current) return;
|
|
269
|
+
if (event.status === 'started' || event.status === 'progress') {
|
|
270
|
+
const waited = event.status === 'started' ? 0 : event.waitedMs;
|
|
271
|
+
log(
|
|
272
|
+
current,
|
|
273
|
+
event.status === 'started'
|
|
274
|
+
? `… Waiting for ${event.searchIndexes} search index(es) to become queryable` +
|
|
275
|
+
(event.lockReleased ? ' — the migration lock is released meanwhile' : '')
|
|
276
|
+
: `… Still waiting for search indexes [${Math.round(waited / 1000)}s]`,
|
|
277
|
+
);
|
|
278
|
+
progress(current, 'search-wait', { searchIndexes: event.searchIndexes, waitedMs: waited });
|
|
279
|
+
} else if (event.status === 'ready') {
|
|
280
|
+
log(current, `✔ Search index(es) queryable [${event.waitedMs}ms]`);
|
|
281
|
+
} else {
|
|
282
|
+
log(current, `✖ Wait for search indexes ended: ${event.status} [${event.waitedMs}ms]`);
|
|
283
|
+
}
|
|
284
|
+
},
|
|
262
285
|
'converge:end': (event) => {
|
|
263
286
|
if (current && event.success) log(current, `✔ Converged ${event.changed} change(s)`);
|
|
264
287
|
},
|
|
@@ -362,6 +385,7 @@ function createMigrationProcessor(options = {}) {
|
|
|
362
385
|
inSync: result.inSync,
|
|
363
386
|
collections: result.collections,
|
|
364
387
|
...(result.unstable ? { unstable: result.unstable } : {}),
|
|
388
|
+
...(result.search ? { search: result.search } : {}),
|
|
365
389
|
...(ctx.runId ? { runId: ctx.runId } : {}),
|
|
366
390
|
lockWaitMs: waitedMs,
|
|
367
391
|
});
|
package/src/bullmq/producer.js
CHANGED
|
@@ -252,10 +252,13 @@ async function planDownJobs(kit, options = {}) {
|
|
|
252
252
|
* that will do the work. All false on a queue that cannot read jobs back.
|
|
253
253
|
*/
|
|
254
254
|
async function foreignJobs(queue, ids, groupId) {
|
|
255
|
-
if (typeof queue.getJob !== 'function') return ids.
|
|
256
|
-
// A first deploy can enqueue hundreds of jobs: read them back a few at a time
|
|
257
|
-
|
|
258
|
-
return
|
|
255
|
+
if (typeof queue.getJob !== 'function') return new Array(ids.length).fill(false);
|
|
256
|
+
// A first deploy can enqueue hundreds of jobs: read them back a few at a time,
|
|
257
|
+
// each answered as it arrives.
|
|
258
|
+
return mapLimit(ids, LOOKUP_CONCURRENCY, async (id) => {
|
|
259
|
+
const job = await queue.getJob(id);
|
|
260
|
+
return Boolean(job && job.data?.groupId !== groupId);
|
|
261
|
+
});
|
|
259
262
|
}
|
|
260
263
|
|
|
261
264
|
/**
|
|
@@ -299,16 +302,16 @@ async function enqueueGroup(queue, kit, plan, { queueEvents, getQueueEvents } =
|
|
|
299
302
|
expected: specs.length,
|
|
300
303
|
});
|
|
301
304
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
);
|
|
305
|
+
// The ids of every job added, and the migration ones as jobs — one pass.
|
|
306
|
+
const ids = new Array(added.length);
|
|
307
|
+
jobs = new Array(plan.migrations.length);
|
|
308
|
+
for (const [index, job] of added.entries()) {
|
|
309
|
+
ids[index] = String(job.id);
|
|
310
|
+
if (index < jobs.length) {
|
|
311
|
+
jobs[index] = { id: ids[index], migration: plan.migrations[index], index };
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
const foreign = await foreignJobs(queue, ids, groupId);
|
|
312
315
|
for (const job of jobs) {
|
|
313
316
|
if (foreign[job.index]) deduplicated.push(job.migration);
|
|
314
317
|
}
|