@alexify/migronaut 2.1.0 → 2.3.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 +223 -0
- package/README.md +68 -10
- package/bullmq.d.ts +465 -7
- package/index.d.ts +1272 -18
- package/migronaut.schema.json +150 -2
- package/package.json +8 -2
- package/src/bullmq/background-processor.js +469 -0
- package/src/bullmq/index.js +12 -0
- package/src/bullmq/jobs.js +254 -7
- package/src/bullmq/processor.js +153 -15
- package/src/bullmq/producer.js +202 -27
- package/src/bullmq/service.js +480 -45
- package/src/cli/commands/background.js +500 -0
- package/src/cli/commands/converge.js +38 -10
- 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/cli/table.js +68 -9
- package/src/core/audit.js +98 -3
- package/src/core/background-audit.js +139 -0
- package/src/core/background-drift.js +126 -0
- package/src/core/background-dry-run.js +366 -0
- package/src/core/background-engine.js +818 -0
- package/src/core/background-kit.js +425 -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 +605 -0
- package/src/core/background.js +1121 -0
- package/src/core/bson-peer.js +23 -0
- package/src/core/changelog.js +32 -0
- package/src/core/collections.js +125 -31
- package/src/core/config.js +133 -13
- package/src/core/converge-plan.js +343 -61
- package/src/core/converge-search-run.js +440 -0
- package/src/core/converge-search.js +404 -0
- package/src/core/converge.js +428 -183
- package/src/core/index-spec.js +27 -16
- package/src/core/lock.js +97 -32
- package/src/core/migrator.js +951 -26
- package/src/core/options.js +32 -1
- package/src/core/run.js +26 -12
- package/src/core/runner.js +1 -1
- package/src/core/search-index-spec.js +758 -0
- package/src/core/server-info.js +70 -0
- package/src/core/shard-info.js +76 -0
- package/src/core/versioning-spec.js +181 -0
- package/src/errors/index.js +97 -5
- package/src/index.js +16 -0
- package/src/utils/canonical.js +34 -1
- package/src/utils/error.js +11 -2
- package/src/utils/loader.js +77 -9
- package/src/utils/migration-name.js +33 -1
- package/src/utils/telemetry.js +125 -1
- package/src/utils/template.js +69 -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/core/index-spec.js
CHANGED
|
@@ -53,6 +53,9 @@ const INDEX_KEYS = new Set([
|
|
|
53
53
|
...IGNORED_OPTIONS,
|
|
54
54
|
]);
|
|
55
55
|
|
|
56
|
+
/** Every option an index is normalized with — built once, read per index */
|
|
57
|
+
const NORMALIZED_OPTIONS = Object.freeze([...SEMANTIC_OPTIONS, ...DECLARED_ONLY_OPTIONS]);
|
|
58
|
+
|
|
56
59
|
const BOOLEAN_OPTIONS = new Set(['unique', 'sparse', 'hidden', 'background']);
|
|
57
60
|
const OBJECT_OPTIONS = new Set([
|
|
58
61
|
'partialFilterExpression',
|
|
@@ -98,16 +101,20 @@ function keyIssues(key, report) {
|
|
|
98
101
|
return null;
|
|
99
102
|
}
|
|
100
103
|
let usable = true;
|
|
104
|
+
let integerLike = false;
|
|
101
105
|
for (const [field, direction] of entries) {
|
|
102
106
|
if (typeof field !== 'string' || field.length === 0) {
|
|
103
107
|
report('field names must be non-empty strings');
|
|
104
108
|
usable = false;
|
|
105
|
-
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (!integerLike && INTEGER_LIKE.test(field)) integerLike = true;
|
|
112
|
+
if (!DIRECTIONS.has(direction)) {
|
|
106
113
|
report(`direction of "${field}" must be 1, -1, 'text', 'hashed', '2d' or '2dsphere'`);
|
|
107
114
|
usable = false;
|
|
108
115
|
}
|
|
109
116
|
}
|
|
110
|
-
if (usable && entries.length > 1 &&
|
|
117
|
+
if (usable && entries.length > 1 && integerLike) {
|
|
111
118
|
if (!(key instanceof Map)) {
|
|
112
119
|
report(
|
|
113
120
|
'has an integer-like field name — JavaScript reorders such keys, so declare this key ' +
|
|
@@ -145,10 +152,14 @@ function indexIssues(index, path) {
|
|
|
145
152
|
if (!INDEX_KEYS.has(key)) report(key)('is not a supported index option');
|
|
146
153
|
}
|
|
147
154
|
const entries = keyIssues(index.key, report('key'));
|
|
148
|
-
|
|
155
|
+
let isText = false;
|
|
149
156
|
// `wildcardProjection` belongs to an all-fields wildcard (`$**`, compound or
|
|
150
157
|
// not) — the server refuses it on a path wildcard such as `a.$**`.
|
|
151
|
-
|
|
158
|
+
let isAllFieldsWildcard = false;
|
|
159
|
+
for (const [field, direction] of entries ?? []) {
|
|
160
|
+
if (direction === TEXT) isText = true;
|
|
161
|
+
if (field === '$**') isAllFieldsWildcard = true;
|
|
162
|
+
}
|
|
152
163
|
|
|
153
164
|
if (index.name !== undefined) {
|
|
154
165
|
if (typeof index.name !== 'string' || index.name.length === 0) {
|
|
@@ -209,23 +220,23 @@ function indexIssues(index, path) {
|
|
|
209
220
|
}
|
|
210
221
|
|
|
211
222
|
/**
|
|
212
|
-
* The key as the server stores it
|
|
213
|
-
*
|
|
214
|
-
* first text field, and move
|
|
223
|
+
* The key as the server stores it, and whether it is a text index — in one
|
|
224
|
+
* pass. A text index is not stored under its fields: they collapse into
|
|
225
|
+
* `_fts: 'text', _ftsx: 1` at the position of the first text field, and move
|
|
226
|
+
* into `weights`.
|
|
215
227
|
*/
|
|
216
228
|
function serverKeyOf(entries) {
|
|
217
|
-
if (!entries.some(([, direction]) => direction === TEXT)) return entries;
|
|
218
229
|
const out = [];
|
|
219
|
-
let
|
|
230
|
+
let isText = false;
|
|
220
231
|
for (const entry of entries) {
|
|
221
232
|
if (entry[1] !== TEXT) {
|
|
222
233
|
out.push(entry);
|
|
223
|
-
} else if (!
|
|
234
|
+
} else if (!isText) {
|
|
224
235
|
out.push(['_fts', TEXT], ['_ftsx', 1]);
|
|
225
|
-
|
|
236
|
+
isText = true;
|
|
226
237
|
}
|
|
227
238
|
}
|
|
228
|
-
return out;
|
|
239
|
+
return { serverKey: isText ? out : entries, isText };
|
|
229
240
|
}
|
|
230
241
|
|
|
231
242
|
/** Text fields at weight 1, then whatever the declaration weighs differently */
|
|
@@ -243,10 +254,10 @@ function textWeights(entries, declared) {
|
|
|
243
254
|
*/
|
|
244
255
|
function normalizeDeclaredIndex(index) {
|
|
245
256
|
const entries = keyEntries(index.key);
|
|
246
|
-
const
|
|
257
|
+
const { serverKey, isText } = serverKeyOf(entries);
|
|
247
258
|
const name = index.name ?? defaultIndexName(entries);
|
|
248
259
|
const options = {};
|
|
249
|
-
for (const option of
|
|
260
|
+
for (const option of NORMALIZED_OPTIONS) {
|
|
250
261
|
const value = index[option];
|
|
251
262
|
if (value === undefined) continue;
|
|
252
263
|
// `false` is the server default; sending it would only make the stored
|
|
@@ -258,7 +269,7 @@ function normalizeDeclaredIndex(index) {
|
|
|
258
269
|
return {
|
|
259
270
|
name,
|
|
260
271
|
entries,
|
|
261
|
-
serverKey
|
|
272
|
+
serverKey,
|
|
262
273
|
isText,
|
|
263
274
|
options,
|
|
264
275
|
// The key travels as a Map: the field order is the index, and the driver
|
|
@@ -271,7 +282,7 @@ function normalizeDeclaredIndex(index) {
|
|
|
271
282
|
function normalizeLiveIndex(raw) {
|
|
272
283
|
const entries = Object.entries(raw.key ?? {});
|
|
273
284
|
const options = {};
|
|
274
|
-
for (const option of
|
|
285
|
+
for (const option of NORMALIZED_OPTIONS) {
|
|
275
286
|
const value = raw[option];
|
|
276
287
|
if (value === undefined) continue;
|
|
277
288
|
if (BOOLEAN_OPTIONS.has(option)) {
|
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:
|
|
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:
|
|
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:
|
|
135
|
-
throw new LockAlreadyHeldError(
|
|
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:
|
|
184
|
+
const current = await collection.findOne({ _id: this.#id });
|
|
159
185
|
if (!current || current.owner !== owner || current.nonce !== nonce) {
|
|
160
|
-
throw new LockAlreadyHeldError(
|
|
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:
|
|
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:
|
|
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:
|
|
200
|
-
await collection.deleteOne({ _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:
|
|
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
|
-
|
|
249
|
+
`Failed to release ${this.#label}`,
|
|
224
250
|
{ error: errorText(error) },
|
|
225
251
|
{ cause: error },
|
|
226
252
|
);
|
|
@@ -229,10 +255,18 @@ class MigrationLock {
|
|
|
229
255
|
}
|
|
230
256
|
|
|
231
257
|
/**
|
|
232
|
-
* Run `fn(signal)` while holding the migration lock. The lock is
|
|
233
|
-
* released in a `finally` block. While `fn` runs, a heartbeat renews the
|
|
234
|
-
* every `ttlMs/2` so a migration that takes longer than the TTL never lets
|
|
235
|
-
* lock go stale and get reclaimed by another process.
|
|
258
|
+
* Run `fn(signal, control)` while holding the migration lock. The lock is
|
|
259
|
+
* always released in a `finally` block. While `fn` runs, a heartbeat renews the
|
|
260
|
+
* lock every `ttlMs/2` so a migration that takes longer than the TTL never lets
|
|
261
|
+
* its lock go stale and get reclaimed by another process.
|
|
262
|
+
*
|
|
263
|
+
* `control.release()` gives the lock up before `fn` returns — once, for a
|
|
264
|
+
* tail of work that only reads (converge waiting for search index builds):
|
|
265
|
+
* the heartbeat stops, the lock document goes, `onLockReleased({ early: true })`
|
|
266
|
+
* fires, and the signal no longer aborts on a lost lock. It resolves to
|
|
267
|
+
* whether the lock was released: one that fails is only warned about (the
|
|
268
|
+
* lock frees itself once its TTL runs out) and tried again at the end. With
|
|
269
|
+
* `noLock` there is nothing to release (`false`).
|
|
236
270
|
*
|
|
237
271
|
* If the lock is nonetheless lost — another process reclaimed it, or the
|
|
238
272
|
* heartbeat cannot reach the database — the `signal` is aborted with a
|
|
@@ -245,6 +279,9 @@ class MigrationLock {
|
|
|
245
279
|
*/
|
|
246
280
|
async function runWithLock(lock, options, fn) {
|
|
247
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;
|
|
248
285
|
|
|
249
286
|
if (options.noLock) {
|
|
250
287
|
options.logger.warn('⚠ Running without a lock (--no-lock) — concurrent runs are unsafe', {
|
|
@@ -254,7 +291,7 @@ async function runWithLock(lock, options, fn) {
|
|
|
254
291
|
// `skipped: true` tells them apart from a real lock.
|
|
255
292
|
options.onLockAcquired?.({ skipped: true });
|
|
256
293
|
try {
|
|
257
|
-
return await fn(controller.signal);
|
|
294
|
+
return await fn(controller.signal, { release: async () => false });
|
|
258
295
|
} finally {
|
|
259
296
|
options.onLockReleased?.({ skipped: true });
|
|
260
297
|
}
|
|
@@ -270,15 +307,13 @@ async function runWithLock(lock, options, fn) {
|
|
|
270
307
|
const loseLock = (reason) => {
|
|
271
308
|
// The most alert-worthy line this module emits — structured fields so a
|
|
272
309
|
// JSON sink can trigger on it without parsing the human string.
|
|
273
|
-
options.logger.warn(`⚠ Lost the
|
|
310
|
+
options.logger.warn(`⚠ Lost the ${label} mid-run (${reason})`, {
|
|
274
311
|
event: 'lock:lost',
|
|
275
312
|
reason,
|
|
276
313
|
});
|
|
277
314
|
options.onLockLostEvent?.(reason);
|
|
278
315
|
if (abortOnLoss && !controller.signal.aborted) {
|
|
279
|
-
controller.abort(
|
|
280
|
-
new LockLostError('Lost the migration lock mid-run', { reason, aborted: true }),
|
|
281
|
-
);
|
|
316
|
+
controller.abort(new LockLostError(`Lost the ${label} mid-run`, { reason, aborted: true }));
|
|
282
317
|
}
|
|
283
318
|
};
|
|
284
319
|
|
|
@@ -352,23 +387,53 @@ async function runWithLock(lock, options, fn) {
|
|
|
352
387
|
}, intervalMs);
|
|
353
388
|
heartbeat.unref?.();
|
|
354
389
|
|
|
390
|
+
const stopHeartbeat = async () => {
|
|
391
|
+
stopped = true;
|
|
392
|
+
clearInterval(heartbeat);
|
|
393
|
+
clearTimeout(deadline);
|
|
394
|
+
// Let any renewal already in flight settle, so no stray query outlives this
|
|
395
|
+
// call and lands after the caller has closed the client.
|
|
396
|
+
await inFlight;
|
|
397
|
+
};
|
|
398
|
+
let released = false;
|
|
399
|
+
let releasing;
|
|
400
|
+
const control = {
|
|
401
|
+
release: () =>
|
|
402
|
+
(releasing ??= (async () => {
|
|
403
|
+
await stopHeartbeat();
|
|
404
|
+
try {
|
|
405
|
+
await lock.release();
|
|
406
|
+
released = true;
|
|
407
|
+
options.onLockReleased?.({ early: true });
|
|
408
|
+
} catch (releaseError) {
|
|
409
|
+
const message = errorText(releaseError);
|
|
410
|
+
options.logger.warn(
|
|
411
|
+
`⚠ Failed to release the ${label} early: ${message} — it frees itself once ` +
|
|
412
|
+
'its TTL runs out',
|
|
413
|
+
{ event: 'lock:release-failed', error: message, early: true },
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
return released;
|
|
417
|
+
})()),
|
|
418
|
+
};
|
|
419
|
+
|
|
355
420
|
let result;
|
|
356
421
|
let runError;
|
|
357
422
|
let failed = false;
|
|
358
423
|
try {
|
|
359
|
-
result = await fn(controller.signal);
|
|
424
|
+
result = await fn(controller.signal, control);
|
|
360
425
|
} catch (error) {
|
|
361
426
|
failed = true;
|
|
362
427
|
runError = error;
|
|
363
428
|
} finally {
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
clearTimeout(deadline);
|
|
367
|
-
// Let any renewal already in flight settle, so no stray query outlives this
|
|
368
|
-
// call and lands after the caller has closed the client.
|
|
369
|
-
await inFlight;
|
|
429
|
+
await releasing;
|
|
430
|
+
await stopHeartbeat();
|
|
370
431
|
}
|
|
371
432
|
|
|
433
|
+
if (released) {
|
|
434
|
+
if (failed) throw runError;
|
|
435
|
+
return result;
|
|
436
|
+
}
|
|
372
437
|
try {
|
|
373
438
|
await lock.release();
|
|
374
439
|
options.onLockReleased?.();
|
|
@@ -386,7 +451,7 @@ async function runWithLock(lock, options, fn) {
|
|
|
386
451
|
throw releaseError;
|
|
387
452
|
}
|
|
388
453
|
const message = errorText(releaseError);
|
|
389
|
-
options.logger.warn(`⚠ Failed to release the
|
|
454
|
+
options.logger.warn(`⚠ Failed to release the ${label}: ${message}`, {
|
|
390
455
|
event: 'lock:release-failed',
|
|
391
456
|
error: message,
|
|
392
457
|
});
|
|
@@ -396,4 +461,4 @@ async function runWithLock(lock, options, fn) {
|
|
|
396
461
|
return result;
|
|
397
462
|
}
|
|
398
463
|
|
|
399
|
-
module.exports = { LOCK_ID, MigrationLock, runWithLock, toLockInfo };
|
|
464
|
+
module.exports = { LOCK_ID, LOCK_LABEL, MigrationLock, runWithLock, toLockInfo };
|