@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.
Files changed (68) hide show
  1. package/CHANGELOG.md +190 -0
  2. package/README.md +41 -3
  3. package/bullmq.d.ts +484 -8
  4. package/index.d.ts +1264 -9
  5. package/migronaut.schema.json +93 -1
  6. package/package.json +9 -2
  7. package/src/bullmq/background-processor.js +541 -0
  8. package/src/bullmq/index.js +12 -0
  9. package/src/bullmq/jobs.js +254 -7
  10. package/src/bullmq/processor.js +348 -21
  11. package/src/bullmq/producer.js +185 -13
  12. package/src/bullmq/service.js +484 -45
  13. package/src/cli/commands/background.js +500 -0
  14. package/src/cli/commands/create.js +6 -0
  15. package/src/cli/exit-codes.js +6 -0
  16. package/src/cli/index.js +2 -0
  17. package/src/core/audit.js +11 -1
  18. package/src/core/background-audit.js +139 -0
  19. package/src/core/background-drift.js +126 -0
  20. package/src/core/background-dry-run.js +375 -0
  21. package/src/core/background-engine.js +849 -0
  22. package/src/core/background-kit.js +432 -0
  23. package/src/core/background-partition.js +298 -0
  24. package/src/core/background-runner.js +305 -0
  25. package/src/core/background-sandbox.js +701 -0
  26. package/src/core/background-shard.js +542 -0
  27. package/src/core/background-spec.js +597 -0
  28. package/src/core/background-store.js +951 -0
  29. package/src/core/background-throttle.js +269 -0
  30. package/src/core/background-watch-plan.js +164 -0
  31. package/src/core/background-watch-store.js +78 -0
  32. package/src/core/background-watch.js +610 -0
  33. package/src/core/background.js +1127 -0
  34. package/src/core/bson-peer.js +23 -0
  35. package/src/core/changelog.js +32 -0
  36. package/src/core/collections.js +78 -8
  37. package/src/core/config.js +102 -12
  38. package/src/core/converge-plan.js +86 -7
  39. package/src/core/converge.js +88 -0
  40. package/src/core/lock.js +48 -21
  41. package/src/core/migration-logger.js +279 -0
  42. package/src/core/migrator.js +1027 -22
  43. package/src/core/options.js +36 -0
  44. package/src/core/run-recorder.js +6 -1
  45. package/src/core/run.js +26 -12
  46. package/src/core/runner.js +34 -8
  47. package/src/core/server-info.js +9 -2
  48. package/src/core/shard-info.js +76 -0
  49. package/src/core/versioning-spec.js +181 -0
  50. package/src/errors/index.js +88 -0
  51. package/src/index.js +16 -0
  52. package/src/utils/error.js +11 -2
  53. package/src/utils/job-ref.js +44 -0
  54. package/src/utils/loader.js +77 -9
  55. package/src/utils/migration-name.js +33 -1
  56. package/src/utils/redact.js +140 -3
  57. package/src/utils/telemetry.js +110 -0
  58. package/src/utils/template.js +62 -1
  59. package/src/versioning/config.js +155 -0
  60. package/src/versioning/document.js +326 -0
  61. package/src/versioning/index.js +50 -0
  62. package/src/versioning/internal.js +279 -0
  63. package/src/versioning/mongoose.js +151 -0
  64. package/src/versioning/occ.js +318 -0
  65. package/src/versioning/registry.js +187 -0
  66. package/src/versioning/upcaster.js +213 -0
  67. package/versioning.d.ts +666 -0
  68. package/versioning.js +1 -0
package/src/core/lock.js CHANGED
@@ -8,9 +8,15 @@ const { errorText } = require('../utils/error.js');
8
8
  const { randomId } = require('../utils/id.js');
9
9
  const { safeUsername } = require('../utils/user.js');
10
10
 
11
- /** Fixed `_id` of the singleton lock document */
11
+ /** Fixed `_id` of the singleton migration lock document */
12
12
  const LOCK_ID = 'migronaut_lock';
13
13
 
14
+ /** What the migration lock is called in messages — another lock passes its own `label` */
15
+ const LOCK_LABEL = 'migration lock';
16
+
17
+ /** `migration lock` → `Migration lock`, for the start of a sentence */
18
+ const sentence = (label) => label.charAt(0).toUpperCase() + label.slice(1);
19
+
14
20
  /** Returns true when an error is a MongoDB duplicate-key error (code 11000) */
15
21
  function isDuplicateKeyError(error) {
16
22
  return typeof error === 'object' && error !== null && 'code' in error && error.code === 11000;
@@ -40,11 +46,19 @@ function toLockInfo(doc) {
40
46
  * MongoDB-native distributed lock backed by a single document, using an atomic
41
47
  * upsert as a test-and-set. A lock older than `ttlSeconds` is considered stale
42
48
  * and may be reclaimed.
49
+ *
50
+ * `options.id` is the lock document's `_id` (default: the migration lock's)
51
+ * and `options.label` what messages call it — so other locks (a background
52
+ * migration's coordinator, `background:<name>`) live next to the migration
53
+ * lock in the same collection without ever touching it: `unlock`, the audit
54
+ * and `forceRelease` of the migration lock only see its own document.
43
55
  */
44
56
  class MigrationLock {
45
57
  #db;
46
58
  #collectionName;
47
59
  #ttlSeconds;
60
+ #id;
61
+ #label;
48
62
  /** Token naming this instance as the current holder; set on acquire, cleared on release */
49
63
  #owner;
50
64
  /**
@@ -55,10 +69,22 @@ class MigrationLock {
55
69
  */
56
70
  #nonce;
57
71
 
58
- constructor(db, collectionName, ttlSeconds) {
72
+ constructor(db, collectionName, ttlSeconds, { id = LOCK_ID, label = LOCK_LABEL } = {}) {
59
73
  this.#db = db;
60
74
  this.#collectionName = collectionName;
61
75
  this.#ttlSeconds = ttlSeconds;
76
+ this.#id = id;
77
+ this.#label = label;
78
+ }
79
+
80
+ /** The lock document's `_id` */
81
+ get id() {
82
+ return this.#id;
83
+ }
84
+
85
+ /** What messages call this lock */
86
+ get label() {
87
+ return this.#label;
62
88
  }
63
89
 
64
90
  /** The token identifying this holder, or undefined when the lock is not held */
@@ -91,7 +117,7 @@ class MigrationLock {
91
117
  // strings: a pipeline expression would otherwise interpret a leading `$`
92
118
  // in a value as a field path.
93
119
  const lockDoc = {
94
- _id: LOCK_ID,
120
+ _id: this.#id,
95
121
  lockedAt: '$$NOW',
96
122
  pid: { $literal: process.pid },
97
123
  host: { $literal: os.hostname() },
@@ -109,7 +135,7 @@ class MigrationLock {
109
135
  // or the holder is stale; otherwise keep the current document untouched.
110
136
  // The read-back below tells those outcomes apart.
111
137
  result = await collection.updateOne(
112
- { _id: LOCK_ID },
138
+ { _id: this.#id },
113
139
  [
114
140
  {
115
141
  $replaceWith: {
@@ -131,8 +157,8 @@ class MigrationLock {
131
157
  } catch (error) {
132
158
  if (isDuplicateKeyError(error)) {
133
159
  // Two processes raced the very first insert; the loser lands here.
134
- const holder = await collection.findOne({ _id: LOCK_ID });
135
- throw new LockAlreadyHeldError('Migration lock is already held', {
160
+ const holder = await collection.findOne({ _id: this.#id });
161
+ throw new LockAlreadyHeldError(`${sentence(this.#label)} is already held`, {
136
162
  holder: toLockInfo(holder) ?? undefined,
137
163
  ttlMs: this.ttlMs,
138
164
  });
@@ -155,9 +181,9 @@ class MigrationLock {
155
181
  // writer's document wins; either way the loser reads a different one here
156
182
  // and backs off instead of running concurrently. The nonce is what makes
157
183
  // that hold when both carry the same owner token.
158
- const current = await collection.findOne({ _id: LOCK_ID });
184
+ const current = await collection.findOne({ _id: this.#id });
159
185
  if (!current || current.owner !== owner || current.nonce !== nonce) {
160
- throw new LockAlreadyHeldError('Migration lock is already held', {
186
+ throw new LockAlreadyHeldError(`${sentence(this.#label)} is already held`, {
161
187
  holder: toLockInfo(current) ?? undefined,
162
188
  ttlMs: this.ttlMs,
163
189
  });
@@ -178,7 +204,7 @@ class MigrationLock {
178
204
  }
179
205
  const result = await this.#db
180
206
  .collection(this.#collectionName)
181
- .updateOne({ _id: LOCK_ID, owner: this.#owner, nonce: this.#nonce }, [
207
+ .updateOne({ _id: this.#id, owner: this.#owner, nonce: this.#nonce }, [
182
208
  { $set: { lockedAt: '$$NOW' } },
183
209
  ]);
184
210
  return result.matchedCount === 1;
@@ -186,7 +212,7 @@ class MigrationLock {
186
212
 
187
213
  /** Read the current lock document, or null when no lock is held */
188
214
  async inspect() {
189
- return this.#db.collection(this.#collectionName).findOne({ _id: LOCK_ID });
215
+ return this.#db.collection(this.#collectionName).findOne({ _id: this.#id });
190
216
  }
191
217
 
192
218
  /**
@@ -196,8 +222,8 @@ class MigrationLock {
196
222
  */
197
223
  async forceRelease() {
198
224
  const collection = this.#db.collection(this.#collectionName);
199
- const existing = await collection.findOne({ _id: LOCK_ID });
200
- await collection.deleteOne({ _id: LOCK_ID });
225
+ const existing = await collection.findOne({ _id: this.#id });
226
+ await collection.deleteOne({ _id: this.#id });
201
227
  return existing;
202
228
  }
203
229
 
@@ -213,14 +239,14 @@ class MigrationLock {
213
239
  */
214
240
  async release() {
215
241
  if (!this.#owner) return;
216
- const filter = { _id: LOCK_ID, owner: this.#owner, nonce: this.#nonce };
242
+ const filter = { _id: this.#id, owner: this.#owner, nonce: this.#nonce };
217
243
  try {
218
244
  await this.#db.collection(this.#collectionName).deleteOne(filter);
219
245
  this.#owner = undefined;
220
246
  this.#nonce = undefined;
221
247
  } catch (error) {
222
248
  throw new LockReleaseFailedError(
223
- 'Failed to release migration lock',
249
+ `Failed to release ${this.#label}`,
224
250
  { error: errorText(error) },
225
251
  { cause: error },
226
252
  );
@@ -253,6 +279,9 @@ class MigrationLock {
253
279
  */
254
280
  async function runWithLock(lock, options, fn) {
255
281
  const controller = new AbortController();
282
+ // Any object with acquire/renew/release/ttlMs is a lock here — a partition
283
+ // lease included; `label` names it in the messages.
284
+ const label = typeof lock?.label === 'string' ? lock.label : LOCK_LABEL;
256
285
 
257
286
  if (options.noLock) {
258
287
  options.logger.warn('⚠ Running without a lock (--no-lock) — concurrent runs are unsafe', {
@@ -278,15 +307,13 @@ async function runWithLock(lock, options, fn) {
278
307
  const loseLock = (reason) => {
279
308
  // The most alert-worthy line this module emits — structured fields so a
280
309
  // JSON sink can trigger on it without parsing the human string.
281
- options.logger.warn(`⚠ Lost the migration lock mid-run (${reason})`, {
310
+ options.logger.warn(`⚠ Lost the ${label} mid-run (${reason})`, {
282
311
  event: 'lock:lost',
283
312
  reason,
284
313
  });
285
314
  options.onLockLostEvent?.(reason);
286
315
  if (abortOnLoss && !controller.signal.aborted) {
287
- controller.abort(
288
- new LockLostError('Lost the migration lock mid-run', { reason, aborted: true }),
289
- );
316
+ controller.abort(new LockLostError(`Lost the ${label} mid-run`, { reason, aborted: true }));
290
317
  }
291
318
  };
292
319
 
@@ -381,7 +408,7 @@ async function runWithLock(lock, options, fn) {
381
408
  } catch (releaseError) {
382
409
  const message = errorText(releaseError);
383
410
  options.logger.warn(
384
- `⚠ Failed to release the migration lock early: ${message} — it frees itself once ` +
411
+ `⚠ Failed to release the ${label} early: ${message} — it frees itself once ` +
385
412
  'its TTL runs out',
386
413
  { event: 'lock:release-failed', error: message, early: true },
387
414
  );
@@ -424,7 +451,7 @@ async function runWithLock(lock, options, fn) {
424
451
  throw releaseError;
425
452
  }
426
453
  const message = errorText(releaseError);
427
- options.logger.warn(`⚠ Failed to release the migration lock: ${message}`, {
454
+ options.logger.warn(`⚠ Failed to release the ${label}: ${message}`, {
428
455
  event: 'lock:release-failed',
429
456
  error: message,
430
457
  });
@@ -434,4 +461,4 @@ async function runWithLock(lock, options, fn) {
434
461
  return result;
435
462
  }
436
463
 
437
- module.exports = { LOCK_ID, MigrationLock, runWithLock, toLockInfo };
464
+ module.exports = { LOCK_ID, LOCK_LABEL, MigrationLock, runWithLock, toLockInfo };
@@ -0,0 +1,279 @@
1
+ const { isPlainObject } = require('../utils/canonical.js');
2
+ const { errorText } = require('../utils/error.js');
3
+ const { jobFields } = require('../utils/job-ref.js');
4
+ const { BOUNDS, redactBounded, redactOutbound } = require('../utils/redact.js');
5
+
6
+ /**
7
+ * The logger a migration gets as `ctx.logger`, and the `migration:log` event.
8
+ *
9
+ * Every call is a line on the kit's own logger with the run's correlation
10
+ * bound into its fields (run id, migration, direction, batch, attempt, job) —
11
+ * a JSON sink can join it to the kit's lines, the changelog and the queue job
12
+ * without parsing anything. A call whose fields say `userland: true` is also
13
+ * emitted as `migration:log`, for the application to keep: migronaut stores
14
+ * none of it. The marker is per call, so operational noise stays in the logs
15
+ * and only what the author meant for their users reaches the event.
16
+ *
17
+ * Mechanism only: no kit, no decisions about where lines go — `sink` is the
18
+ * kit's resolved logger, `emitter` its event channel.
19
+ */
20
+
21
+ const MIGRATION_LOG_EVENT = 'migration:log';
22
+ const USERLAND = 'userland';
23
+
24
+ /** The longest message an event carries; the log line keeps the whole one */
25
+ const MAX_MESSAGE_LENGTH = 2048;
26
+
27
+ /** A counter, one per run (or lane slice): orders a run's events within one millisecond */
28
+ function sequence() {
29
+ let next = 0;
30
+ return () => {
31
+ next += 1;
32
+ return next;
33
+ };
34
+ }
35
+
36
+ /**
37
+ * The run-level correlation of an ordinary run, frozen — `ctx.run` in
38
+ * `beforeAll`/`afterAll`, and what every migration's own adds to.
39
+ * `base` holds the run id and, when the caller gave them, the job and the
40
+ * actor: `{ id, jobId?, groupId?, requestedBy?, reason? }`.
41
+ */
42
+ function runInfo(base, direction) {
43
+ return Object.freeze({ id: base.id, direction, ...withoutId(base) });
44
+ }
45
+
46
+ /** `ctx.run` of one attempt of one migration — a new frozen object per attempt */
47
+ function migrationRunInfo(run, { migration, batch, attempt }) {
48
+ return Object.freeze({
49
+ ...run,
50
+ migration,
51
+ ...(batch !== undefined ? { batch } : {}),
52
+ attempt,
53
+ });
54
+ }
55
+
56
+ function withoutId(base) {
57
+ const rest = {};
58
+ for (const key of Object.keys(base)) {
59
+ if (key !== 'id' && base[key] !== undefined) rest[key] = base[key];
60
+ }
61
+ return rest;
62
+ }
63
+
64
+ /** Keys of `info` a log line leaves out: who asked and why go to the event only */
65
+ const EVENT_ONLY = new Set(['requestedBy', 'reason']);
66
+
67
+ /**
68
+ * The one mapping from a context's correlation (`ctx.run`, or a background
69
+ * migration's `ctx.background`) to what a log line binds and what the event
70
+ * carries — so the three can never disagree. `id` becomes `runId`, a
71
+ * background `name` becomes `migration`; undefined values are left out.
72
+ */
73
+ function correlationOf(kind, info, direction) {
74
+ const event = { kind };
75
+ const line = {};
76
+ const put = (key, value) => {
77
+ if (value === undefined) return;
78
+ event[key] = value;
79
+ if (!EVENT_ONLY.has(key)) line[key] = value;
80
+ };
81
+ if (kind === 'background') {
82
+ put('runId', info.runId);
83
+ put('migration', info.name);
84
+ put('direction', direction);
85
+ for (const key of Object.keys(info)) {
86
+ if (key !== 'runId' && key !== 'name') put(key, info[key]);
87
+ }
88
+ } else {
89
+ put('runId', info.id);
90
+ for (const key of Object.keys(info)) {
91
+ if (key !== 'id') put(key, info[key]);
92
+ }
93
+ }
94
+ return { event, line };
95
+ }
96
+
97
+ /** A log message as text: an Error by its (redacted) message, anything else stringified */
98
+ function messageText(msg) {
99
+ if (typeof msg === 'string') return msg;
100
+ if (msg instanceof Error) return errorText(msg);
101
+ return msg === undefined ? '' : String(msg);
102
+ }
103
+
104
+ /**
105
+ * What a run's loggers have already said about themselves — one debug line
106
+ * each for a call dropped and a call cut to the event's bounds, per run (or
107
+ * lane slice), not per call: `{ dropped, truncated }`.
108
+ */
109
+ function notices() {
110
+ return { dropped: false, truncated: false };
111
+ }
112
+
113
+ /**
114
+ * A logger bound to one context's correlation.
115
+ *
116
+ * - `sink` — the kit's resolved logger (already guarded, `null` config → silent);
117
+ * - `kind` — `'migration'` or `'background'`, and `info` — the frozen
118
+ * `ctx.run` / `ctx.background` (`direction` given separately for background);
119
+ * - `emitter` — `{ wanted(), emit(payload) }`, absent where nothing may be
120
+ * emitted (a dry run); `nextSeq` — the run's counter;
121
+ * - `noticed` — the run's {@link notices}: a dropped or a cut call leaves one
122
+ * debug line, since an invisible one is undebuggable;
123
+ * - `dryRun` — marks every line, and emits nothing.
124
+ *
125
+ * Pino's own argument order — `(fields, msg)` — is accepted too.
126
+ */
127
+ function createMigrationLogger({
128
+ sink,
129
+ kind,
130
+ info,
131
+ direction,
132
+ emitter,
133
+ nextSeq,
134
+ noticed = notices(),
135
+ dryRun = false,
136
+ }) {
137
+ const { event, line } = correlationOf(kind, info, direction);
138
+ if (dryRun) line.dryRun = true;
139
+ /** The one debug line for `what` (a key of `noticed`) — the call that caused it goes on */
140
+ const notice = (what, text) => {
141
+ if (noticed[what]) return;
142
+ noticed[what] = true;
143
+ sink.debug(text, { ...line });
144
+ };
145
+ const write = (level) => (first, second) => {
146
+ // Logging must never break a migration: a getter that throws, a message
147
+ // that cannot be stringified — the call is dropped, the run goes on.
148
+ try {
149
+ let msg = first;
150
+ let fields = second;
151
+ if (isPlainObject(first) && (second === undefined || typeof second === 'string')) {
152
+ msg = second;
153
+ fields = first;
154
+ }
155
+ const text = messageText(msg);
156
+ const plain = isPlainObject(fields);
157
+ if (plain) sink[level](text, { ...fields, ...line });
158
+ else if (fields === undefined) sink[level](text, { ...line });
159
+ else sink[level](text, { ...line, value: fields });
160
+ if (!plain || fields[USERLAND] !== true || !emitter || dryRun || !emitter.wanted()) return;
161
+ const data = redactBounded(fields, { omit: USERLAND });
162
+ // The event leaves the process: the values a server error quotes (an
163
+ // E11000's duplicate key) are masked too, as everywhere text leaves it.
164
+ // The log line keeps them — it is what a developer debugs with.
165
+ const message = redactOutbound(text);
166
+ const clipped = message.length > MAX_MESSAGE_LENGTH;
167
+ const truncated = data.truncated || clipped;
168
+ emitter.emit({
169
+ ...event,
170
+ level,
171
+ msg: clipped ? `${message.slice(0, MAX_MESSAGE_LENGTH)}…` : message,
172
+ data: data.value,
173
+ at: new Date(),
174
+ seq: nextSeq(),
175
+ ...(truncated ? { truncated: true } : {}),
176
+ });
177
+ if (truncated) {
178
+ notice(
179
+ 'truncated',
180
+ 'A ctx.logger call was cut to the bounds of its migration:log event ' +
181
+ `(message ${MAX_MESSAGE_LENGTH} characters; data ${BOUNDS.depth} levels, ` +
182
+ `${BOUNDS.entries} entries, ${BOUNDS.string}-character strings)`,
183
+ );
184
+ }
185
+ } catch (error) {
186
+ // See above: dropped — but said once, at debug level.
187
+ try {
188
+ notice('dropped', `A ctx.logger call was dropped: ${errorText(error)}`);
189
+ } catch {
190
+ // Not even that: the thrown value cannot be described.
191
+ }
192
+ }
193
+ };
194
+ return { debug: write('debug'), info: write('info'), warn: write('warn'), error: write('error') };
195
+ }
196
+
197
+ /**
198
+ * The log side of one ordinary run, made when it takes its id — what its
199
+ * migrations' contexts and the kit's own lines bind:
200
+ *
201
+ * - `job` — the queue job it works for (`{ id, groupId? }`), a copy, as a run
202
+ * it drives inline hands it on; `fields` — the same as log fields
203
+ * (`{ jobId?, groupId? }`), on every line of the kit's for the run;
204
+ * - `info(direction)` — `ctx.run` of the run itself (`beforeAll`/`afterAll`),
205
+ * made once per direction (`redo` has two);
206
+ * - `logger(info)` — `ctx.logger` bound to `info`;
207
+ * - `attempt(direction, { migration, batch, attempt })` — what one attempt
208
+ * of one migration adds to its context: `{ run, logger }`.
209
+ *
210
+ * Every logger of the run shares its counter and its notices, so `seq` runs
211
+ * across the whole run and a dropped call is said once. A logger keeps them
212
+ * after the run ended (a timed-out body's late call is still numbered).
213
+ */
214
+ function createRunLog({ id, job, actor = {}, sink, emitter }) {
215
+ const ref =
216
+ job === undefined
217
+ ? undefined
218
+ : { id: job.id, ...(job.groupId !== undefined ? { groupId: job.groupId } : {}) };
219
+ const fields = jobFields(ref);
220
+ const base = { id, ...fields, ...actor };
221
+ const nextSeq = sequence();
222
+ const noticed = notices();
223
+ const infos = new Map();
224
+ const info = (direction) => {
225
+ let run = infos.get(direction);
226
+ if (run === undefined) {
227
+ run = runInfo(base, direction);
228
+ infos.set(direction, run);
229
+ }
230
+ return run;
231
+ };
232
+ const logger = (bound) =>
233
+ createMigrationLogger({ sink, kind: 'migration', info: bound, emitter, nextSeq, noticed });
234
+ return {
235
+ job: ref,
236
+ fields,
237
+ info,
238
+ logger,
239
+ attempt(direction, { migration, batch, attempt }) {
240
+ const run = migrationRunInfo(info(direction), { migration, batch, attempt });
241
+ return { run, logger: logger(run) };
242
+ },
243
+ };
244
+ }
245
+
246
+ /**
247
+ * `logs(info, direction)` for one background lane slice, live watcher or dry
248
+ * run: a `ctx.logger` per `ctx.background`, all sharing one counter and one
249
+ * set of notices. A dry run's mark every line and emit nothing.
250
+ */
251
+ function backgroundLogs({ sink, emitter, dryRun = false }) {
252
+ const nextSeq = sequence();
253
+ const noticed = notices();
254
+ return (info, direction) =>
255
+ createMigrationLogger({
256
+ sink,
257
+ kind: 'background',
258
+ info,
259
+ direction,
260
+ ...(dryRun ? {} : { emitter }),
261
+ nextSeq,
262
+ noticed,
263
+ dryRun,
264
+ });
265
+ }
266
+
267
+ module.exports = {
268
+ MAX_MESSAGE_LENGTH,
269
+ MIGRATION_LOG_EVENT,
270
+ USERLAND,
271
+ backgroundLogs,
272
+ correlationOf,
273
+ createMigrationLogger,
274
+ createRunLog,
275
+ migrationRunInfo,
276
+ notices,
277
+ runInfo,
278
+ sequence,
279
+ };