@torrent-tv/proxy 2.87.20 → 2.87.21

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.21
2
+
3
+ - **Fix**: Give waiting source reads ordered memory reservations ahead of new piece arrivals, share concurrent revival work for one piece, and reserve memory before allocating assembled-file bytes.
4
+
1
5
  ## 2.87.20
2
6
 
3
7
  - **Fix**: Expose completed files through the worker pool adapter so source HTTP reads use the assembled file; open it before sending headers and return to torrent reads if it has already been removed.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torrent-tv/proxy",
3
- "version": "2.87.20",
3
+ "version": "2.87.21",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "publishConfig": {
@@ -405,6 +405,12 @@ export class SharedPieceStore {
405
405
  */
406
406
  #readElsewhere = null;
407
407
 
408
+ /** Reads waiting for a reservation, in arrival order. */
409
+ #readerClaims = [];
410
+
411
+ /** One revival per piece, shared by concurrent readers. */
412
+ #revivals = new Map();
413
+
408
414
  /**
409
415
  * Whether a piece can be had elsewhere, asked without reading it — so a
410
416
  * spilled copy that has become a duplicate can be dropped.
@@ -858,7 +864,7 @@ export class SharedPieceStore {
858
864
  }
859
865
  }
860
866
 
861
- async #claimSlot() {
867
+ async #claimSlot(forReader = false) {
862
868
  // How long THIS claim has been trying, kept here and not on the store.
863
869
  // It was a field, `#pinnedWaitStartedAt`, shared by the two waits inside
864
870
  // `#claimSlotOnce` — the one for the disk and the one for a piece that may
@@ -870,23 +876,36 @@ export class SharedPieceStore {
870
876
  // claimants a shared field is wrong anyway, since one caller giving up
871
877
  // would reset the wait of every other.
872
878
  const waitingSince = Date.now();
873
- for (;;) {
874
- if (this.#closed) {
875
- throw new Error("Piece store is closed.");
879
+ const claimant = forReader ? {} : null;
880
+ if (claimant) this.#readerClaims.push(claimant);
881
+ try {
882
+ for (;;) {
883
+ if (this.#closed) {
884
+ throw new Error("Piece store is closed.");
885
+ }
886
+ // New arrivals must not take every released block ahead of an encoder
887
+ // already waiting to read. Readers share one ordered reservation queue.
888
+ const hasTurn = this.#readerClaims.length === 0 || this.#readerClaims[0] === claimant;
889
+ const isReserved = hasTurn && await this.#claimSlotOnce(waitingSince);
890
+ if (isReserved) {
891
+ let released = false;
892
+ return () => {
893
+ if (released) {
894
+ return;
895
+ }
896
+ released = true;
897
+ this.#outstandingPieces -= 1;
898
+ this.#noteProgress();
899
+ };
900
+ }
901
+ await this.#waitForSlot();
876
902
  }
877
- const isReserved = await this.#claimSlotOnce(waitingSince);
878
- if (isReserved) {
879
- let released = false;
880
- return () => {
881
- if (released) {
882
- return;
883
- }
884
- released = true;
885
- this.#outstandingPieces -= 1;
886
- this.#noteProgress();
887
- };
903
+ } finally {
904
+ if (claimant) {
905
+ const at = this.#readerClaims.indexOf(claimant);
906
+ if (at >= 0) this.#readerClaims.splice(at, 1);
907
+ this.#wake();
888
908
  }
889
- await this.#waitForSlot();
890
909
  }
891
910
  }
892
911
 
@@ -1395,15 +1414,14 @@ export class SharedPieceStore {
1395
1414
  // Not spilled, but the file it belongs to may be here whole — which is
1396
1415
  // the ordinary case once a film has been assembled and its spilled copy
1397
1416
  // dropped as the duplicate it had become.
1398
- const whole = await this.#fromWholeFiles(index);
1399
- if (!whole) {
1400
- return null;
1401
- }
1402
- this.#counters.fromWholeFile += 1;
1417
+ if (this.#isElsewhere && !this.isInWholeFiles(index)) return null;
1403
1418
  // Fragment readers require shared memory, including when the bytes now
1404
- // live in an assembled file. Wait for the same reservation as disk reads.
1405
- const release = await this.#claimSlot();
1419
+ // live in an assembled file. Reserve before allocating ordinary bytes.
1420
+ const release = await this.#claimSlot(true);
1406
1421
  try {
1422
+ const whole = await this.#fromWholeFiles(index);
1423
+ if (!whole) return null;
1424
+ this.#counters.fromWholeFile += 1;
1407
1425
  return this.#registerPiece(index, this.#copyIntoNewBuffer(index, whole));
1408
1426
  } finally {
1409
1427
  release();
@@ -1415,7 +1433,7 @@ export class SharedPieceStore {
1415
1433
  const releaseDisk = this.#disk.hold?.(index) ?? (() => {});
1416
1434
  let release = null;
1417
1435
  try {
1418
- release = await this.#claimSlot();
1436
+ release = await this.#claimSlot(true);
1419
1437
  // Another caller may have brought it back while this one waited for a
1420
1438
  // slot. Registering a second buffer for the same piece would leave
1421
1439
  // whoever holds the first reading memory nothing evicts.
@@ -1860,7 +1878,12 @@ export class SharedPieceStore {
1860
1878
  return this.locate(index);
1861
1879
  }
1862
1880
 
1863
- const revived = await this.#revive(index);
1881
+ let revival = this.#revivals.get(index);
1882
+ if (!revival) {
1883
+ revival = this.#revive(index).finally(() => this.#revivals.delete(index));
1884
+ this.#revivals.set(index, revival);
1885
+ }
1886
+ const revived = await revival;
1864
1887
  if (revived === null) {
1865
1888
  return null;
1866
1889
  }
@@ -34,6 +34,10 @@ function makeDisk({ failWrite = false, failRead = false, holdWrites = false } =
34
34
  resume();
35
35
  }
36
36
  },
37
+ finishWrites() {
38
+ holdWrites = false;
39
+ this.releaseWrites();
40
+ },
37
41
  get heldCount() {
38
42
  return held.length;
39
43
  },
@@ -150,15 +154,20 @@ test("a revival holds disk bytes across the asynchronous memory reservation", as
150
154
 
151
155
  test("an assembled-file revival returns shared bytes and preserves a concurrent pinned reader", async () => {
152
156
  const disk = makeDisk();
157
+ let reads = 0;
153
158
  const store = new SharedPieceStore(CHUNK, {
154
159
  length: CHUNK * 16, memoryBytes: CHUNK * 4, disk,
155
- readPieceElsewhere: async ({ index }) => piece(index), name: "whole-file-read"
160
+ readPieceElsewhere: async ({ index }) => {
161
+ reads += 1;
162
+ return piece(index);
163
+ }, name: "whole-file-read"
156
164
  });
157
165
  try {
158
166
  store.pin(0);
159
167
  const [first, second] = await Promise.all([store.reside(0), store.reside(0)]);
160
168
  assert.ok(first.buffer instanceof SharedArrayBuffer);
161
169
  assert.equal(first.buffer, second.buffer);
170
+ assert.equal(reads, 1, "concurrent readers repeated the whole-file allocation and read");
162
171
  await put(store, 1);
163
172
  await put(store, 2);
164
173
  assert.deepEqual(Buffer.from(first.buffer, first.offset, first.length), piece(0));
@@ -170,6 +179,35 @@ test("an assembled-file revival returns shared bytes and preserves a concurrent
170
179
  }
171
180
  });
172
181
 
182
+ test("a waiting source reader receives a released slot before queued arrivals", async () => {
183
+ const disk = makeDisk({ holdWrites: true });
184
+ const { store } = makeStore({ pieces: 2, disk });
185
+ let arrived = false;
186
+ try {
187
+ await put(store, 0);
188
+ await put(store, 1);
189
+ store.pin(1);
190
+ disk.stored.set(4, piece(4));
191
+ const spilling = put(store, 2);
192
+ await until(() => disk.heldCount > 0, "an arriving piece occupies the spill slot");
193
+ const arrival = put(store, 3).then(() => { arrived = true; });
194
+ store.pin(4);
195
+ const reading = store.reside(4);
196
+ await until(() => store.stats().waitedForDisk >= 2, "both claims wait behind the spill");
197
+ disk.finishWrites();
198
+ const located = await reading;
199
+ assert.deepEqual(Buffer.from(located.buffer, located.offset, located.length), piece(4));
200
+ assert.equal(arrived, false, "an arriving piece overtook an already waiting reader");
201
+ store.unpin(1);
202
+ store.unpin(4);
203
+ await Promise.all([spilling, arrival]);
204
+ assert.equal(store.stats().outstanding, 0);
205
+ } finally {
206
+ disk.finishWrites();
207
+ await new Promise((resolve) => store.destroy(resolve));
208
+ }
209
+ });
210
+
173
211
  /**
174
212
  * Wait until a condition holds, rather than for a chosen interval — a test that
175
213
  * sleeps samples, it does not check (roadmap item 54).