@torrent-tv/proxy 2.83.5 → 2.83.6
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 +8 -0
- package/package.json +1 -1
- package/services/orchestrators/EncodeOrchestrator.js +42 -12
- package/services/torrent-pool.js +3 -3
- package/services/torrent-worker/client.js +5 -2
- package/services/torrent-worker/worker.js +21 -2
- package/test/logger-repeats.test.js +15 -1
- package/utils/logger.js +44 -14
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
## 2.83.6
|
|
2
|
+
- **Fix**: A line forwarded from the torrent thread is no longer judged twice. That thread holds the same repeat rule and applies it before forwarding, and the main thread ran it again on a different history. It does not lose lines today — a re-printed line carries its held-back count, which makes the text unique — but that is an accident of the wording, against a promise this module states outright: a line cannot reach the console and miss the file. It also filled the main thread's bounded map with keys that can never repeat.
|
|
3
|
+
- **Fix**: The LEVEL travels with a forwarded line. It was dropped at the send, so every warning and every error the torrent thread has ever written arrived on the other side as information, and was coloured and recorded as such.
|
|
4
|
+
- **Fix**: The wait after a lost input is cleared only by a run that PRODUCED something, and the field documenting it said "cleared by any other ending" — the rule it replaced, two screens above the code that contradicted it.
|
|
5
|
+
- **New**: `torrent-pool: N piece claim(s) withdrawn` is printed. The pool counted them and nothing read the count, which is the state this project's own rule calls indistinguishable from having noticed once. The gap between it and the stores' `withdrawn=` is the reading: announcements far above withdrawals mean the stores are mostly dropping pieces that were never completed, and a withdrawal count stuck at zero while announcements climb means the mechanism is not reaching the torrent at all.
|
|
6
|
+
- **Chore**: What the plan remembers about an output's input is forgotten when the priority map says nobody is coming there. Three maps keyed by address would otherwise keep an entry for the life of the process, and the wait they describe would greet whoever opened that output next.
|
|
7
|
+
- **Chore**: The suffix saying how many repeats were held back was built in two branches with the same expression; one function now. The quiet check returned milliseconds that its only reader compared against zero; a boolean now.
|
|
8
|
+
|
|
1
9
|
## 2.83.5
|
|
2
10
|
- **Fix**: ONE OWNER OF THE FACT "THIS PROXY HAS PIECE N", AND IT IS THE STORE. The disk tier drops a piece once every reader is past it — correctly, and that is what bounds the spill — while the library kept a second copy of the same fact in its completion bitfield and nothing reconciled them. A read then concluded the piece was had, asked for it, was told it was absent, and failed; nor was it ever fetched again, because the library does not download what it believes it owns. Field 2026-09-12: a film played 80 seconds (1920 frames, and the picture never moved again); the encoder ran on to 725 s, so some 565 spilled pieces fell behind every read head and were dropped, piece 0 among them; the encoder lost its input and restarted, which re-opens the input at byte 0; and `/stream` answered `0 of 2363497962 bytes: Piece 0 is verified but absent from the store` to every read for the next 92 minutes while the browser retried one segment against a healthy transport at 4 ms.
|
|
3
11
|
- **Fix**: So the store announces when it can no longer produce a piece AT ALL — not resident, not on disk, not inside a file held whole — and the pool withdraws the library's claim. The eviction's own bargain, written in its comment as "a seek back re-downloads it", is true for the first time. The three cases are one rule rather than a list of call sites: the disk tier says only that IT has lost a piece, and the store decides whether that is a loss at all, so a piece dropped as a duplicate of an assembled file says nothing and a piece still resident says nothing.
|
package/package.json
CHANGED
|
@@ -63,7 +63,8 @@ export class EncodeOrchestrator {
|
|
|
63
63
|
|
|
64
64
|
/**
|
|
65
65
|
* How many times in a row an output's run has died because its input was not
|
|
66
|
-
* there. Cleared by
|
|
66
|
+
* there. Cleared only by a run that PRODUCED something, which is the only
|
|
67
|
+
* proof that the input can be read — see `#noteInputAvailability`.
|
|
67
68
|
*
|
|
68
69
|
* @type {Map<string, number>}
|
|
69
70
|
*/
|
|
@@ -307,6 +308,14 @@ export class EncodeOrchestrator {
|
|
|
307
308
|
*/
|
|
308
309
|
notePriorityMap(address, zones) {
|
|
309
310
|
this.demand.state(address, zones);
|
|
311
|
+
// NOBODY IS COMING HERE, so what was remembered about this output's input
|
|
312
|
+
// is about nobody. Kept, those three entries would stay for the life of the
|
|
313
|
+
// process, and the wait they describe would greet whoever opens this output
|
|
314
|
+
// next — a viewer arriving is new information, and one attempt for them is
|
|
315
|
+
// right whatever the last one met.
|
|
316
|
+
if (!Array.isArray(zones) || zones.length === 0) {
|
|
317
|
+
this.#forgetInputState(address);
|
|
318
|
+
}
|
|
310
319
|
}
|
|
311
320
|
|
|
312
321
|
/**
|
|
@@ -408,8 +417,7 @@ export class EncodeOrchestrator {
|
|
|
408
417
|
// AND ONLY WHILE NOTHING IS PRODUCING THERE. A run still alive on this
|
|
409
418
|
// output is proof the input can be read, whatever a run beside it met, so
|
|
410
419
|
// the wait must not silence an output that is working.
|
|
411
|
-
|
|
412
|
-
if (quietMs > 0 && this.runsOn(address).every((run) => !run.isAlive)) {
|
|
420
|
+
if (this.#isQuiet(address) && this.runsOn(address).every((run) => !run.isAlive)) {
|
|
413
421
|
return;
|
|
414
422
|
}
|
|
415
423
|
// ONE MAP, NOT ONE WINDOW PER VIEWER PER ZONE.
|
|
@@ -812,23 +820,45 @@ export class EncodeOrchestrator {
|
|
|
812
820
|
}
|
|
813
821
|
|
|
814
822
|
/**
|
|
815
|
-
*
|
|
816
|
-
*
|
|
823
|
+
* Whether this output is still waiting before anything may be placed on it.
|
|
824
|
+
*
|
|
825
|
+
* A boolean rather than the milliseconds left: the figure had one reader and
|
|
826
|
+
* that reader only asked whether it was above zero, so it was a number
|
|
827
|
+
* computed and thrown away. How long the wait is was said when it began.
|
|
817
828
|
*
|
|
818
829
|
* @param {string} address
|
|
819
|
-
* @returns {
|
|
830
|
+
* @returns {boolean}
|
|
820
831
|
*/
|
|
821
|
-
#
|
|
832
|
+
#isQuiet(address) {
|
|
822
833
|
const until = this.#quietUntil.get(address);
|
|
823
834
|
if (!Number.isFinite(until)) {
|
|
824
|
-
return
|
|
835
|
+
return false;
|
|
825
836
|
}
|
|
826
|
-
|
|
827
|
-
if (left <= 0) {
|
|
837
|
+
if (until - this.now() <= 0) {
|
|
828
838
|
this.#quietUntil.delete(address);
|
|
829
|
-
return
|
|
839
|
+
return false;
|
|
840
|
+
}
|
|
841
|
+
return true;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
/**
|
|
845
|
+
* Forget everything remembered about an output nobody is producing for.
|
|
846
|
+
*
|
|
847
|
+
* Three maps are keyed by address, and an output that goes away leaves an
|
|
848
|
+
* entry in each for the life of the process. Small, and exactly the shape of
|
|
849
|
+
* accumulation this layer was built to remove from the session.
|
|
850
|
+
*
|
|
851
|
+
* @param {string} address
|
|
852
|
+
* @returns {void}
|
|
853
|
+
*/
|
|
854
|
+
#forgetInputState(address) {
|
|
855
|
+
this.#inputLostAttempts.delete(address);
|
|
856
|
+
this.#quietUntil.delete(address);
|
|
857
|
+
const timer = this.#quietTimers.get(address);
|
|
858
|
+
if (timer) {
|
|
859
|
+
clearTimeout(timer);
|
|
860
|
+
this.#quietTimers.delete(address);
|
|
830
861
|
}
|
|
831
|
-
return left;
|
|
832
862
|
}
|
|
833
863
|
|
|
834
864
|
/**
|
package/services/torrent-pool.js
CHANGED
|
@@ -1067,6 +1067,9 @@ export class TorrentPool {
|
|
|
1067
1067
|
*/
|
|
1068
1068
|
#wholeSources = new Set();
|
|
1069
1069
|
|
|
1070
|
+
/** How many claims this pool has withdrawn, over its whole life. */
|
|
1071
|
+
#claimsWithdrawn = 0;
|
|
1072
|
+
|
|
1070
1073
|
/**
|
|
1071
1074
|
* Say that every file of a source is held whole.
|
|
1072
1075
|
*
|
|
@@ -1165,9 +1168,6 @@ export class TorrentPool {
|
|
|
1165
1168
|
}
|
|
1166
1169
|
}
|
|
1167
1170
|
|
|
1168
|
-
/** How many claims this pool has withdrawn, over its whole life. */
|
|
1169
|
-
#claimsWithdrawn = 0;
|
|
1170
|
-
|
|
1171
1171
|
get claimsWithdrawn() {
|
|
1172
1172
|
return this.#claimsWithdrawn;
|
|
1173
1173
|
}
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
import { Worker } from "node:worker_threads";
|
|
18
18
|
import { Readable } from "node:stream";
|
|
19
19
|
import { fileURLToPath } from "node:url";
|
|
20
|
-
import { logger } from "../../utils/logger.js";
|
|
20
|
+
import { logger, writeAlreadyDecided } from "../../utils/logger.js";
|
|
21
21
|
import { createCaller, createReceiveStream } from "./channel.js";
|
|
22
22
|
import { Command, Event } from "./protocol.js";
|
|
23
23
|
|
|
@@ -218,7 +218,10 @@ export class TorrentWorkerClient {
|
|
|
218
218
|
this.#fragmentReaders.delete(message.id);
|
|
219
219
|
break;
|
|
220
220
|
case Event.LOG:
|
|
221
|
-
|
|
221
|
+
// Written as it is: the worker holds the same repeat rule and has
|
|
222
|
+
// already applied it, and deciding again here would be one decision
|
|
223
|
+
// taken twice on two different histories.
|
|
224
|
+
writeAlreadyDecided(message.level ?? "info", `torrent-worker: ${message.message}`);
|
|
222
225
|
break;
|
|
223
226
|
case Event.FILE_COMPLETE:
|
|
224
227
|
// Recorded here so the stream route can answer from the file without
|
|
@@ -56,8 +56,12 @@ import { forwardLogsTo, logger } from "../../utils/logger.js";
|
|
|
56
56
|
* copy of the logger that had no file, and every one of their lines was lost:
|
|
57
57
|
* measured over a whole 49 938-line file, not one of them was in it.
|
|
58
58
|
*/
|
|
59
|
-
forwardLogsTo((
|
|
60
|
-
|
|
59
|
+
forwardLogsTo((level, message) => {
|
|
60
|
+
// THE LEVEL TRAVELS TOO. It was dropped here, so every line this thread wrote
|
|
61
|
+
// — a warning about a spill that failed, an error about a torrent that went
|
|
62
|
+
// away — arrived on the other side as information and was coloured and
|
|
63
|
+
// recorded as such.
|
|
64
|
+
parentPort.postMessage({ type: Event.LOG, level, message });
|
|
61
65
|
});
|
|
62
66
|
|
|
63
67
|
// Imported dynamically, and that is load-bearing: static imports are RESOLVED
|
|
@@ -658,6 +662,7 @@ startMemoryReport({
|
|
|
658
662
|
const STORE_REPORT_INTERVAL_MS = 60_000;
|
|
659
663
|
|
|
660
664
|
/** Last reported reserve, so an unchanged one stays silent. */
|
|
665
|
+
let lastClaimsWithdrawn = 0;
|
|
661
666
|
let lastReserveBytes = 0;
|
|
662
667
|
|
|
663
668
|
/** Last reported figures per store, so unchanged ones stay silent. */
|
|
@@ -695,6 +700,20 @@ setInterval(() => {
|
|
|
695
700
|
);
|
|
696
701
|
}
|
|
697
702
|
}
|
|
703
|
+
// WHAT THE ANNOUNCEMENTS ACTUALLY DID. The stores count every piece they
|
|
704
|
+
// stopped being able to produce; this counts the ones where the library did
|
|
705
|
+
// still hold a claim and it was taken back. The GAP between the two is the
|
|
706
|
+
// reading: announcements far above withdrawals mean the stores are mostly
|
|
707
|
+
// dropping pieces that were never completed, and a withdrawal count stuck at
|
|
708
|
+
// zero while announcements climb means the mechanism is not reaching the
|
|
709
|
+
// torrent at all.
|
|
710
|
+
if (pool.claimsWithdrawn !== lastClaimsWithdrawn) {
|
|
711
|
+
lastClaimsWithdrawn = pool.claimsWithdrawn;
|
|
712
|
+
log(
|
|
713
|
+
`torrent-pool: ${pool.claimsWithdrawn} piece claim(s) withdrawn — pieces this proxy ` +
|
|
714
|
+
"had dropped and has now told the swarm it needs again"
|
|
715
|
+
);
|
|
716
|
+
}
|
|
698
717
|
const reserveNow = machineReserveBytes();
|
|
699
718
|
if (reserveNow !== reserveBefore || reserveNow !== lastReserveBytes) {
|
|
700
719
|
lastReserveBytes = reserveNow;
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import test from "node:test";
|
|
16
16
|
import assert from "node:assert/strict";
|
|
17
|
-
import { logger } from "../utils/logger.js";
|
|
17
|
+
import { logger, writeAlreadyDecided } from "../utils/logger.js";
|
|
18
18
|
|
|
19
19
|
/** What reached the console while `body` ran. */
|
|
20
20
|
function captured(body) {
|
|
@@ -109,6 +109,20 @@ test("when it is said again, it says how many were held back", async () => {
|
|
|
109
109
|
assert.ok(Number(said[2]) > 0, `the span it covers must be stated: ${later[0]}`);
|
|
110
110
|
});
|
|
111
111
|
|
|
112
|
+
test("a line already decided on another thread is written as it is", () => {
|
|
113
|
+
const message = unique("forwarded");
|
|
114
|
+
const lines = captured(() => {
|
|
115
|
+
writeAlreadyDecided("info", message);
|
|
116
|
+
writeAlreadyDecided("info", message);
|
|
117
|
+
writeAlreadyDecided("warn", message);
|
|
118
|
+
});
|
|
119
|
+
// The worker holds the same rule and applies it before forwarding. Deciding
|
|
120
|
+
// again here would be one decision taken twice on two different histories,
|
|
121
|
+
// and the file's own promise is that a line cannot reach the console and miss
|
|
122
|
+
// the file.
|
|
123
|
+
assert.equal(lines.length, 3, "a forwarded line is not judged a second time");
|
|
124
|
+
});
|
|
125
|
+
|
|
112
126
|
test("every level goes through the same rule", () => {
|
|
113
127
|
const message = unique("levels");
|
|
114
128
|
const lines = captured(() => {
|
package/utils/logger.js
CHANGED
|
@@ -166,6 +166,30 @@ export const logger = {
|
|
|
166
166
|
error: (message) => write("error", message, chalk.red, console.error)
|
|
167
167
|
};
|
|
168
168
|
|
|
169
|
+
/**
|
|
170
|
+
* Write a line that has ALREADY been through the repeat rule on another thread.
|
|
171
|
+
*
|
|
172
|
+
* The worker holds the same rule and applies it before forwarding, so running
|
|
173
|
+
* it again here would be one decision taken twice, on two different histories.
|
|
174
|
+
* It does not lose lines today — a re-printed line carries its held-back count,
|
|
175
|
+
* which makes the text unique — but that is an accident of the wording, and the
|
|
176
|
+
* file's own promise is that a line cannot reach the console and miss the file.
|
|
177
|
+
* It also filled this thread's bounded map with keys that can never repeat.
|
|
178
|
+
*
|
|
179
|
+
* @param {string} level
|
|
180
|
+
* @param {string} message - Already decided; written as it is.
|
|
181
|
+
* @returns {void}
|
|
182
|
+
*/
|
|
183
|
+
export function writeAlreadyDecided(level, message) {
|
|
184
|
+
const colour = level === "error"
|
|
185
|
+
? chalk.red
|
|
186
|
+
: level === "warn" ? chalk.yellow : level === "success" ? chalk.green : chalk.cyan;
|
|
187
|
+
const toConsole = level === "error" ? console.error : level === "warn" ? console.warn : console.log;
|
|
188
|
+
const line = `${PREFIX} [${ts()}] ${message}`;
|
|
189
|
+
toConsole(colour(line));
|
|
190
|
+
toFile(line);
|
|
191
|
+
}
|
|
192
|
+
|
|
169
193
|
/**
|
|
170
194
|
* An established fact is said once, then with decreasing frequency.
|
|
171
195
|
*
|
|
@@ -195,6 +219,22 @@ const REPEAT_KEYS = 512;
|
|
|
195
219
|
/** @type {Map<string, { suppressed: number, printedAt: number, interval: number }>} */
|
|
196
220
|
const recent = new Map();
|
|
197
221
|
|
|
222
|
+
/**
|
|
223
|
+
* What to append when a line is said again after repeats were held back.
|
|
224
|
+
*
|
|
225
|
+
* One function because it was written twice, in the two branches that decide to
|
|
226
|
+
* speak — which is how two statements of one rule drift apart.
|
|
227
|
+
*
|
|
228
|
+
* @param {number} heldBack
|
|
229
|
+
* @param {number} overMs
|
|
230
|
+
* @returns {string}
|
|
231
|
+
*/
|
|
232
|
+
function heldBackSuffix(heldBack, overMs) {
|
|
233
|
+
return heldBack > 0
|
|
234
|
+
? ` [said ${heldBack} more time(s) in the last ${(overMs / 1000).toFixed(1)}s]`
|
|
235
|
+
: "";
|
|
236
|
+
}
|
|
237
|
+
|
|
198
238
|
/**
|
|
199
239
|
* Whether this line is a repeat to hold back, and what to say if it is not.
|
|
200
240
|
*
|
|
@@ -223,12 +263,7 @@ function repeatCheck(message) {
|
|
|
223
263
|
}
|
|
224
264
|
}
|
|
225
265
|
recent.set(message, { suppressed: 0, printedAt: now, interval: REPEAT_FIRST_MS });
|
|
226
|
-
return {
|
|
227
|
-
hold: false,
|
|
228
|
-
suffix: heldBack > 0
|
|
229
|
-
? ` [said ${heldBack} more time(s) in the last ${(overMs / 1000).toFixed(1)}s]`
|
|
230
|
-
: ""
|
|
231
|
-
};
|
|
266
|
+
return { hold: false, suffix: heldBackSuffix(heldBack, overMs) };
|
|
232
267
|
}
|
|
233
268
|
if (now - seen.printedAt < seen.interval) {
|
|
234
269
|
seen.suppressed += 1;
|
|
@@ -242,14 +277,9 @@ function repeatCheck(message) {
|
|
|
242
277
|
printedAt: now,
|
|
243
278
|
interval: Math.min(REPEAT_MAX_MS, seen.interval * 2)
|
|
244
279
|
});
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
// drops repeats reports a healthy proxy where a loop was spinning.
|
|
249
|
-
suffix: heldBack > 0
|
|
250
|
-
? ` [said ${heldBack} more time(s) in the last ${(overMs / 1000).toFixed(1)}s]`
|
|
251
|
-
: ""
|
|
252
|
-
};
|
|
280
|
+
// SAID, not merely hidden: the rate is the fact here, and a log that quietly
|
|
281
|
+
// drops repeats reports a healthy proxy where a loop was spinning.
|
|
282
|
+
return { hold: false, suffix: heldBackSuffix(heldBack, overMs) };
|
|
253
283
|
}
|
|
254
284
|
|
|
255
285
|
/**
|