swarm-deploy 0.3.1 → 0.4.2
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 +65 -0
- package/README.md +89 -1257
- package/RELEASING.md +15 -8
- package/dist/cli.d.ts +7 -0
- package/dist/cli.js +127 -2
- package/dist/cli.js.map +1 -1
- package/dist/hooks-module.d.ts +3 -2
- package/dist/hooks-module.js +4 -4
- package/dist/hooks-module.js.map +1 -1
- package/dist/hooks.d.ts +75 -2
- package/dist/hooks.js +57 -3
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +58 -8
- package/dist/server.js +287 -57
- package/dist/server.js.map +1 -1
- package/dist/storage/layout.d.ts +29 -0
- package/dist/storage/layout.js +53 -4
- package/dist/storage/layout.js.map +1 -1
- package/dist/storage/link-store.d.ts +48 -7
- package/dist/storage/link-store.js +119 -11
- package/dist/storage/link-store.js.map +1 -1
- package/dist/storage/reset.d.ts +95 -0
- package/dist/storage/reset.js +667 -0
- package/dist/storage/reset.js.map +1 -0
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -46,15 +46,21 @@ exports.MAX_ACTIVE_UPLOADS = MAX_ACTIVE_UPLOADS;
|
|
|
46
46
|
const FINGERPRINT_LENGTH = 12;
|
|
47
47
|
exports.FINGERPRINT_LENGTH = FINGERPRINT_LENGTH;
|
|
48
48
|
const TAR_DURABILITY_BATCH_BYTES = 1024 * 1024;
|
|
49
|
+
/** How long a connection may linger so its peer can read a `HOOK_FAILED` answer before it is destroyed. */
|
|
50
|
+
const ANSWER_CLOSE_GRACE = 5_000;
|
|
49
51
|
/**
|
|
50
52
|
* How many transfers may owe a deferred post-commit retention pass at once.
|
|
51
53
|
*
|
|
52
|
-
* The set grows only when an `afterCommit` hook fails after a durable commit,
|
|
54
|
+
* The set grows only when an `afterCommit` or `afterLink` hook fails after a durable commit,
|
|
53
55
|
* and entries are removed as soon as a retry succeeds, so reaching the bound
|
|
54
56
|
* means a persistently failing hook. Capping it at the connection ceiling
|
|
55
57
|
* keeps a broken deployment step from growing server memory without limit.
|
|
56
58
|
*/
|
|
57
59
|
exports.MAX_OWED_POST_COMMIT_RETENTION = MAX_CONNECTIONS;
|
|
60
|
+
function releaseLinkHooks(pending) {
|
|
61
|
+
for (const item of pending)
|
|
62
|
+
item.turn.release();
|
|
63
|
+
}
|
|
58
64
|
function fail(code, message, cause = null) {
|
|
59
65
|
return new errors_js_1.SwarmDeployError(code, message, cause);
|
|
60
66
|
}
|
|
@@ -64,6 +70,12 @@ function codeOf(error) {
|
|
|
64
70
|
? error.code
|
|
65
71
|
: errors_js_1.ERRORS.PROTOCOL_INVALID;
|
|
66
72
|
}
|
|
73
|
+
function frozenRelease(release) {
|
|
74
|
+
return Object.freeze({
|
|
75
|
+
series: release.series,
|
|
76
|
+
...(release.version === undefined ? {} : { version: release.version })
|
|
77
|
+
});
|
|
78
|
+
}
|
|
67
79
|
function positive(value, name, max = Number.MAX_SAFE_INTEGER) {
|
|
68
80
|
if (!Number.isSafeInteger(value) || value <= 0 || value > max) {
|
|
69
81
|
throw fail(errors_js_1.ERRORS.PROTOCOL_INVALID, `Invalid ${name}`);
|
|
@@ -136,7 +148,7 @@ class Server extends EventEmitter {
|
|
|
136
148
|
activeUploads = new Set();
|
|
137
149
|
/**
|
|
138
150
|
* Transfer IDs this process committed but whose deferred post-commit
|
|
139
|
-
* retention is still owed because `afterCommit` has not yet succeeded. Only
|
|
151
|
+
* retention is still owed because `afterCommit` or `afterLink` has not yet succeeded. Only
|
|
140
152
|
* these may start a retention pass from an already-committed retry, so an
|
|
141
153
|
* ordinary duplicate offer cannot be used to force repeated full passes.
|
|
142
154
|
* The set is in-memory only and bounded by `MAX_OWED_POST_COMMIT_RETENTION`;
|
|
@@ -152,6 +164,13 @@ class Server extends EventEmitter {
|
|
|
152
164
|
* and its hooks concurrently against one staging and commit identity.
|
|
153
165
|
*/
|
|
154
166
|
activeTransfers = new Set();
|
|
167
|
+
/**
|
|
168
|
+
* One first-in, first-out line of `afterLink` calls per link name. A place is
|
|
169
|
+
* reserved while the root lease orders the link decision, and the hook runs
|
|
170
|
+
* after the lease is released, so a link's hooks start in decision order
|
|
171
|
+
* and never overlap, while hook code never runs under the lease.
|
|
172
|
+
*/
|
|
173
|
+
linkHookTurns = new hooks_js_1.HookTurns();
|
|
155
174
|
receives = new Set();
|
|
156
175
|
layout = null;
|
|
157
176
|
sessions = null;
|
|
@@ -305,25 +324,40 @@ class Server extends EventEmitter {
|
|
|
305
324
|
transferId: metadata.transferId,
|
|
306
325
|
...((0, controls_js_1.isTreeMetadata)(metadata) ? { entryCount: metadata.entryCount } : {}),
|
|
307
326
|
...(metadata.sourceParent === undefined ? {} : { sourceParent: metadata.sourceParent }),
|
|
308
|
-
...(release === null
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
327
|
+
...(release === null ? {} : { release: frozenRelease(release) })
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
/** Describes a durably committed artifact from its commit record, as a link hook sees it. */
|
|
331
|
+
recordArtifact(record) {
|
|
332
|
+
return Object.freeze({
|
|
333
|
+
name: record.name,
|
|
334
|
+
kind: (0, commit_journal_js_1.commitRecordKind)(record),
|
|
335
|
+
size: record.size,
|
|
336
|
+
sha256: record.sha256,
|
|
337
|
+
transferId: record.transferId,
|
|
338
|
+
...(record.entryCount === undefined ? {} : { entryCount: record.entryCount }),
|
|
339
|
+
...(record.release === undefined ? {} : { release: frozenRelease(record.release) })
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
hookLink(link, mode) {
|
|
343
|
+
return Object.freeze({
|
|
344
|
+
name: link.name,
|
|
345
|
+
target: link.target,
|
|
346
|
+
mode,
|
|
347
|
+
path: _path_1.default.join(this.layout.root, link.name),
|
|
348
|
+
targetPath: _path_1.default.join(this.layout.root, link.target)
|
|
316
349
|
});
|
|
317
350
|
}
|
|
318
351
|
/**
|
|
319
352
|
* Computes and converges the desired links from durable commit records.
|
|
320
353
|
* Called with the root lease already held.
|
|
321
354
|
*/
|
|
322
|
-
async
|
|
355
|
+
async convergeLinksUnlocked(records) {
|
|
323
356
|
const desired = (0, symlinks_js_1.selectDesiredLinks)(this.symlinks, records);
|
|
324
|
-
if (!this.links)
|
|
325
|
-
return new Set(desired.map((link) => link.transferId));
|
|
326
|
-
|
|
357
|
+
if (!this.links) {
|
|
358
|
+
return { pins: new Set(desired.map((link) => link.transferId)), desired, result: null };
|
|
359
|
+
}
|
|
360
|
+
const result = await this.links.reconcile(desired, this.linkNames, {
|
|
327
361
|
managedArtifactNames: new Set(records.map((record) => record.name))
|
|
328
362
|
});
|
|
329
363
|
const ledger = await this.links.list();
|
|
@@ -338,7 +372,10 @@ class Server extends EventEmitter {
|
|
|
338
372
|
else
|
|
339
373
|
pins.add(link.transferId);
|
|
340
374
|
}
|
|
341
|
-
return pins;
|
|
375
|
+
return { pins, desired, result };
|
|
376
|
+
}
|
|
377
|
+
async reconcileLinksUnlocked(records) {
|
|
378
|
+
return (await this.convergeLinksUnlocked(records)).pins;
|
|
342
379
|
}
|
|
343
380
|
/** Pins link targets when reconciliation fails but retention must continue. */
|
|
344
381
|
async linkPinsFallbackUnlocked(records) {
|
|
@@ -353,12 +390,37 @@ class Server extends EventEmitter {
|
|
|
353
390
|
}
|
|
354
391
|
return pins;
|
|
355
392
|
}
|
|
356
|
-
/**
|
|
357
|
-
|
|
393
|
+
/**
|
|
394
|
+
* Reconciles after a durable commit; this is the only caller that leases the
|
|
395
|
+
* root for it. When an `afterLink` hook is configured it also returns one
|
|
396
|
+
* pending call for every automatic link that now designates `artifact`,
|
|
397
|
+
* whether this reconcile changed it or an earlier attempt already had, so a
|
|
398
|
+
* retry re-runs a hook that failed. The caller owns the returned places in
|
|
399
|
+
* the hook lines and must release them.
|
|
400
|
+
*/
|
|
401
|
+
async reconcileAfterCommit(artifact) {
|
|
358
402
|
if (!this.links || !this.commits)
|
|
359
|
-
return;
|
|
403
|
+
return [];
|
|
360
404
|
try {
|
|
361
|
-
await (0, root_coordinator_js_1.withRootLease)(this.layout.root, async () =>
|
|
405
|
+
return await (0, root_coordinator_js_1.withRootLease)(this.layout.root, async () => {
|
|
406
|
+
const { desired, result } = await this.convergeLinksUnlocked(await this.commits.list());
|
|
407
|
+
if (this.hooks.afterLink === undefined)
|
|
408
|
+
return [];
|
|
409
|
+
const unchanged = new Set(result?.unchanged);
|
|
410
|
+
const pending = desired
|
|
411
|
+
.filter((link) => link.transferId === artifact.transferId)
|
|
412
|
+
.map((link) => ({
|
|
413
|
+
link: this.hookLink(link, 'automatic'),
|
|
414
|
+
artifact,
|
|
415
|
+
status: unchanged.has(link.name) ? 'UNCHANGED' : 'LINKED',
|
|
416
|
+
trigger: 'upload'
|
|
417
|
+
}));
|
|
418
|
+
// Reserved last and without a failure point after it, so no place can leak.
|
|
419
|
+
return pending.map((item) => ({
|
|
420
|
+
...item,
|
|
421
|
+
turn: this.linkHookTurns.reserve(item.link.name)
|
|
422
|
+
}));
|
|
423
|
+
});
|
|
362
424
|
}
|
|
363
425
|
catch (error) {
|
|
364
426
|
if (error instanceof errors_js_1.SwarmDeployError && error.code === errors_js_1.ERRORS.LINK_CONFLICT)
|
|
@@ -366,6 +428,41 @@ class Server extends EventEmitter {
|
|
|
366
428
|
throw new errors_js_1.SwarmDeployError(errors_js_1.ERRORS.LINK_FAILED, 'Unable to reconcile managed symlinks', error);
|
|
367
429
|
}
|
|
368
430
|
}
|
|
431
|
+
/**
|
|
432
|
+
* Runs pending `afterLink` calls one after another, each once every earlier
|
|
433
|
+
* hook for its link has finished. No storage lease is held while hook code
|
|
434
|
+
* runs. `begin` is told which call is starting so a caller can attribute a
|
|
435
|
+
* failure. The first failure stops the rest; the caller releases the places.
|
|
436
|
+
*/
|
|
437
|
+
async runLinkHooks(pending, owner, begin) {
|
|
438
|
+
for (const item of pending) {
|
|
439
|
+
begin(item);
|
|
440
|
+
if ((await item.turn.ready(this.signal)) === 'aborted')
|
|
441
|
+
throw (0, abort_js_1.abortError)();
|
|
442
|
+
try {
|
|
443
|
+
await this.runHook('afterLink', this.hooks.afterLink, Object.freeze({
|
|
444
|
+
link: item.link,
|
|
445
|
+
artifact: item.artifact,
|
|
446
|
+
status: item.status,
|
|
447
|
+
trigger: item.trigger
|
|
448
|
+
}));
|
|
449
|
+
}
|
|
450
|
+
catch (error) {
|
|
451
|
+
if (error instanceof errors_js_1.SwarmDeployError && error.code === errors_js_1.ERRORS.HOOK_FAILED) {
|
|
452
|
+
this.logger.warn('Link hook failed', {
|
|
453
|
+
fingerprint: owner ? fingerprint(owner) : 'invalid',
|
|
454
|
+
name: item.link.name,
|
|
455
|
+
target: item.link.target,
|
|
456
|
+
trigger: item.trigger,
|
|
457
|
+
status: item.status,
|
|
458
|
+
reason: errors_js_1.ERRORS.HOOK_FAILED
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
throw error;
|
|
462
|
+
}
|
|
463
|
+
item.turn.release();
|
|
464
|
+
}
|
|
465
|
+
}
|
|
369
466
|
setManagedLinkNames(records) {
|
|
370
467
|
this.managedLinkNamesSnapshot = new Set([
|
|
371
468
|
...this.linkNames,
|
|
@@ -382,7 +479,13 @@ class Server extends EventEmitter {
|
|
|
382
479
|
async receiveLink(socket, reader, owner, request) {
|
|
383
480
|
let emitted = false;
|
|
384
481
|
let resultWriteStarted = false;
|
|
385
|
-
|
|
482
|
+
// `durable` is set once the link is durable, so a later hook failure can report what the
|
|
483
|
+
// link did; `failed` is the hook that was running. Held in an object because a closure sets it.
|
|
484
|
+
const state = {
|
|
485
|
+
durable: null,
|
|
486
|
+
failed: null
|
|
487
|
+
};
|
|
488
|
+
const emitTerminal = (status, reason, result) => {
|
|
386
489
|
if (emitted)
|
|
387
490
|
return;
|
|
388
491
|
emitted = true;
|
|
@@ -391,7 +494,8 @@ class Server extends EventEmitter {
|
|
|
391
494
|
target: request.target,
|
|
392
495
|
name: request.name,
|
|
393
496
|
status,
|
|
394
|
-
...(reason === undefined ? {} : { reason })
|
|
497
|
+
...(reason === undefined ? {} : { reason }),
|
|
498
|
+
...(result === undefined ? {} : { result })
|
|
395
499
|
});
|
|
396
500
|
};
|
|
397
501
|
try {
|
|
@@ -406,7 +510,7 @@ class Server extends EventEmitter {
|
|
|
406
510
|
if (this.linkNames.has(request.name)) {
|
|
407
511
|
throw fail(errors_js_1.ERRORS.LINK_NOT_ALLOWED, 'Manual link name is reserved by an automatic rule');
|
|
408
512
|
}
|
|
409
|
-
const
|
|
513
|
+
const decided = await (0, root_coordinator_js_1.withRootLease)(this.layout.root, async () => {
|
|
410
514
|
const records = await this.commits.list();
|
|
411
515
|
const target = records.find((record) => record.name === request.target && !(0, files_js_1.isReservedHistoryName)(record.name));
|
|
412
516
|
if (!target)
|
|
@@ -428,30 +532,58 @@ class Server extends EventEmitter {
|
|
|
428
532
|
automaticLinkNames: this.linkNames
|
|
429
533
|
});
|
|
430
534
|
await this.refreshManagedLinkNames();
|
|
431
|
-
|
|
535
|
+
// The link is durable here. Its hook runs after this lease is released, so a
|
|
536
|
+
// slow or failing hook never blocks commits, retention, or other links.
|
|
537
|
+
const pending = this.hooks.afterLink === undefined
|
|
538
|
+
? null
|
|
539
|
+
: {
|
|
540
|
+
link: this.hookLink(link, 'manual'),
|
|
541
|
+
artifact: this.recordArtifact(target),
|
|
542
|
+
status: linked.status,
|
|
543
|
+
trigger: 'request',
|
|
544
|
+
turn: this.linkHookTurns.reserve(link.name)
|
|
545
|
+
};
|
|
546
|
+
return { status: linked.status, pending };
|
|
432
547
|
});
|
|
548
|
+
state.durable = decided.status;
|
|
549
|
+
if (decided.pending !== null) {
|
|
550
|
+
const pending = decided.pending;
|
|
551
|
+
try {
|
|
552
|
+
await this.runLinkHooks([pending], owner, (item) => {
|
|
553
|
+
state.failed = item;
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
finally {
|
|
557
|
+
pending.turn.release();
|
|
558
|
+
}
|
|
559
|
+
}
|
|
433
560
|
resultWriteStarted = true;
|
|
434
|
-
await (0, direct_wire_js_1.writeLinkResult)(socket, { v: 1, status:
|
|
435
|
-
emitTerminal(
|
|
561
|
+
await (0, direct_wire_js_1.writeLinkResult)(socket, { v: 1, status: decided.status }, { signal: this.signal, timeout: this.idleTimeout });
|
|
562
|
+
emitTerminal(decided.status === 'LINKED' ? 'linked' : 'unchanged');
|
|
436
563
|
}
|
|
437
564
|
catch (error) {
|
|
438
565
|
let reason = error instanceof errors_js_1.SwarmDeployError &&
|
|
439
566
|
(error.code === errors_js_1.ERRORS.LINK_NOT_ALLOWED ||
|
|
440
567
|
error.code === errors_js_1.ERRORS.LINK_TARGET_NOT_FOUND ||
|
|
441
568
|
error.code === errors_js_1.ERRORS.LINK_CONFLICT ||
|
|
442
|
-
error.code === errors_js_1.ERRORS.LINK_FAILED
|
|
569
|
+
error.code === errors_js_1.ERRORS.LINK_FAILED ||
|
|
570
|
+
error.code === errors_js_1.ERRORS.HOOK_FAILED)
|
|
443
571
|
? error.code
|
|
444
572
|
: errors_js_1.ERRORS.LINK_FAILED;
|
|
445
573
|
const rejected = reason === errors_js_1.ERRORS.LINK_NOT_ALLOWED ||
|
|
446
574
|
reason === errors_js_1.ERRORS.LINK_TARGET_NOT_FOUND ||
|
|
447
575
|
reason === errors_js_1.ERRORS.LINK_CONFLICT;
|
|
576
|
+
let answered = false;
|
|
448
577
|
if (!resultWriteStarted) {
|
|
449
578
|
resultWriteStarted = true;
|
|
450
579
|
try {
|
|
451
580
|
await (0, direct_wire_js_1.writeLinkResult)(socket, { v: 1, status: 'FAILED', code: reason }, { timeout: this.idleTimeout });
|
|
581
|
+
answered = true;
|
|
452
582
|
}
|
|
453
583
|
catch {
|
|
454
|
-
|
|
584
|
+
// A failed hook stays observable here even when the client cannot be told.
|
|
585
|
+
if (reason !== errors_js_1.ERRORS.HOOK_FAILED)
|
|
586
|
+
reason = errors_js_1.ERRORS.LINK_FAILED;
|
|
455
587
|
}
|
|
456
588
|
}
|
|
457
589
|
else {
|
|
@@ -459,11 +591,61 @@ class Server extends EventEmitter {
|
|
|
459
591
|
}
|
|
460
592
|
this.logger.warn('Direct link failed', { fingerprint: fingerprint(owner), reason });
|
|
461
593
|
this.emitSafe('failure', { fingerprint: fingerprint(owner), reason });
|
|
462
|
-
emitTerminal(rejected && reason !== errors_js_1.ERRORS.LINK_FAILED ? 'rejected' : 'failed', reason);
|
|
594
|
+
emitTerminal(rejected && reason !== errors_js_1.ERRORS.LINK_FAILED ? 'rejected' : 'failed', reason, reason === errors_js_1.ERRORS.HOOK_FAILED && state.durable !== null ? state.durable : undefined);
|
|
595
|
+
if (answered && reason === errors_js_1.ERRORS.HOOK_FAILED) {
|
|
596
|
+
this.closeAfterAnswer(socket);
|
|
597
|
+
}
|
|
598
|
+
else {
|
|
599
|
+
try {
|
|
600
|
+
socket.destroy();
|
|
601
|
+
}
|
|
602
|
+
catch { }
|
|
603
|
+
}
|
|
604
|
+
// The client has been answered and the link is already durable; onFailure only observes.
|
|
605
|
+
if (reason === errors_js_1.ERRORS.HOOK_FAILED && state.failed !== null) {
|
|
606
|
+
await this.runFailureHook(Object.freeze({
|
|
607
|
+
artifact: state.failed.artifact,
|
|
608
|
+
path: state.failed.link.path,
|
|
609
|
+
phase: 'afterLink',
|
|
610
|
+
resumed: false,
|
|
611
|
+
alreadyCommitted: true,
|
|
612
|
+
error: (0, hooks_js_1.callbackError)(error),
|
|
613
|
+
link: state.failed.link
|
|
614
|
+
}), owner, fingerprint(b4a_1.default.from(state.failed.artifact.transferId, 'hex')));
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Closes a connection after its `HOOK_FAILED` answer was written. Destroying
|
|
620
|
+
* the stream right behind the write can reset it before the peer has read the
|
|
621
|
+
* record, so the caller would see a reset instead of the stable code. The
|
|
622
|
+
* write side is ended first and the connection is destroyed once the peer
|
|
623
|
+
* closes it, or after a grace period if it never does. The work that failed
|
|
624
|
+
* is already durable, so lingering costs only the connection.
|
|
625
|
+
*/
|
|
626
|
+
closeAfterAnswer(socket) {
|
|
627
|
+
let timer;
|
|
628
|
+
const destroy = () => {
|
|
629
|
+
clearTimeout(timer);
|
|
463
630
|
try {
|
|
464
631
|
socket.destroy();
|
|
465
632
|
}
|
|
466
633
|
catch { }
|
|
634
|
+
};
|
|
635
|
+
try {
|
|
636
|
+
;
|
|
637
|
+
socket.end?.();
|
|
638
|
+
}
|
|
639
|
+
catch {
|
|
640
|
+
destroy();
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
socket.once('close', () => clearTimeout(timer));
|
|
644
|
+
timer = setTimeout(destroy, ANSWER_CLOSE_GRACE);
|
|
645
|
+
// A lingering answer must never keep the process alive on its own.
|
|
646
|
+
if (typeof timer === 'object' && timer !== null && 'unref' in timer) {
|
|
647
|
+
if (typeof timer.unref === 'function')
|
|
648
|
+
timer.unref();
|
|
467
649
|
}
|
|
468
650
|
}
|
|
469
651
|
/** Runs a gating callback; only callback exceptions become HOOK_FAILED. */
|
|
@@ -540,6 +722,7 @@ class Server extends EventEmitter {
|
|
|
540
722
|
let artifact = null;
|
|
541
723
|
let phase = 'offer';
|
|
542
724
|
let hookPath = null;
|
|
725
|
+
let failedLink = null;
|
|
543
726
|
let resumed = false;
|
|
544
727
|
let alreadyCommitted = false;
|
|
545
728
|
let hooksFinished = false;
|
|
@@ -566,7 +749,21 @@ class Server extends EventEmitter {
|
|
|
566
749
|
// finished its own. `onFailure` only observes, so holding the transfer
|
|
567
750
|
// across a slow callback would reject the client's legitimate retry.
|
|
568
751
|
releaseTransfer();
|
|
569
|
-
await this.runFailureHook(Object.freeze({
|
|
752
|
+
await this.runFailureHook(Object.freeze({
|
|
753
|
+
artifact,
|
|
754
|
+
path: hookPath,
|
|
755
|
+
phase,
|
|
756
|
+
resumed,
|
|
757
|
+
alreadyCommitted,
|
|
758
|
+
error,
|
|
759
|
+
...(phase === 'afterLink' && failedLink !== null ? { link: failedLink } : {})
|
|
760
|
+
}), owner, event.transfer);
|
|
761
|
+
};
|
|
762
|
+
// Attributes an `afterLink` failure to the link whose hook was starting.
|
|
763
|
+
const beginLinkHook = (item) => {
|
|
764
|
+
phase = 'afterLink';
|
|
765
|
+
failedLink = item.link;
|
|
766
|
+
hookPath = item.link.path;
|
|
570
767
|
};
|
|
571
768
|
const rejectEarly = async (reason, message) => {
|
|
572
769
|
await this.rejectOffer(socket, event, owner, reason);
|
|
@@ -629,7 +826,7 @@ class Server extends EventEmitter {
|
|
|
629
826
|
signal: this.signal,
|
|
630
827
|
replaceNames: this.replaceNames,
|
|
631
828
|
release,
|
|
632
|
-
deferPostCommitRetention: this.
|
|
829
|
+
deferPostCommitRetention: this.defersPostCommitRetention()
|
|
633
830
|
});
|
|
634
831
|
this.emitSafe('commit', {
|
|
635
832
|
...event,
|
|
@@ -639,17 +836,23 @@ class Server extends EventEmitter {
|
|
|
639
836
|
commitSucceeded = true;
|
|
640
837
|
hookPath = _path_1.default.join(this.layout.root, metadata.name);
|
|
641
838
|
await this.retireQuietly(verified.transferId, owner);
|
|
642
|
-
await this.reconcileAfterCommit();
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
this.
|
|
839
|
+
const linkHooks = await this.reconcileAfterCommit(hookArtifact);
|
|
840
|
+
try {
|
|
841
|
+
phase = 'afterCommit';
|
|
842
|
+
if (this.defersPostCommitRetention()) {
|
|
843
|
+
this.owePostCommitRetention(metadata.transferId);
|
|
844
|
+
}
|
|
845
|
+
await this.runHook('afterCommit', this.hooks.afterCommit, Object.freeze({
|
|
846
|
+
artifact: hookArtifact,
|
|
847
|
+
path: hookPath,
|
|
848
|
+
resumed,
|
|
849
|
+
alreadyCommitted: false
|
|
850
|
+
}));
|
|
851
|
+
await this.runLinkHooks(linkHooks, owner, beginLinkHook);
|
|
852
|
+
}
|
|
853
|
+
finally {
|
|
854
|
+
releaseLinkHooks(linkHooks);
|
|
646
855
|
}
|
|
647
|
-
await this.runHook('afterCommit', this.hooks.afterCommit, Object.freeze({
|
|
648
|
-
artifact: hookArtifact,
|
|
649
|
-
path: hookPath,
|
|
650
|
-
resumed,
|
|
651
|
-
alreadyCommitted: false
|
|
652
|
-
}));
|
|
653
856
|
hooksFinished = true;
|
|
654
857
|
await this.settleDeferredRetention(metadata.transferId, owner);
|
|
655
858
|
finalStarted = true;
|
|
@@ -677,14 +880,20 @@ class Server extends EventEmitter {
|
|
|
677
880
|
if (inspected.status === 'ALREADY_COMMITTED') {
|
|
678
881
|
alreadyCommitted = true;
|
|
679
882
|
hookPath = _path_1.default.join(this.layout.root, metadata.name);
|
|
680
|
-
await this.reconcileAfterCommit();
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
883
|
+
const linkHooks = await this.reconcileAfterCommit(hookArtifact);
|
|
884
|
+
try {
|
|
885
|
+
phase = 'afterCommit';
|
|
886
|
+
await this.runHook('afterCommit', this.hooks.afterCommit, Object.freeze({
|
|
887
|
+
artifact: hookArtifact,
|
|
888
|
+
path: hookPath,
|
|
889
|
+
resumed: false,
|
|
890
|
+
alreadyCommitted: true
|
|
891
|
+
}));
|
|
892
|
+
await this.runLinkHooks(linkHooks, owner, beginLinkHook);
|
|
893
|
+
}
|
|
894
|
+
finally {
|
|
895
|
+
releaseLinkHooks(linkHooks);
|
|
896
|
+
}
|
|
688
897
|
hooksFinished = true;
|
|
689
898
|
// Only a transfer this process committed can still owe a deferred
|
|
690
899
|
// pass; an ordinary duplicate offer never starts one.
|
|
@@ -821,19 +1030,27 @@ class Server extends EventEmitter {
|
|
|
821
1030
|
reason
|
|
822
1031
|
});
|
|
823
1032
|
}
|
|
1033
|
+
let answered = false;
|
|
824
1034
|
try {
|
|
825
1035
|
if (!sentAdmission) {
|
|
826
1036
|
await (0, direct_wire_js_1.writeAdmission)(socket, { v: 1, status: 'REJECTED', code: reason }, { timeout: this.idleTimeout });
|
|
1037
|
+
answered = true;
|
|
827
1038
|
}
|
|
828
1039
|
else if (!finalStarted) {
|
|
829
1040
|
await (0, direct_wire_js_1.writeFinal)(socket, { v: 1, status: 'FAILED', code: reason }, { timeout: this.idleTimeout });
|
|
1041
|
+
answered = true;
|
|
830
1042
|
}
|
|
831
1043
|
}
|
|
832
1044
|
catch { }
|
|
833
|
-
|
|
834
|
-
|
|
1045
|
+
if (answered && reason === errors_js_1.ERRORS.HOOK_FAILED) {
|
|
1046
|
+
this.closeAfterAnswer(socket);
|
|
1047
|
+
}
|
|
1048
|
+
else {
|
|
1049
|
+
try {
|
|
1050
|
+
socket.destroy();
|
|
1051
|
+
}
|
|
1052
|
+
catch { }
|
|
835
1053
|
}
|
|
836
|
-
catch { }
|
|
837
1054
|
this.activeUploads.delete(socket);
|
|
838
1055
|
if (!hooksFinished)
|
|
839
1056
|
await reportFailure((0, hooks_js_1.callbackError)(error));
|
|
@@ -864,14 +1081,26 @@ class Server extends EventEmitter {
|
|
|
864
1081
|
this.pendingAfterCommit.add(transferId);
|
|
865
1082
|
}
|
|
866
1083
|
/**
|
|
867
|
-
*
|
|
868
|
-
*
|
|
869
|
-
*
|
|
870
|
-
*
|
|
871
|
-
*
|
|
1084
|
+
* Whether the post-commit retention pass waits for the hooks that follow a
|
|
1085
|
+
* commit. Either one defers it: `afterCommit` so its callback still finds
|
|
1086
|
+
* the artifact, and `afterLink` so this commit's own link reconciliation runs
|
|
1087
|
+
* before this commit's retention pass does. That narrows, but does not remove,
|
|
1088
|
+
* the chance that the link already moved: another commit's retention or
|
|
1089
|
+
* reconciliation can do it first, and the hook then sees `UNCHANGED`.
|
|
1090
|
+
*/
|
|
1091
|
+
defersPostCommitRetention() {
|
|
1092
|
+
return this.hooks.afterCommit !== undefined || this.hooks.afterLink !== undefined;
|
|
1093
|
+
}
|
|
1094
|
+
/**
|
|
1095
|
+
* Runs the post-commit retention pass that configured `afterCommit` or
|
|
1096
|
+
* `afterLink` hooks deferred, only after they succeeded, and then stops
|
|
1097
|
+
* owing it. The retention manager already reports its own failures as
|
|
1098
|
+
* non-fatal, so nothing here can fail the upload; the pass is attempted
|
|
1099
|
+
* exactly once per successful hook run, which is why the transfer is cleared
|
|
1100
|
+
* either way.
|
|
872
1101
|
*/
|
|
873
1102
|
async settleDeferredRetention(transferId, owner) {
|
|
874
|
-
if (this.
|
|
1103
|
+
if (!this.defersPostCommitRetention())
|
|
875
1104
|
return;
|
|
876
1105
|
try {
|
|
877
1106
|
if (this.retention)
|
|
@@ -910,6 +1139,7 @@ class Server extends EventEmitter {
|
|
|
910
1139
|
try {
|
|
911
1140
|
this.layout = (0, layout_js_1.initLayout)(this.storageDir);
|
|
912
1141
|
this.releaseLock = await (0, layout_js_1.acquireStorageLock)(this.layout, { storage: this.storage });
|
|
1142
|
+
await (0, layout_js_1.assertNoPendingStorageReset)(this.layout, this.storage);
|
|
913
1143
|
this.sessions = new session_store_js_1.SessionStore({
|
|
914
1144
|
layout: this.layout,
|
|
915
1145
|
maxStagingBytes: this.maxStagingBytes,
|