@torrent-tv/proxy 2.81.1 → 2.81.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 +5 -0
- package/package.json +1 -1
- package/server.js +9 -0
- package/services/disk/free.js +38 -0
- package/services/disk/wire.js +8 -7
- package/services/hls-session-manager.js +9 -4
- package/test/disk-space.test.js +15 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
## 2.81.2
|
|
2
|
+
|
|
3
|
+
- **Fix**: `disk: 0MB free` on a host with 103 GB. The free space was read from the segments’ own directory, which is made when the first session starts and removed when the proxy stops — so at every start, and after every clean exit, the reading failed and answered zero, which means “no room” to everything downstream. It reads the nearest ancestor that exists; the disk is the same disk either way.
|
|
4
|
+
- **Fix**: The spilled pieces were never registered with the owner of the disk, so it divided nothing and they kept the ceiling they had. The session manager holds no torrent pool — it is handed closures over the thread boundary — and 2.81.0 asked it for a field that does not exist. They arrive the same way everything else from that thread does.
|
|
5
|
+
|
|
1
6
|
## 2.81.1
|
|
2
7
|
|
|
3
8
|
- **Fix**: The `disk:` line is actually said. 2.81.0 built the reading and called it from nowhere, so the one thing that can answer "why is there no room" was absent from the log. The owner says it itself, once a pass, in the series beside the memory reading.
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -178,6 +178,15 @@ export async function startProxyServer({
|
|
|
178
178
|
// CPU-bound transcode from a download-starved input before downscaling.
|
|
179
179
|
// What every torrent here has moved, so the proxy can price its own
|
|
180
180
|
// downloading, hashing and delivery against the machine (roadmap item 7).
|
|
181
|
+
// What the spilled pieces weigh on the torrent thread, and how to tell them
|
|
182
|
+
// their share of the disk. The owner of the disk is on this side, where the
|
|
183
|
+
// segments are; the pieces are on the other.
|
|
184
|
+
spillDisk: typeof torrentPool.allowSpillBytes === "function"
|
|
185
|
+
? {
|
|
186
|
+
held: () => torrentPool.spilledBytes ?? 0,
|
|
187
|
+
allow: (bytes) => torrentPool.allowSpillBytes(bytes)
|
|
188
|
+
}
|
|
189
|
+
: null,
|
|
181
190
|
getTorrentTotals: async () => {
|
|
182
191
|
if (typeof torrentPool.getTorrentTotals !== "function") {
|
|
183
192
|
return null;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file How much room the disk holding a directory has.
|
|
3
|
+
*
|
|
4
|
+
* Separate from the plain `statfs` reading because of one case that is the
|
|
5
|
+
* normal one, not an edge: the directory may not exist yet. The segments live
|
|
6
|
+
* under `os.tmpdir()/torrent-tv-hls`, which is made when the first session is
|
|
7
|
+
* created and removed when the proxy stops — so at every start, and after every
|
|
8
|
+
* clean exit, `statfs` on it fails. Field 2026-09-10: the first reading said
|
|
9
|
+
* `disk: 0MB free` on a host with 103 GB, and zero means "no room" to everything
|
|
10
|
+
* that reads it.
|
|
11
|
+
*
|
|
12
|
+
* The disk is the same disk whether or not that directory has been made yet, so
|
|
13
|
+
* the answer is the nearest ancestor that exists.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { statfs } from "node:fs/promises";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {string} directory
|
|
21
|
+
* @returns {Promise<number | null>} Bytes free, or null where nothing answered.
|
|
22
|
+
*/
|
|
23
|
+
export async function freeBytesFor(directory) {
|
|
24
|
+
let at = path.resolve(directory);
|
|
25
|
+
for (let depth = 0; depth < 16; depth += 1) {
|
|
26
|
+
try {
|
|
27
|
+
const stats = await statfs(at);
|
|
28
|
+
return Number(stats.bavail) * Number(stats.bsize);
|
|
29
|
+
} catch {
|
|
30
|
+
const up = path.dirname(at);
|
|
31
|
+
if (up === at) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
at = up;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
package/services/disk/wire.js
CHANGED
|
@@ -19,14 +19,16 @@ import { DiskSpace } from "./DiskSpace.js";
|
|
|
19
19
|
*
|
|
20
20
|
* @param {object} params
|
|
21
21
|
* @param {{ root: string, stats: () => { bytes: number } }} params.segmentStore
|
|
22
|
-
* @param {{
|
|
22
|
+
* @param {{ held: () => number, allow: (bytes: number) => unknown }} [params.spill] -
|
|
23
|
+
* The pieces the memory store spills. They live on the torrent thread, so
|
|
24
|
+
* this is a pair of closures over the channel rather than the pool itself.
|
|
23
25
|
* @param {(directory: string) => Promise<number | null>} params.readFree
|
|
24
26
|
* @param {{ info: Function, warn?: Function }} [params.logger]
|
|
25
27
|
* @returns {{ revise: () => Promise<unknown>, segmentBytes: () => number, describe: () => string }}
|
|
26
28
|
* What the segments may hold is asked for rather than pushed: zero until the
|
|
27
29
|
* first revision, and zero stops growth rather than licensing it.
|
|
28
30
|
*/
|
|
29
|
-
export function wireDiskSpace({ segmentStore,
|
|
31
|
+
export function wireDiskSpace({ segmentStore, spill, readFree, logger }) {
|
|
30
32
|
const space = new DiskSpace({ readFree: () => readFree(segmentStore.root), logger });
|
|
31
33
|
let segmentBytes = 0;
|
|
32
34
|
space.register({
|
|
@@ -39,16 +41,15 @@ export function wireDiskSpace({ segmentStore, torrentPool, readFree, logger }) {
|
|
|
39
41
|
segmentBytes = bytes;
|
|
40
42
|
}
|
|
41
43
|
});
|
|
42
|
-
if (typeof
|
|
43
|
-
// The
|
|
44
|
-
// the share travels the channel that already carries everything else, and
|
|
44
|
+
if (typeof spill?.allow === "function") {
|
|
45
|
+
// The share travels the channel that already carries everything else, and
|
|
45
46
|
// the reply says what they hold — one exchange, both directions.
|
|
46
47
|
space.register({
|
|
47
48
|
name: "spilled pieces",
|
|
48
|
-
held: () =>
|
|
49
|
+
held: () => spill.held?.() ?? 0,
|
|
49
50
|
wanted: () => Number.MAX_SAFE_INTEGER,
|
|
50
51
|
allow: (bytes) => {
|
|
51
|
-
void
|
|
52
|
+
void spill.allow(bytes);
|
|
52
53
|
}
|
|
53
54
|
});
|
|
54
55
|
}
|
|
@@ -93,8 +93,8 @@ import { Viewers } from "./viewer/Viewers.js";
|
|
|
93
93
|
import { LiveOutputs } from "./output/LiveOutputs.js";
|
|
94
94
|
import { variantHeightsFor } from "./output/ladder.js";
|
|
95
95
|
import { EncodeOrchestrator } from "./orchestrators/EncodeOrchestrator.js";
|
|
96
|
-
import { readDiskFree } from "./memory-report.js";
|
|
97
96
|
import { wireDiskSpace } from "./disk/wire.js";
|
|
97
|
+
import { freeBytesFor } from "./disk/free.js";
|
|
98
98
|
|
|
99
99
|
/**
|
|
100
100
|
* Whether an encoder run died because its INPUT went away, rather than because
|
|
@@ -1398,7 +1398,8 @@ export class HlsSessionManager {
|
|
|
1398
1398
|
segmentFormatId = undefined,
|
|
1399
1399
|
stateDir = "",
|
|
1400
1400
|
segmentStore = null,
|
|
1401
|
-
getTorrentTotals
|
|
1401
|
+
getTorrentTotals,
|
|
1402
|
+
spillDisk = null}) {
|
|
1402
1403
|
this.enabled = Boolean(enabled);
|
|
1403
1404
|
this.ffmpegBin = ffmpegBin;
|
|
1404
1405
|
this.keyframeTableBudgetMs = Number.isFinite(keyframeTableBudgetMs) && keyframeTableBudgetMs > 0
|
|
@@ -1451,6 +1452,10 @@ export class HlsSessionManager {
|
|
|
1451
1452
|
// torrent itself costs the machine (item 7). Optional: a proxy wired
|
|
1452
1453
|
// without it simply never learns that figure.
|
|
1453
1454
|
this.getTorrentTotals = typeof getTorrentTotals === "function" ? getTorrentTotals : null;
|
|
1455
|
+
// The spilled pieces, as a pair of closures over the torrent thread: what
|
|
1456
|
+
// they weigh and how to tell them their share. Not the pool — the pool is
|
|
1457
|
+
// on the other side of the thread boundary and this side holds none of it.
|
|
1458
|
+
this.spillDisk = spillDisk;
|
|
1454
1459
|
// Detected H.264 encoder descriptor (hardware or software). Defaults to
|
|
1455
1460
|
// software libx264 when no detection result is supplied. May be downgraded
|
|
1456
1461
|
// to software at runtime if a hardware encode fails.
|
|
@@ -1597,8 +1602,8 @@ export class HlsSessionManager {
|
|
|
1597
1602
|
// One owner of the disk, and the list of what takes it lives with the owner.
|
|
1598
1603
|
this.diskSpace = wireDiskSpace({
|
|
1599
1604
|
segmentStore: this.segmentStore,
|
|
1600
|
-
|
|
1601
|
-
readFree:
|
|
1605
|
+
spill: this.spillDisk,
|
|
1606
|
+
readFree: freeBytesFor,
|
|
1602
1607
|
logger
|
|
1603
1608
|
});
|
|
1604
1609
|
// Realtime-budget monitor: only meaningful for the software encoder with a
|
package/test/disk-space.test.js
CHANGED
|
@@ -148,3 +148,18 @@ test("it says what it decided, every pass", async () => {
|
|
|
148
148
|
assert.equal(lines.length, 1, "a pass that decided the shares said nothing about them");
|
|
149
149
|
assert.match(lines[0], /disk: 100MB free; segments 4MB of 8MB/);
|
|
150
150
|
});
|
|
151
|
+
|
|
152
|
+
test("the free space is read from the nearest directory that exists", async () => {
|
|
153
|
+
// The segments' directory is made when the first session starts and removed
|
|
154
|
+
// when the proxy stops, so at every start `statfs` on it fails. Field
|
|
155
|
+
// 2026-09-10: the first reading said `disk: 0MB free` on a host with 103 GB,
|
|
156
|
+
// and zero means "no room" to everything that reads it.
|
|
157
|
+
const { freeBytesFor } = await import("../services/disk/free.js");
|
|
158
|
+
const os = await import("node:os");
|
|
159
|
+
const path = await import("node:path");
|
|
160
|
+
const missing = path.join(os.tmpdir(), "no-such-directory-here", "nor-here");
|
|
161
|
+
|
|
162
|
+
const bytes = await freeBytesFor(missing);
|
|
163
|
+
|
|
164
|
+
assert.ok(Number.isFinite(bytes) && bytes > 0, "a directory that does not exist read as no disk");
|
|
165
|
+
});
|