@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
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
const {
|
|
2
|
+
BackgroundConflictError,
|
|
3
|
+
ChecksumMismatchError,
|
|
4
|
+
IrreversibleMigrationError,
|
|
5
|
+
RunAbortedError,
|
|
6
|
+
} = require('../errors/index.js');
|
|
7
|
+
const { errorText } = require('../utils/error.js');
|
|
8
|
+
const {
|
|
9
|
+
control,
|
|
10
|
+
coordinate,
|
|
11
|
+
failedError,
|
|
12
|
+
requiresStatus,
|
|
13
|
+
runSlice,
|
|
14
|
+
tryUnblock,
|
|
15
|
+
waitForLanes,
|
|
16
|
+
} = require('./background.js');
|
|
17
|
+
const { stillDirty } = require('./background-drift.js');
|
|
18
|
+
const { sleep } = require('./background-throttle.js');
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The kit's background side — what MigratorKit's background methods do
|
|
22
|
+
* beyond plumbing: driving lanes in this process, a `down` without a
|
|
23
|
+
* revert, the `requires` guard, and the public views of states, partitions
|
|
24
|
+
* and watchers. A flow like background.js: it gets what it needs from the
|
|
25
|
+
* kit (`host`), built only by migrator.js:
|
|
26
|
+
*
|
|
27
|
+
* `{ store, deps(owner?, { job }?), newId(), logger, emit(event, payload),
|
|
28
|
+
* registered(name), status(name) }` — `deps` is background.js's deps (`job`:
|
|
29
|
+
* the queue job a run driving it inline works for), `registered` the state or
|
|
30
|
+
* NotAppliedError, `status` the public view.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Slice errors no retry fixes: the file changed or is gone or invalid, the
|
|
35
|
+
* configuration or the deployment cannot run it. A lane ends on them.
|
|
36
|
+
*/
|
|
37
|
+
const FATAL_SLICE_CODES = new Set([
|
|
38
|
+
'CHECKSUM_MISMATCH',
|
|
39
|
+
'MIGRATION_FILE_NOT_FOUND',
|
|
40
|
+
'MIGRATION_INVALID_EXPORT',
|
|
41
|
+
'MIGRATION_INVALID_NAME',
|
|
42
|
+
'CONFIG_INVALID',
|
|
43
|
+
'TRANSACTIONS_UNSUPPORTED',
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
/** Failed slices in a row after which a lane of runBackground gives up */
|
|
47
|
+
const MAX_LANE_FAILURES = 10;
|
|
48
|
+
|
|
49
|
+
/** A lane's backoff after a failed slice: 250 ms · 2ⁿ, up to this */
|
|
50
|
+
const MAX_LANE_BACKOFF_MS = 30_000;
|
|
51
|
+
|
|
52
|
+
/** Statuses a `down` without a revert pauses before it judges whether anything was rewritten */
|
|
53
|
+
const PAUSABLE = new Set(['blocked', 'pending', 'running']);
|
|
54
|
+
|
|
55
|
+
/** How recent a streaming watcher's record must be for the poll to leave its collection alone */
|
|
56
|
+
const STREAMING_FRESH_MS = 60_000;
|
|
57
|
+
|
|
58
|
+
/** A background migration that has rewritten documents and declares no way back */
|
|
59
|
+
const irreversibleBackground = (name) =>
|
|
60
|
+
new IrreversibleMigrationError(
|
|
61
|
+
`Background migration ${name} has already rewritten documents and declares no revert — ` +
|
|
62
|
+
'write a background migration back instead',
|
|
63
|
+
{ names: [name] },
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
// ─── Driving it from this process ─────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* runBackground's loop: the coordinator and up to `concurrency` lanes until
|
|
70
|
+
* it is done (`untilDone`) or for one round. `inline` (a run waiting for it
|
|
71
|
+
* under the migration lock) cannot wait out a file that changed on disk
|
|
72
|
+
* since it was registered — that wait is for a deploy in progress, and this
|
|
73
|
+
* run is the deploy — so it fails instead. `job`: the queue job that run works
|
|
74
|
+
* for, named on every lane's lines and `migration:log` events.
|
|
75
|
+
*/
|
|
76
|
+
async function drive(
|
|
77
|
+
host,
|
|
78
|
+
name,
|
|
79
|
+
{ signal, sliceMs, untilDone = true, concurrency = 1, inline, job },
|
|
80
|
+
) {
|
|
81
|
+
const jobOption = job ? { job } : {};
|
|
82
|
+
const deps = host.deps(host.newId(), jobOption);
|
|
83
|
+
const stopped = () =>
|
|
84
|
+
new RunAbortedError(`Stopped driving background migration ${name} — it goes on from here`, {
|
|
85
|
+
migration: name,
|
|
86
|
+
});
|
|
87
|
+
for (;;) {
|
|
88
|
+
if (signal?.aborted) throw stopped();
|
|
89
|
+
const answer = await coordinate(deps, name, {
|
|
90
|
+
signal,
|
|
91
|
+
driver: { kind: inline ? 'inline' : 'local' },
|
|
92
|
+
});
|
|
93
|
+
if (answer.next === 'done' || answer.next === 'superseded') break;
|
|
94
|
+
if (answer.next === 'process') {
|
|
95
|
+
const state = await host.registered(name);
|
|
96
|
+
const count = Math.max(1, Math.min(concurrency, state.spec?.maxParallel ?? 1));
|
|
97
|
+
await lanes(host, name, count, { signal, sliceMs, untilDone, jobOption });
|
|
98
|
+
} else {
|
|
99
|
+
if (inline && answer.reason === 'checksum') {
|
|
100
|
+
throw new ChecksumMismatchError(
|
|
101
|
+
`Background migration ${name} changed on disk since it was registered — it cannot ` +
|
|
102
|
+
'run inline until it is pinned again (migronaut background repin)',
|
|
103
|
+
{ migration: name, background: true },
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
if (!untilDone) break;
|
|
107
|
+
try {
|
|
108
|
+
await sleep(answer.retryAfterMs ?? 1000, signal);
|
|
109
|
+
} catch {
|
|
110
|
+
throw stopped();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (!untilDone) break;
|
|
114
|
+
}
|
|
115
|
+
if (signal?.aborted) throw stopped();
|
|
116
|
+
const state = await host.registered(name);
|
|
117
|
+
if (state.status === 'failed') throw failedError(state);
|
|
118
|
+
return host.status(name);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** `count` lanes at once: the first that fails for good stops the others, and its error is thrown */
|
|
122
|
+
async function lanes(host, name, count, { signal, sliceMs, untilDone, jobOption }) {
|
|
123
|
+
const stop = new AbortController();
|
|
124
|
+
const laneSignal = signal ? AbortSignal.any([signal, stop.signal]) : stop.signal;
|
|
125
|
+
const running = [];
|
|
126
|
+
for (let i = 0; i < count; i++) {
|
|
127
|
+
running.push(
|
|
128
|
+
lane(host, name, { signal: laneSignal, sliceMs, untilDone, jobOption }).catch((error) => {
|
|
129
|
+
if (!stop.signal.aborted) stop.abort(error);
|
|
130
|
+
throw error;
|
|
131
|
+
}),
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
const settled = await Promise.allSettled(running);
|
|
135
|
+
for (const result of settled) if (result.status === 'rejected') throw result.reason;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* One lane: slices until nothing is left to claim (or one, without
|
|
140
|
+
* untilDone). A failed slice is counted on its partition (which fails after
|
|
141
|
+
* `maxSliceFailures` in a row) and retried after a backoff; an error no
|
|
142
|
+
* retry can fix — the file changed or is gone, the deployment cannot run it
|
|
143
|
+
* — ends the lane, and so do `MAX_LANE_FAILURES` in a row.
|
|
144
|
+
*/
|
|
145
|
+
async function lane(host, name, { signal, sliceMs, untilDone, jobOption = {} }) {
|
|
146
|
+
let failures = 0;
|
|
147
|
+
for (;;) {
|
|
148
|
+
if (signal?.aborted) return;
|
|
149
|
+
const owner = host.newId();
|
|
150
|
+
let slice;
|
|
151
|
+
try {
|
|
152
|
+
slice = await runSlice(host.deps(owner, jobOption), name, { signal, sliceMs, owner });
|
|
153
|
+
failures = 0;
|
|
154
|
+
} catch (error) {
|
|
155
|
+
if (signal?.aborted) return;
|
|
156
|
+
failures += 1;
|
|
157
|
+
if (FATAL_SLICE_CODES.has(error?.code) || failures >= MAX_LANE_FAILURES) throw error;
|
|
158
|
+
host.logger.warn(
|
|
159
|
+
`⚠ Background migration ${name}: a slice failed (${errorText(error)}) — retrying`,
|
|
160
|
+
{ background: name, runId: owner, error: errorText(error) },
|
|
161
|
+
);
|
|
162
|
+
if (!untilDone) return;
|
|
163
|
+
try {
|
|
164
|
+
await sleep(Math.min(MAX_LANE_BACKOFF_MS, 250 * 2 ** failures), signal);
|
|
165
|
+
} catch {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (!untilDone) return;
|
|
171
|
+
if (slice.outcome === 'yielded') continue;
|
|
172
|
+
if (slice.outcome === 'busy') {
|
|
173
|
+
try {
|
|
174
|
+
await sleep(slice.retryAfterMs ?? 1000, signal);
|
|
175
|
+
} catch {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ─── down, and requires ───────────────────────────────────────────────────────
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Whether a background migration has written anything a `down` without a
|
|
188
|
+
* revert could not put back: documents rewritten — or, for a step
|
|
189
|
+
* migration (which says how many it rewrote only when it wants to), any
|
|
190
|
+
* step checkpointed at all.
|
|
191
|
+
*/
|
|
192
|
+
async function hasRewritten(store, name, spec) {
|
|
193
|
+
const { migrated, batches } = await store.progress(name);
|
|
194
|
+
return spec.mode === 'step' ? batches > 0 : migrated > 0;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* `down` of a background migration without a revert: withdrawn — but only
|
|
199
|
+
* while nothing has been rewritten, since nothing could put it back. A plan
|
|
200
|
+
* never committed means no lane ever claimed anything: one conditional
|
|
201
|
+
* delete. Otherwise the lanes are paused and waited for first, so the check
|
|
202
|
+
* sees every batch they wrote; a refused `down` resumes it.
|
|
203
|
+
*/
|
|
204
|
+
async function withdraw(host, name, spec, session) {
|
|
205
|
+
const { store } = host;
|
|
206
|
+
const withdrawn = { status: 'withdrawn', direction: 'forward' };
|
|
207
|
+
const state = await store.get(name);
|
|
208
|
+
if (state === null) return withdrawn;
|
|
209
|
+
if (await hasRewritten(store, name, spec)) throw irreversibleBackground(name);
|
|
210
|
+
if (
|
|
211
|
+
(state.generation ?? 0) === 0 &&
|
|
212
|
+
(await store.remove(name, { session, filter: { generation: 0 } }))
|
|
213
|
+
) {
|
|
214
|
+
return withdrawn;
|
|
215
|
+
}
|
|
216
|
+
const deps = host.deps();
|
|
217
|
+
const paused = PAUSABLE.has(state.status)
|
|
218
|
+
? (await control(deps, name, 'pause', { reason: 'down withdraws it' })).applied === 'changed'
|
|
219
|
+
: false;
|
|
220
|
+
const stopped = await waitForLanes(deps, name);
|
|
221
|
+
if (!stopped || (await hasRewritten(store, name, spec))) {
|
|
222
|
+
if (paused) await control(deps, name, 'resume', { reason: 'down refused' });
|
|
223
|
+
if (!stopped) {
|
|
224
|
+
throw new BackgroundConflictError(
|
|
225
|
+
`Background migration ${name} still has lanes at work — down cannot tell whether ` +
|
|
226
|
+
'they rewrote documents; try again once they stop',
|
|
227
|
+
{ migration: name, action: 'withdraw' },
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
throw irreversibleBackground(name);
|
|
231
|
+
}
|
|
232
|
+
await store.remove(name, { session });
|
|
233
|
+
return withdrawn;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* The background migrations of `requires` not done yet:
|
|
238
|
+
* `[{ migration, status }]`. A completed one is checked against its data —
|
|
239
|
+
* and, unless this is only a preview (`reopen: false`), reopened when old
|
|
240
|
+
* shapes reappeared.
|
|
241
|
+
*/
|
|
242
|
+
async function unsatisfied(host, requires, { reopen = true } = {}) {
|
|
243
|
+
const deps = host.deps();
|
|
244
|
+
const waiting = [];
|
|
245
|
+
for (const row of await requiresStatus(deps, requires)) {
|
|
246
|
+
if (!row.done) {
|
|
247
|
+
waiting.push({ migration: row.migration, status: row.status });
|
|
248
|
+
} else if (row.state !== undefined && (await stillDirty(deps, row.state))) {
|
|
249
|
+
if (reopen) {
|
|
250
|
+
await control(deps, row.migration, 'retry', { reason: 'old-shape documents reappeared' });
|
|
251
|
+
host.emit('background:drift', {
|
|
252
|
+
migration: row.migration,
|
|
253
|
+
source: 'requires',
|
|
254
|
+
action: 'reopened',
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
waiting.push({ migration: row.migration, status: reopen ? 'running' : 'completed' });
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return waiting;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ─── What it says ─────────────────────────────────────────────────────────────
|
|
264
|
+
|
|
265
|
+
/** Who drove it last — and a queue chain's round */
|
|
266
|
+
function coordinatorView(state) {
|
|
267
|
+
return state.coordinator
|
|
268
|
+
? {
|
|
269
|
+
coordinator: {
|
|
270
|
+
kind: state.coordinator.kind,
|
|
271
|
+
...(state.round !== undefined ? { round: state.round } : {}),
|
|
272
|
+
at: state.coordinator.at,
|
|
273
|
+
},
|
|
274
|
+
}
|
|
275
|
+
: {};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** How a background migration's plan used the shard key — for its status */
|
|
279
|
+
function shardingView(sharding) {
|
|
280
|
+
let hashed = false;
|
|
281
|
+
for (const value of Object.values(sharding.key ?? {})) if (value === 'hashed') hashed = true;
|
|
282
|
+
return {
|
|
283
|
+
mode: sharding.mode,
|
|
284
|
+
...(sharding.key !== undefined ? { shardKey: sharding.key, hashed } : {}),
|
|
285
|
+
...(sharding.groups !== undefined ? { groups: sharding.groups } : {}),
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** The public view of a state document, with its current plan's partition counts */
|
|
290
|
+
async function stateView(store, state) {
|
|
291
|
+
const spec = state.spec ?? {};
|
|
292
|
+
const counts =
|
|
293
|
+
state.plan !== undefined
|
|
294
|
+
? await store.partitionCounts(state._id, {
|
|
295
|
+
generation: state.generation,
|
|
296
|
+
plan: state.plan.token,
|
|
297
|
+
})
|
|
298
|
+
: undefined;
|
|
299
|
+
const leases = await store.leases(state._id);
|
|
300
|
+
return {
|
|
301
|
+
migration: state._id,
|
|
302
|
+
status: state.status,
|
|
303
|
+
phase: state.phase,
|
|
304
|
+
direction: state.direction ?? 'forward',
|
|
305
|
+
registration: state.registration,
|
|
306
|
+
mode: state.mode ?? spec.mode,
|
|
307
|
+
...(state.collection !== undefined ? { collection: state.collection } : {}),
|
|
308
|
+
...(spec.from !== undefined ? { from: spec.from, to: spec.to } : {}),
|
|
309
|
+
generation: state.generation ?? 0,
|
|
310
|
+
pass: state.pass ?? 0,
|
|
311
|
+
maxParallel: spec.maxParallel ?? 1,
|
|
312
|
+
transaction: Boolean(spec.transaction),
|
|
313
|
+
totals: { ...state.totals },
|
|
314
|
+
failedDocuments: (state.badIds ?? []).length,
|
|
315
|
+
requires: state.requires ?? [],
|
|
316
|
+
waitsFor: state.waitsFor ?? [],
|
|
317
|
+
...(counts ? { partitions: counts } : {}),
|
|
318
|
+
liveLeases: leases.live,
|
|
319
|
+
...coordinatorView(state),
|
|
320
|
+
...(state.plan
|
|
321
|
+
? {
|
|
322
|
+
plan: {
|
|
323
|
+
method: state.plan.method,
|
|
324
|
+
estimate: state.plan.estimate,
|
|
325
|
+
...(state.plan.atLeast ? { atLeast: true } : {}),
|
|
326
|
+
partitions: state.plan.partitions,
|
|
327
|
+
...(state.plan.degraded ? { degraded: state.plan.degraded } : {}),
|
|
328
|
+
},
|
|
329
|
+
}
|
|
330
|
+
: {}),
|
|
331
|
+
...(state.plan?.sharding ? { sharding: shardingView(state.plan.sharding) } : {}),
|
|
332
|
+
registeredAt: state.registeredAt,
|
|
333
|
+
...(state.startedAt ? { startedAt: state.startedAt } : {}),
|
|
334
|
+
...(state.completedAt ? { completedAt: state.completedAt } : {}),
|
|
335
|
+
...(state.lastProgressAt ? { lastProgressAt: state.lastProgressAt } : {}),
|
|
336
|
+
...(state.lastError ? { lastError: state.lastError } : {}),
|
|
337
|
+
...(state.description ? { description: state.description } : {}),
|
|
338
|
+
...(state.previous ? { previous: { ...state.previous } } : {}),
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** A partition as `backgroundPartitions` shows it — its lease holder, never the token */
|
|
343
|
+
function partitionView(partition) {
|
|
344
|
+
return {
|
|
345
|
+
id: String(partition._id),
|
|
346
|
+
generation: partition.generation,
|
|
347
|
+
seq: partition.seq,
|
|
348
|
+
status: partition.status,
|
|
349
|
+
scope: partition.scope,
|
|
350
|
+
estimate: partition.estimate,
|
|
351
|
+
counters: { ...partition.counters },
|
|
352
|
+
...(partition.group !== undefined ? { group: partition.group } : {}),
|
|
353
|
+
...(partition.lease
|
|
354
|
+
? {
|
|
355
|
+
lease: {
|
|
356
|
+
slot: partition.lease.slot,
|
|
357
|
+
owner: partition.lease.owner,
|
|
358
|
+
host: partition.lease.host,
|
|
359
|
+
pid: partition.lease.pid,
|
|
360
|
+
renewedAt: partition.lease.renewedAt,
|
|
361
|
+
},
|
|
362
|
+
}
|
|
363
|
+
: {}),
|
|
364
|
+
...(partition.throttle ? { throttle: partition.throttle } : {}),
|
|
365
|
+
claims: partition.claims ?? 0,
|
|
366
|
+
reclaims: partition.reclaims ?? 0,
|
|
367
|
+
failures: partition.failures ?? 0,
|
|
368
|
+
...(partition.lastError ? { lastError: partition.lastError } : {}),
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* The background migrations with work to do — blocked ones whose requires
|
|
374
|
+
* are met are unblocked on the way — with what a heal needs to tell a
|
|
375
|
+
* stalled one (live leases read for all of them at once).
|
|
376
|
+
*/
|
|
377
|
+
async function runnable(host) {
|
|
378
|
+
const { store } = host;
|
|
379
|
+
const deps = host.deps();
|
|
380
|
+
const states = [];
|
|
381
|
+
for (const state of await store.list({ status: { $in: ['blocked', 'pending', 'running'] } })) {
|
|
382
|
+
let current = state;
|
|
383
|
+
if (state.status === 'blocked') current = (await tryUnblock(deps, state)) ?? state;
|
|
384
|
+
if (current.status !== 'blocked') states.push(current);
|
|
385
|
+
}
|
|
386
|
+
const names = [];
|
|
387
|
+
for (const state of states) names.push(state._id);
|
|
388
|
+
const live = await store.liveLeasesOf(names);
|
|
389
|
+
const rows = [];
|
|
390
|
+
for (const state of states) {
|
|
391
|
+
rows.push({
|
|
392
|
+
migration: state._id,
|
|
393
|
+
status: state.status,
|
|
394
|
+
maxParallel: state.spec?.maxParallel ?? 1,
|
|
395
|
+
liveLeases: live.get(state._id) ?? 0,
|
|
396
|
+
registeredAt: state.registeredAt,
|
|
397
|
+
...(state.startedAt ? { startedAt: state.startedAt } : {}),
|
|
398
|
+
...(state.lastProgressAt ? { lastProgressAt: state.lastProgressAt } : {}),
|
|
399
|
+
...coordinatorView(state),
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
return rows;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/** A drift watcher's stored document as its status row */
|
|
406
|
+
function watchRow(row) {
|
|
407
|
+
return {
|
|
408
|
+
collection: row._id,
|
|
409
|
+
state: row.state ?? 'starting',
|
|
410
|
+
...(row.target !== undefined ? { target: row.target } : {}),
|
|
411
|
+
edges: row.edges ?? [],
|
|
412
|
+
...(row.leader
|
|
413
|
+
? { leader: { host: row.leader.host, pid: row.leader.pid, at: row.leader.at } }
|
|
414
|
+
: {}),
|
|
415
|
+
counters: { events: 0, upgraded: 0, failed: 0, skipped: 0, ...row.counters },
|
|
416
|
+
...(row.lastEventAt ? { lastEventAt: row.lastEventAt } : {}),
|
|
417
|
+
updatedAt: row.updatedAt,
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
module.exports = {
|
|
422
|
+
STREAMING_FRESH_MS,
|
|
423
|
+
drive,
|
|
424
|
+
hasRewritten,
|
|
425
|
+
irreversibleBackground,
|
|
426
|
+
partitionView,
|
|
427
|
+
runnable,
|
|
428
|
+
stateView,
|
|
429
|
+
unsatisfied,
|
|
430
|
+
watchRow,
|
|
431
|
+
withdraw,
|
|
432
|
+
};
|