@torrent-tv/proxy 2.87.12 → 2.87.13
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
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
## 2.87.13
|
|
2
|
+
|
|
3
|
+
- **Fix**: Include distinct active piece reads in memory demand and preserve their slots immediately, so reads without priority-map ranges cannot deadlock the torrent piece store.
|
|
4
|
+
|
|
1
5
|
## 2.87.12
|
|
2
6
|
|
|
3
7
|
- **Fix**: Check each automatic output against whole-machine encoding occupancy before opening it, so a rejected size can resolve to a lower admissible rung instead of failing the request.
|
package/package.json
CHANGED
|
@@ -593,13 +593,13 @@ export class SharedPieceStore {
|
|
|
593
593
|
}
|
|
594
594
|
|
|
595
595
|
/**
|
|
596
|
-
* Whether the machine's share
|
|
596
|
+
* Whether the machine's share is smaller than current read demand.
|
|
597
597
|
*
|
|
598
598
|
* The store holds the window anyway — refusing would leave the read it is
|
|
599
599
|
* serving unable to finish, which is worse — but it is the honest measure of
|
|
600
600
|
* "this machine cannot take any more", and it is measured rather than
|
|
601
|
-
* guessed: it is the last revision's
|
|
602
|
-
*
|
|
601
|
+
* guessed: it is the last revision's comparison of the machine's share
|
|
602
|
+
* against declared windows and distinct pieces pinned by live reads.
|
|
603
603
|
*/
|
|
604
604
|
get isBeyondTheMachine() {
|
|
605
605
|
return this.#beyondTheMachine;
|
|
@@ -650,15 +650,17 @@ export class SharedPieceStore {
|
|
|
650
650
|
* resident piece ends up pinned, the read returns zero bytes and ffmpeg takes
|
|
651
651
|
* that for the end of the file (field 2026-08-15, roadmap item 9).
|
|
652
652
|
*
|
|
653
|
-
*
|
|
654
|
-
*
|
|
655
|
-
*
|
|
653
|
+
* A declared window is not the only evidence of demand: a live reader pins
|
|
654
|
+
* each piece it is waiting on or handing to its consumer. Include those
|
|
655
|
+
* distinct pins as well, because some byte reads do not have a priority-map
|
|
656
|
+
* range to declare.
|
|
656
657
|
*/
|
|
657
658
|
get wantedBytes() {
|
|
658
659
|
const demand = this.#lru.demand();
|
|
660
|
+
const pinned = this.#lru.pinnedCount;
|
|
659
661
|
if (demand.readers > 0) {
|
|
660
662
|
this.#widestSeenPieces = Math.max(this.#widestSeenPieces, demand.widestPieces);
|
|
661
|
-
const pieces = Math.max(MIN_RESIDENT_PIECES, demand.unionPieces, demand.widestPieces);
|
|
663
|
+
const pieces = Math.max(MIN_RESIDENT_PIECES, demand.unionPieces, demand.widestPieces, pinned);
|
|
662
664
|
// Plus room to absorb what arrives while a write is finishing. Asking for
|
|
663
665
|
// exactly what the readers want leaves no free place ever, so every
|
|
664
666
|
// arrival evicts one of them — measured 2026-09-02: `6 reader(s) want 23
|
|
@@ -666,9 +668,9 @@ export class SharedPieceStore {
|
|
|
666
668
|
// that followed.
|
|
667
669
|
return (pieces + this.slackPieces()) * this.#chunkLength;
|
|
668
670
|
}
|
|
669
|
-
// WITH NO
|
|
670
|
-
//
|
|
671
|
-
// `MIN_RESIDENT_PIECES`
|
|
671
|
+
// WITH NO DECLARED RANGE, the floor is the widest prior window or the
|
|
672
|
+
// distinct pieces pinned by current reads, both measured rather than
|
|
673
|
+
// chosen. `MIN_RESIDENT_PIECES` applies until either has been observed.
|
|
672
674
|
//
|
|
673
675
|
// It used to fall to the minimum the moment the last reader went, and the
|
|
674
676
|
// allowance is re-derived once a minute (`STORE_REPORT_INTERVAL_MS`) while
|
|
@@ -677,12 +679,11 @@ export class SharedPieceStore {
|
|
|
677
679
|
// to another file of the same torrent and the store was at
|
|
678
680
|
// `0/3 (0MB of 12MB allowed)` with the machine offering 4.3 GB.
|
|
679
681
|
//
|
|
680
|
-
//
|
|
681
|
-
//
|
|
682
|
-
//
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
return (Math.max(MIN_RESIDENT_PIECES, this.#widestSeenPieces) + this.slackPieces()) * this.#chunkLength;
|
|
682
|
+
// Retaining the last declared window between reads avoids dropping back to
|
|
683
|
+
// the minimum; current pins cover reads that have no declared window. A
|
|
684
|
+
// store that has never been read keeps only the measured minimum.
|
|
685
|
+
const pieces = Math.max(MIN_RESIDENT_PIECES, this.#widestSeenPieces, pinned);
|
|
686
|
+
return (pieces + this.slackPieces()) * this.#chunkLength;
|
|
686
687
|
}
|
|
687
688
|
|
|
688
689
|
reviseGrowthCeiling(allowedBytes) {
|
|
@@ -692,27 +693,33 @@ export class SharedPieceStore {
|
|
|
692
693
|
// the machine was full kept a small allowance for its whole life, however
|
|
693
694
|
// much memory was freed afterwards (roadmap item 2, 2026-09-02).
|
|
694
695
|
const wanted = Math.floor(Number(allowedBytes) / this.#chunkLength);
|
|
695
|
-
// Never below what the live readers together
|
|
696
|
-
// share says less.
|
|
697
|
-
//
|
|
698
|
-
//
|
|
699
|
-
//
|
|
696
|
+
// Never below what the live readers together need, even when the machine's
|
|
697
|
+
// share says less. That is the union of declared windows, or the distinct
|
|
698
|
+
// pieces pinned by direct reads that have no declared window. A store that
|
|
699
|
+
// cannot hold what its readers are pinning cannot complete those reads:
|
|
700
|
+
// every resident piece ends up pinned, a read returns zero bytes and
|
|
701
|
+
// ffmpeg takes that for the end of the file, which killed every encoder on
|
|
702
|
+
// that file in the field on
|
|
700
703
|
// 2026-08-15 (one reader) and again on 2026-09-07 (three readers of one
|
|
701
704
|
// file at once — picture, sound and the edge-warming read — where this
|
|
702
705
|
// floor was still computed from only the widest one of them, `wantedBytes`
|
|
703
706
|
// above already asks for the union and got it right; this is the same
|
|
704
|
-
// union, used as the floor instead of only as the ask).
|
|
705
|
-
//
|
|
707
|
+
// union, used as the floor instead of only as the ask). A live read can
|
|
708
|
+
// also pin pieces without a priority-map range, so those measured pins are
|
|
709
|
+
// part of the same minimum. Exceeding the share is the lesser failure, and
|
|
710
|
+
// the line below says when it happens.
|
|
706
711
|
const demand = this.#lru.demand();
|
|
707
|
-
|
|
712
|
+
const activeDemandPieces = Math.max(
|
|
708
713
|
MIN_RESIDENT_PIECES,
|
|
709
|
-
demand.readers > 0 ? demand.unionPieces :
|
|
714
|
+
demand.readers > 0 ? demand.unionPieces : 0,
|
|
715
|
+
this.#lru.pinnedCount
|
|
716
|
+
);
|
|
717
|
+
this.#growthCeiling = Math.max(
|
|
718
|
+
activeDemandPieces,
|
|
710
719
|
Number.isFinite(wanted) ? wanted : MIN_RESIDENT_PIECES
|
|
711
720
|
);
|
|
712
|
-
const
|
|
713
|
-
|
|
714
|
-
&& wanted < demand.unionPieces;
|
|
715
|
-
this.#beyondTheMachine = belowAWindow;
|
|
721
|
+
const belowActiveDemand = Number.isFinite(wanted) && wanted < activeDemandPieces;
|
|
722
|
+
this.#beyondTheMachine = belowActiveDemand;
|
|
716
723
|
// The LRU is told too. It was constructed with the store's original
|
|
717
724
|
// capacity and never revised, so `isFull()` answered against a number that
|
|
718
725
|
// had not been the limit for some time — dormant only because nothing calls
|
|
@@ -754,7 +761,7 @@ export class SharedPieceStore {
|
|
|
754
761
|
committedBytes: this.#blocksAllocated * this.#chunkLength,
|
|
755
762
|
evicted,
|
|
756
763
|
releasedBlocks,
|
|
757
|
-
|
|
764
|
+
belowActiveDemand
|
|
758
765
|
};
|
|
759
766
|
}
|
|
760
767
|
|
|
@@ -803,6 +810,19 @@ export class SharedPieceStore {
|
|
|
803
810
|
|
|
804
811
|
pin(index) {
|
|
805
812
|
this.#lru.pin(index);
|
|
813
|
+
// A piece reader pins before it waits for the bytes to arrive or return
|
|
814
|
+
// from disk. That is the only live record of some reads; the priority map
|
|
815
|
+
// can have no declared range for them (for example, a verified piece that
|
|
816
|
+
// was spilled). Include the measured set of distinct active pins in the
|
|
817
|
+
// immediate floor so a claim does not wait for the next budget pass to get
|
|
818
|
+
// a slot. `wantedBytes` carries the same demand to the machine-wide owner.
|
|
819
|
+
const pinned = this.#lru.pinnedCount;
|
|
820
|
+
if (pinned > this.#growthCeiling) {
|
|
821
|
+
this.#growthCeiling = pinned;
|
|
822
|
+
this.#lru.setCapacity(pinned);
|
|
823
|
+
this.#beyondTheMachine = true;
|
|
824
|
+
this.#wake();
|
|
825
|
+
}
|
|
806
826
|
}
|
|
807
827
|
|
|
808
828
|
unpin(index) {
|
|
@@ -638,11 +638,11 @@ setInterval(() => {
|
|
|
638
638
|
(revised.releasedBlocks > 0 ? `, gave back ${revised.releasedBlocks} block(s) of memory` : "") +
|
|
639
639
|
` — now ${Math.round(revised.committedBytes / 1048576)}MB committed`
|
|
640
640
|
);
|
|
641
|
-
} else if (revised.
|
|
641
|
+
} else if (revised.belowActiveDemand) {
|
|
642
642
|
log(
|
|
643
|
-
`piece-store "${revised.name.slice(0, 40)}": the machine's share is smaller than
|
|
644
|
-
`
|
|
645
|
-
"
|
|
643
|
+
`piece-store "${revised.name.slice(0, 40)}": the machine's share is smaller than active read demand, ` +
|
|
644
|
+
`so the allowance is held at ${Math.round(revised.ceilingBytes / 1048576)}MB ` +
|
|
645
|
+
"— a store cannot finish a read if it cannot hold the pieces that read is using"
|
|
646
646
|
);
|
|
647
647
|
} else if (revised.committedBytes > revised.ceilingBytes) {
|
|
648
648
|
log(
|
|
@@ -428,6 +428,41 @@ test("a store whose readers have gone asks for nothing, one that never had them
|
|
|
428
428
|
}
|
|
429
429
|
});
|
|
430
430
|
|
|
431
|
+
test("live piece pins set memory demand when no priority-map range is declared", async () => {
|
|
432
|
+
const { store, directory } = await makeStore(2);
|
|
433
|
+
try {
|
|
434
|
+
await put(store, 0, pieceOf(0));
|
|
435
|
+
await put(store, 1, pieceOf(1));
|
|
436
|
+
|
|
437
|
+
store.pin(0);
|
|
438
|
+
store.pin(1);
|
|
439
|
+
store.pin(2);
|
|
440
|
+
|
|
441
|
+
assert.equal(store.stats().demand.readers, 0, "this read has no priority-map range");
|
|
442
|
+
assert.equal(store.stats().pinned, 3, "the three distinct live pieces are measured");
|
|
443
|
+
assert.equal(store.capacity, 3, "the active read demand expands capacity immediately");
|
|
444
|
+
assert.equal(store.wantedBytes, 3 * PIECE, "the budget owner sees the same measured demand");
|
|
445
|
+
|
|
446
|
+
const revised = store.reviseGrowthCeiling(2 * PIECE);
|
|
447
|
+
assert.equal(revised.ceilingBytes, 3 * PIECE, "a budget pass preserves pieces in active reads");
|
|
448
|
+
assert.equal(revised.belowActiveDemand, true, "the store reports that the machine share was short");
|
|
449
|
+
|
|
450
|
+
await put(store, 2, pieceOf(2));
|
|
451
|
+
assert.ok(store.locate(0), "the first active piece remains resident");
|
|
452
|
+
assert.ok(store.locate(1), "the second active piece remains resident");
|
|
453
|
+
assert.ok(store.locate(2), "the pending active piece obtains a memory slot");
|
|
454
|
+
|
|
455
|
+
store.unpin(0);
|
|
456
|
+
store.unpin(1);
|
|
457
|
+
store.unpin(2);
|
|
458
|
+
const released = store.reviseGrowthCeiling(2 * PIECE);
|
|
459
|
+
assert.equal(released.ceilingBytes, 2 * PIECE, "the measured floor falls after the reads release their pieces");
|
|
460
|
+
} finally {
|
|
461
|
+
await store.destroy();
|
|
462
|
+
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
|
|
431
466
|
test("the allowance is never cut below one reader's whole window", async () => {
|
|
432
467
|
const { store, directory } = await makeStore(16);
|
|
433
468
|
try {
|
|
@@ -438,13 +473,13 @@ test("the allowance is never cut below one reader's whole window", async () => {
|
|
|
438
473
|
// which killed every encoder on that file in the field on 2026-08-15.
|
|
439
474
|
const revised = store.reviseGrowthCeiling(2 * PIECE);
|
|
440
475
|
assert.equal(revised.ceilingBytes, 10 * PIECE, "one whole window is the floor");
|
|
441
|
-
assert.equal(revised.
|
|
476
|
+
assert.equal(revised.belowActiveDemand, true, "and the store says the share was smaller than that");
|
|
442
477
|
|
|
443
478
|
// With no reader there is no window to protect and the share is obeyed.
|
|
444
479
|
store.releaseProtection("video");
|
|
445
480
|
const obeyed = store.reviseGrowthCeiling(2 * PIECE);
|
|
446
481
|
assert.equal(obeyed.ceilingBytes, 2 * PIECE);
|
|
447
|
-
assert.equal(obeyed.
|
|
482
|
+
assert.equal(obeyed.belowActiveDemand, false);
|
|
448
483
|
} finally {
|
|
449
484
|
store.destroy(() => undefined);
|
|
450
485
|
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|