@torrent-tv/proxy 2.80.19 → 2.81.0
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 +12 -0
- package/package.json +1 -1
- package/research/double-spawn-2026-09-10.md +171 -0
- package/services/disk/DiskSpace.js +146 -0
- package/services/disk/wire.js +60 -0
- package/services/encode/EncodeRun.js +25 -3
- package/services/encode/SegmentStore.js +137 -9
- package/services/hls-session-manager.js +26 -45
- package/services/orchestrators/EncodeOrchestrator.js +4 -1
- package/services/piece-store/allowance.js +107 -0
- package/services/piece-store/piece-disk-store.js +365 -0
- package/services/piece-store/shared-piece-store.js +1549 -1535
- package/services/torrent-worker/client.js +32 -0
- package/services/torrent-worker/pool-adapter.js +15 -0
- package/services/torrent-worker/protocol.js +9 -0
- package/services/torrent-worker/worker.js +8 -1
- package/services/viewer/positions.js +48 -0
- package/test/audio-inventory.test.js +176 -176
- package/test/auto-quality-step.test.js +514 -514
- package/test/concurrent-cost.test.js +138 -138
- package/test/coverage-follows-the-disk.test.js +191 -191
- package/test/coverage-map.test.js +195 -195
- package/test/declared-tracks.test.js +35 -35
- package/test/disk-space.test.js +138 -0
- package/test/encode-orchestrator.test.js +0 -3
- package/test/encode-run.test.js +5 -12
- package/test/held-request-width.test.js +155 -155
- package/test/helpers/encode-run.js +2 -2
- package/test/matroska-blocks.test.js +0 -0
- package/test/matroska-cues-track.test.js +192 -192
- package/test/mp4-composition-times.test.js +0 -0
- package/test/mp4-subtitles.test.js +173 -173
- package/test/one-authority.test.js +281 -220
- package/test/orchestrator-wired.test.js +199 -199
- package/test/packet-witness-ring.test.js +236 -236
- package/test/packet-witness.test.js +148 -148
- package/test/piece-disk-store.test.js +267 -0
- package/test/piece-reader.test.js +4 -4
- package/test/piece-store-eviction.test.js +17 -17
- package/test/piece-store-reservations.test.js +20 -1
- package/test/piece-store-slow-disk.test.js +16 -1
- package/test/read-window.test.js +6 -6
- package/test/run-intervals.test.js +100 -100
- package/test/seek-landing.test.js +109 -109
- package/test/segment-store-eviction.test.js +232 -0
- package/test/segments-are-shared.test.js +1 -1
- package/test/shared-piece-store.test.js +12 -12
- package/test/sidecar-naming.test.js +142 -142
- package/test/subtitle-cue-framing.test.js +200 -200
- package/test/subtitle-cue-walk.test.js +369 -369
- package/test/subtitle-defaults.test.js +97 -97
- package/test/subtitle-track-numbering.test.js +370 -370
- package/test/tail-duplication.test.js +167 -167
- package/test/tracks-begin-together.test.js +195 -195
- package/test/two-viewers-one-picture.test.js +374 -374
- package/test/video-facts.test.js +102 -102
- package/test/wedge-certainty.test.js +131 -131
- package/services/piece-store/disk-tier.js +0 -151
|
@@ -1,109 +1,109 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file Ask ffmpeg late enough that it lands where we meant.
|
|
3
|
-
*
|
|
4
|
-
* `fftools/ffmpeg_demux.c` moves an input seek back by `3*AV_TIME_BASE / 23` —
|
|
5
|
-
* 130.435 ms — whenever the container does not declare `AVFMT_SEEK_TO_PTS` and
|
|
6
|
-
* a stream carries B-frames. So asking for a keyframe lands on the one before
|
|
7
|
-
* it, and since `-segment_times` is measured from where the run really began,
|
|
8
|
-
* every cut of that run inherits the shift.
|
|
9
|
-
*
|
|
10
|
-
* Measured 2026-08-21 on Matroska with keyframes every 2 s: `-ss 10` produced a
|
|
11
|
-
* first segment starting at 8.000, `-ss 10.130435` one starting at 10.000. On
|
|
12
|
-
* MP4, where the heuristic does not fire, 10, 10.130435 and 10.2 all produced
|
|
13
|
-
* 10.000 — right in one case, harmless in the other.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import assert from "node:assert/strict";
|
|
17
|
-
import { SourceFile } from "../services/source/SourceFile.js";
|
|
18
|
-
import test from "node:test";
|
|
19
|
-
|
|
20
|
-
import { seekLandingOffsetFor } from "../services/hls-session-manager.js";
|
|
21
|
-
|
|
22
|
-
const OFFSET = 3 / 23;
|
|
23
|
-
|
|
24
|
-
test("a copied picture is asked for one heuristic later than the keyframe", () => {
|
|
25
|
-
const session = { transcodeVideo: false, file: new SourceFile({ sourceKey: "s", fileIndex: 0 }).learn({ keyframeTimes: [0, 2.002, 4.004, 6.006] }) };
|
|
26
|
-
assert.equal(seekLandingOffsetFor(session, 2.002), OFFSET);
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
test("a re-encode is asked for exactly what it should produce", () => {
|
|
30
|
-
// It decodes from the keyframe and discards frames up to the requested time,
|
|
31
|
-
// so pushing the request later would start its output late.
|
|
32
|
-
const session = { transcodeVideo: true, file: new SourceFile({ sourceKey: "s", fileIndex: 0 }).learn({ keyframeTimes: [0, 2.002, 4.004] }) };
|
|
33
|
-
assert.equal(seekLandingOffsetFor(session, 2.002), 0);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
test("the offset never reaches the next keyframe", () => {
|
|
37
|
-
// Keyframes 0.1 s apart: half of that is the most that can be added without
|
|
38
|
-
// risking a landing on the NEXT one where the heuristic does not fire.
|
|
39
|
-
const session = { transcodeVideo: false, file: new SourceFile({ sourceKey: "s", fileIndex: 0 }).learn({ keyframeTimes: [0, 0.1, 0.2, 0.3] }) };
|
|
40
|
-
assert.equal(seekLandingOffsetFor(session, 0.1), 0.05);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
test("the last keyframe has nothing after it to collide with", () => {
|
|
44
|
-
const session = { transcodeVideo: false, file: new SourceFile({ sourceKey: "s", fileIndex: 0 }).learn({ keyframeTimes: [0, 2.002, 4.004] }) };
|
|
45
|
-
assert.equal(seekLandingOffsetFor(session, 4.004), OFFSET);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
test("no keyframe list is still answered", () => {
|
|
49
|
-
assert.equal(seekLandingOffsetFor({ transcodeVideo: false }, 5), OFFSET);
|
|
50
|
-
assert.equal(seekLandingOffsetFor(null, 5), OFFSET);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
test("a grid whose times are approximate is asked for that much later again", () => {
|
|
54
|
-
// AVI names a keyframe by its frame NUMBER and the time is that number times
|
|
55
|
-
// the frame duration, so a name can sit just BELOW the keyframe it refers to
|
|
56
|
-
// — measured 2026-08-21, 10-44 ms out on two files, always under one frame.
|
|
57
|
-
// Asking at the name alone would seek to before the real keyframe and land on
|
|
58
|
-
// the one before that, which is the fault this offset exists for.
|
|
59
|
-
const session = {
|
|
60
|
-
transcodeVideo: false,
|
|
61
|
-
file: new SourceFile({ sourceKey: "s", fileIndex: 0 }).learn({ keyframeTimes: [0, 4.004, 8.008, 12.012], keyframeTolerance: 0.04 })
|
|
62
|
-
};
|
|
63
|
-
assert.equal(seekLandingOffsetFor(session, 4.004), OFFSET + 0.04);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
test("an exact grid claims no tolerance", () => {
|
|
67
|
-
// Matroska and MP4 state instants outright — measured the same day, nine
|
|
68
|
-
// files and 11 665 keyframes with not one disagreement.
|
|
69
|
-
const session = { transcodeVideo: false, file: new SourceFile({ sourceKey: "s", fileIndex: 0 }).learn({ keyframeTimes: [0, 4.004, 8.008], keyframeTolerance: 0 }) };
|
|
70
|
-
assert.equal(seekLandingOffsetFor(session, 4.004), OFFSET);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
test("the bound still holds once a tolerance is added", () => {
|
|
74
|
-
const session = { transcodeVideo: false, file: new SourceFile({ sourceKey: "s", fileIndex: 0 }).learn({ keyframeTimes: [0, 0.1, 0.2], keyframeTolerance: 1 }) };
|
|
75
|
-
assert.equal(seekLandingOffsetFor(session, 0.1), 0.05);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
test("an output carrying only sound is not pushed past what it asked for", () => {
|
|
79
|
-
// The field failure of 2026-09-06: the sound played 130 ms ahead of the
|
|
80
|
-
// picture from every restart onward, and the viewer saw lips out of step with
|
|
81
|
-
// the voice from two minutes in.
|
|
82
|
-
//
|
|
83
|
-
// The offset exists because ffmpeg's demuxer moves a seek target back for a
|
|
84
|
-
// container it reads in decode order, after which a COPY lands on the previous
|
|
85
|
-
// keyframe. A re-encode trims to the requested time itself and is excluded —
|
|
86
|
-
// and an output with no picture is exactly that, since its one track is
|
|
87
|
-
// `-c:a aac`. It was not excluded, because the test asked whether the PICTURE
|
|
88
|
-
// is re-encoded and an output with no picture answers no.
|
|
89
|
-
const soundtrack = {
|
|
90
|
-
audioOnly: true,
|
|
91
|
-
transcodeVideo: false,
|
|
92
|
-
file: new SourceFile({ sourceKey: "s", fileIndex: 0 })
|
|
93
|
-
.learn({ keyframeTimes: [0, 4.004, 8.008, 12.012], keyframeTolerance: 0 })
|
|
94
|
-
};
|
|
95
|
-
assert.equal(seekLandingOffsetFor(soundtrack, 4.004), 0);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
test("the picture of the same film still gets the offset", () => {
|
|
99
|
-
// The pair to the check above: the two outputs are repositioned to one
|
|
100
|
-
// boundary and must be given DIFFERENT requests, because one is copied and one
|
|
101
|
-
// is re-encoded. Given the same request they land 130 ms apart.
|
|
102
|
-
const picture = {
|
|
103
|
-
audioOnly: false,
|
|
104
|
-
transcodeVideo: false,
|
|
105
|
-
file: new SourceFile({ sourceKey: "s", fileIndex: 0 })
|
|
106
|
-
.learn({ keyframeTimes: [0, 4.004, 8.008, 12.012], keyframeTolerance: 0 })
|
|
107
|
-
};
|
|
108
|
-
assert.equal(seekLandingOffsetFor(picture, 4.004), OFFSET);
|
|
109
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* @file Ask ffmpeg late enough that it lands where we meant.
|
|
3
|
+
*
|
|
4
|
+
* `fftools/ffmpeg_demux.c` moves an input seek back by `3*AV_TIME_BASE / 23` —
|
|
5
|
+
* 130.435 ms — whenever the container does not declare `AVFMT_SEEK_TO_PTS` and
|
|
6
|
+
* a stream carries B-frames. So asking for a keyframe lands on the one before
|
|
7
|
+
* it, and since `-segment_times` is measured from where the run really began,
|
|
8
|
+
* every cut of that run inherits the shift.
|
|
9
|
+
*
|
|
10
|
+
* Measured 2026-08-21 on Matroska with keyframes every 2 s: `-ss 10` produced a
|
|
11
|
+
* first segment starting at 8.000, `-ss 10.130435` one starting at 10.000. On
|
|
12
|
+
* MP4, where the heuristic does not fire, 10, 10.130435 and 10.2 all produced
|
|
13
|
+
* 10.000 — right in one case, harmless in the other.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import assert from "node:assert/strict";
|
|
17
|
+
import { SourceFile } from "../services/source/SourceFile.js";
|
|
18
|
+
import test from "node:test";
|
|
19
|
+
|
|
20
|
+
import { seekLandingOffsetFor } from "../services/hls-session-manager.js";
|
|
21
|
+
|
|
22
|
+
const OFFSET = 3 / 23;
|
|
23
|
+
|
|
24
|
+
test("a copied picture is asked for one heuristic later than the keyframe", () => {
|
|
25
|
+
const session = { transcodeVideo: false, file: new SourceFile({ sourceKey: "s", fileIndex: 0 }).learn({ keyframeTimes: [0, 2.002, 4.004, 6.006] }) };
|
|
26
|
+
assert.equal(seekLandingOffsetFor(session, 2.002), OFFSET);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("a re-encode is asked for exactly what it should produce", () => {
|
|
30
|
+
// It decodes from the keyframe and discards frames up to the requested time,
|
|
31
|
+
// so pushing the request later would start its output late.
|
|
32
|
+
const session = { transcodeVideo: true, file: new SourceFile({ sourceKey: "s", fileIndex: 0 }).learn({ keyframeTimes: [0, 2.002, 4.004] }) };
|
|
33
|
+
assert.equal(seekLandingOffsetFor(session, 2.002), 0);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("the offset never reaches the next keyframe", () => {
|
|
37
|
+
// Keyframes 0.1 s apart: half of that is the most that can be added without
|
|
38
|
+
// risking a landing on the NEXT one where the heuristic does not fire.
|
|
39
|
+
const session = { transcodeVideo: false, file: new SourceFile({ sourceKey: "s", fileIndex: 0 }).learn({ keyframeTimes: [0, 0.1, 0.2, 0.3] }) };
|
|
40
|
+
assert.equal(seekLandingOffsetFor(session, 0.1), 0.05);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("the last keyframe has nothing after it to collide with", () => {
|
|
44
|
+
const session = { transcodeVideo: false, file: new SourceFile({ sourceKey: "s", fileIndex: 0 }).learn({ keyframeTimes: [0, 2.002, 4.004] }) };
|
|
45
|
+
assert.equal(seekLandingOffsetFor(session, 4.004), OFFSET);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("no keyframe list is still answered", () => {
|
|
49
|
+
assert.equal(seekLandingOffsetFor({ transcodeVideo: false }, 5), OFFSET);
|
|
50
|
+
assert.equal(seekLandingOffsetFor(null, 5), OFFSET);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("a grid whose times are approximate is asked for that much later again", () => {
|
|
54
|
+
// AVI names a keyframe by its frame NUMBER and the time is that number times
|
|
55
|
+
// the frame duration, so a name can sit just BELOW the keyframe it refers to
|
|
56
|
+
// — measured 2026-08-21, 10-44 ms out on two files, always under one frame.
|
|
57
|
+
// Asking at the name alone would seek to before the real keyframe and land on
|
|
58
|
+
// the one before that, which is the fault this offset exists for.
|
|
59
|
+
const session = {
|
|
60
|
+
transcodeVideo: false,
|
|
61
|
+
file: new SourceFile({ sourceKey: "s", fileIndex: 0 }).learn({ keyframeTimes: [0, 4.004, 8.008, 12.012], keyframeTolerance: 0.04 })
|
|
62
|
+
};
|
|
63
|
+
assert.equal(seekLandingOffsetFor(session, 4.004), OFFSET + 0.04);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("an exact grid claims no tolerance", () => {
|
|
67
|
+
// Matroska and MP4 state instants outright — measured the same day, nine
|
|
68
|
+
// files and 11 665 keyframes with not one disagreement.
|
|
69
|
+
const session = { transcodeVideo: false, file: new SourceFile({ sourceKey: "s", fileIndex: 0 }).learn({ keyframeTimes: [0, 4.004, 8.008], keyframeTolerance: 0 }) };
|
|
70
|
+
assert.equal(seekLandingOffsetFor(session, 4.004), OFFSET);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("the bound still holds once a tolerance is added", () => {
|
|
74
|
+
const session = { transcodeVideo: false, file: new SourceFile({ sourceKey: "s", fileIndex: 0 }).learn({ keyframeTimes: [0, 0.1, 0.2], keyframeTolerance: 1 }) };
|
|
75
|
+
assert.equal(seekLandingOffsetFor(session, 0.1), 0.05);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("an output carrying only sound is not pushed past what it asked for", () => {
|
|
79
|
+
// The field failure of 2026-09-06: the sound played 130 ms ahead of the
|
|
80
|
+
// picture from every restart onward, and the viewer saw lips out of step with
|
|
81
|
+
// the voice from two minutes in.
|
|
82
|
+
//
|
|
83
|
+
// The offset exists because ffmpeg's demuxer moves a seek target back for a
|
|
84
|
+
// container it reads in decode order, after which a COPY lands on the previous
|
|
85
|
+
// keyframe. A re-encode trims to the requested time itself and is excluded —
|
|
86
|
+
// and an output with no picture is exactly that, since its one track is
|
|
87
|
+
// `-c:a aac`. It was not excluded, because the test asked whether the PICTURE
|
|
88
|
+
// is re-encoded and an output with no picture answers no.
|
|
89
|
+
const soundtrack = {
|
|
90
|
+
audioOnly: true,
|
|
91
|
+
transcodeVideo: false,
|
|
92
|
+
file: new SourceFile({ sourceKey: "s", fileIndex: 0 })
|
|
93
|
+
.learn({ keyframeTimes: [0, 4.004, 8.008, 12.012], keyframeTolerance: 0 })
|
|
94
|
+
};
|
|
95
|
+
assert.equal(seekLandingOffsetFor(soundtrack, 4.004), 0);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("the picture of the same film still gets the offset", () => {
|
|
99
|
+
// The pair to the check above: the two outputs are repositioned to one
|
|
100
|
+
// boundary and must be given DIFFERENT requests, because one is copied and one
|
|
101
|
+
// is re-encoded. Given the same request they land 130 ms apart.
|
|
102
|
+
const picture = {
|
|
103
|
+
audioOnly: false,
|
|
104
|
+
transcodeVideo: false,
|
|
105
|
+
file: new SourceFile({ sourceKey: "s", fileIndex: 0 })
|
|
106
|
+
.learn({ keyframeTimes: [0, 4.004, 8.008, 12.012], keyframeTolerance: 0 })
|
|
107
|
+
};
|
|
108
|
+
assert.equal(seekLandingOffsetFor(picture, 4.004), OFFSET);
|
|
109
|
+
});
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file What goes first when the disk is short: the viewers decide.
|
|
3
|
+
*
|
|
4
|
+
* The store used to throw away whole outputs by when their directory was last
|
|
5
|
+
* read, which says nothing about what anybody is about to watch. The order here
|
|
6
|
+
* is the priority map's own, read from the other end: nobody's output first,
|
|
7
|
+
* then what is behind the earliest viewer, then what is ahead of the furthest.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import test from "node:test";
|
|
11
|
+
import assert from "node:assert/strict";
|
|
12
|
+
import fs from "node:fs/promises";
|
|
13
|
+
import os from "node:os";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import { SegmentStore } from "../services/encode/SegmentStore.js";
|
|
16
|
+
|
|
17
|
+
const SEGMENT = 1024;
|
|
18
|
+
|
|
19
|
+
const format = {
|
|
20
|
+
isSegmentFileName: (name) => /^segment-\d{5}\.mp4$/.test(name),
|
|
21
|
+
segmentIndexFromName: (name) => {
|
|
22
|
+
const match = /^segment-(\d{5})\.mp4$/.exec(name);
|
|
23
|
+
return match ? Number(match[1]) : -1;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {number} [now]
|
|
29
|
+
*/
|
|
30
|
+
async function makeStore(now = 1000) {
|
|
31
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), "segment-store-test-"));
|
|
32
|
+
const clock = { at: now };
|
|
33
|
+
const store = new SegmentStore({ root, now: () => clock.at });
|
|
34
|
+
return { store, root, clock };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @param {SegmentStore} store
|
|
39
|
+
* @param {string} key
|
|
40
|
+
* @param {number[]} indexes
|
|
41
|
+
*/
|
|
42
|
+
async function fill(store, key, indexes) {
|
|
43
|
+
store.useFormat(key, format);
|
|
44
|
+
const dir = store.directoryFor(key);
|
|
45
|
+
for (const index of indexes) {
|
|
46
|
+
await fs.writeFile(
|
|
47
|
+
path.join(dir, `segment-${String(index).padStart(5, "0")}.mp4`),
|
|
48
|
+
Buffer.alloc(SEGMENT, index % 251)
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @param {SegmentStore} store
|
|
55
|
+
* @param {string} key
|
|
56
|
+
*/
|
|
57
|
+
async function heldNumbers(store, key) {
|
|
58
|
+
const names = await fs.readdir(store.pathFor(key)).catch(() => []);
|
|
59
|
+
return names
|
|
60
|
+
.filter((name) => format.isSegmentFileName(name))
|
|
61
|
+
.map((name) => format.segmentIndexFromName(name))
|
|
62
|
+
.sort((left, right) => left - right);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
test("what nobody is watching goes before anything anybody is", async () => {
|
|
66
|
+
const { store, root } = await makeStore();
|
|
67
|
+
try {
|
|
68
|
+
await fill(store, "watched", [0, 1, 2, 3]);
|
|
69
|
+
await fill(store, "abandoned", [0, 1, 2, 3]);
|
|
70
|
+
|
|
71
|
+
// Room for five segments of the eight held.
|
|
72
|
+
store.enforce({
|
|
73
|
+
idleMs: Number.POSITIVE_INFINITY,
|
|
74
|
+
maxBytes: 5 * SEGMENT,
|
|
75
|
+
viewersAt: (key) => (key === "watched" ? [2] : [])
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Three of eight had to go, and all three came from the output nobody is on.
|
|
79
|
+
assert.deepEqual(
|
|
80
|
+
await heldNumbers(store, "watched"),
|
|
81
|
+
[0, 1, 2, 3],
|
|
82
|
+
"the watched output lost segments while an unwatched one still held some"
|
|
83
|
+
);
|
|
84
|
+
assert.equal((await heldNumbers(store, "abandoned")).length, 1, "the unwatched output kept too much");
|
|
85
|
+
} finally {
|
|
86
|
+
await fs.rm(root, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("behind the viewer goes before ahead of the viewer, furthest behind first", async () => {
|
|
91
|
+
const { store, root } = await makeStore();
|
|
92
|
+
try {
|
|
93
|
+
await fill(store, "one", [0, 1, 2, 3, 4, 5]);
|
|
94
|
+
|
|
95
|
+
// The viewer stands on #3. Room for four of the six.
|
|
96
|
+
store.enforce({
|
|
97
|
+
idleMs: Number.POSITIVE_INFINITY,
|
|
98
|
+
maxBytes: 4 * SEGMENT,
|
|
99
|
+
viewersAt: () => [3]
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
assert.deepEqual(
|
|
103
|
+
await heldNumbers(store, "one"),
|
|
104
|
+
[2, 3, 4, 5],
|
|
105
|
+
"the two furthest behind the viewer should have gone, and nothing ahead"
|
|
106
|
+
);
|
|
107
|
+
} finally {
|
|
108
|
+
await fs.rm(root, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("with nothing left behind, the furthest ahead goes next", async () => {
|
|
113
|
+
const { store, root } = await makeStore();
|
|
114
|
+
try {
|
|
115
|
+
await fill(store, "one", [4, 5, 6, 7, 8]);
|
|
116
|
+
|
|
117
|
+
// The viewer is on #4, so nothing is behind. Room for three of the five.
|
|
118
|
+
store.enforce({
|
|
119
|
+
idleMs: Number.POSITIVE_INFINITY,
|
|
120
|
+
maxBytes: 3 * SEGMENT,
|
|
121
|
+
viewersAt: () => [4]
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
assert.deepEqual(
|
|
125
|
+
await heldNumbers(store, "one"),
|
|
126
|
+
[4, 5, 6],
|
|
127
|
+
"the two furthest ahead should have gone, nearest kept"
|
|
128
|
+
);
|
|
129
|
+
} finally {
|
|
130
|
+
await fs.rm(root, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("the segment a viewer is standing on is never taken", async () => {
|
|
135
|
+
const { store, root } = await makeStore();
|
|
136
|
+
try {
|
|
137
|
+
await fill(store, "one", [0, 1, 2]);
|
|
138
|
+
|
|
139
|
+
// Two viewers, one on each end, and room for one segment only.
|
|
140
|
+
store.enforce({
|
|
141
|
+
idleMs: Number.POSITIVE_INFINITY,
|
|
142
|
+
maxBytes: SEGMENT,
|
|
143
|
+
viewersAt: () => [0, 2]
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const left = await heldNumbers(store, "one");
|
|
147
|
+
assert.ok(left.includes(0), "the segment the first viewer is on was taken");
|
|
148
|
+
assert.ok(left.includes(2), "the segment the second viewer is on was taken");
|
|
149
|
+
assert.deepEqual(left, [0, 2], "the one between two viewers should have gone");
|
|
150
|
+
} finally {
|
|
151
|
+
await fs.rm(root, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("two viewers of one output: behind the EARLIEST, ahead of the FURTHEST", async () => {
|
|
156
|
+
const { store, root } = await makeStore();
|
|
157
|
+
try {
|
|
158
|
+
await fill(store, "one", [0, 1, 2, 3, 4, 5, 6]);
|
|
159
|
+
|
|
160
|
+
// Viewers on #2 and #5. Behind means below #2; ahead means above #5.
|
|
161
|
+
store.enforce({
|
|
162
|
+
idleMs: Number.POSITIVE_INFINITY,
|
|
163
|
+
maxBytes: 5 * SEGMENT,
|
|
164
|
+
viewersAt: () => [2, 5]
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
assert.deepEqual(
|
|
168
|
+
await heldNumbers(store, "one"),
|
|
169
|
+
[2, 3, 4, 5, 6],
|
|
170
|
+
"what lies behind the earliest viewer must go before anything ahead of the furthest"
|
|
171
|
+
);
|
|
172
|
+
} finally {
|
|
173
|
+
await fs.rm(root, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("an output nobody has read for long enough goes whole, however much room there is", async () => {
|
|
178
|
+
const { store, root, clock } = await makeStore();
|
|
179
|
+
try {
|
|
180
|
+
await fill(store, "stale", [0, 1, 2]);
|
|
181
|
+
clock.at += 60 * 60 * 1000;
|
|
182
|
+
await fill(store, "fresh", [0, 1, 2]);
|
|
183
|
+
|
|
184
|
+
const result = store.enforce({
|
|
185
|
+
idleMs: 30 * 60 * 1000,
|
|
186
|
+
// Room for everything: this is the rule that does not wait for pressure.
|
|
187
|
+
maxBytes: Number.MAX_SAFE_INTEGER,
|
|
188
|
+
viewersAt: () => []
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
assert.equal(result.droppedIdle, 1);
|
|
192
|
+
assert.deepEqual(await heldNumbers(store, "stale"), [], "the stale output was kept");
|
|
193
|
+
assert.deepEqual(await heldNumbers(store, "fresh"), [0, 1, 2], "the fresh output was taken");
|
|
194
|
+
} finally {
|
|
195
|
+
await fs.rm(root, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("told nothing about viewers, it falls back to the oldest directory", async () => {
|
|
200
|
+
const { store, root, clock } = await makeStore();
|
|
201
|
+
try {
|
|
202
|
+
await fill(store, "older", [0, 1, 2]);
|
|
203
|
+
clock.at += 1000;
|
|
204
|
+
await fill(store, "newer", [0, 1, 2]);
|
|
205
|
+
|
|
206
|
+
const result = store.enforce({ idleMs: Number.POSITIVE_INFINITY, maxBytes: 4 * SEGMENT });
|
|
207
|
+
|
|
208
|
+
assert.equal(result.droppedForRoom, 1);
|
|
209
|
+
assert.deepEqual(await heldNumbers(store, "older"), []);
|
|
210
|
+
assert.deepEqual(await heldNumbers(store, "newer"), [0, 1, 2]);
|
|
211
|
+
} finally {
|
|
212
|
+
await fs.rm(root, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("a clean exit leaves nothing of ours, so what is found next time is from a kill", async () => {
|
|
217
|
+
const { store, root } = await makeStore();
|
|
218
|
+
try {
|
|
219
|
+
await fill(store, "one", [0, 1, 2]);
|
|
220
|
+
await fill(store, "two", [0, 1]);
|
|
221
|
+
// A directory this process adopted at startup and no session owns: the very
|
|
222
|
+
// thing the old rule left behind, which is how an orphan became permanent.
|
|
223
|
+
await fs.mkdir(path.join(root, "abandoned"), { recursive: true });
|
|
224
|
+
await fs.writeFile(path.join(root, "abandoned", "segment-00000.mp4"), Buffer.alloc(SEGMENT));
|
|
225
|
+
|
|
226
|
+
assert.equal(store.dropAll("the proxy is shutting down"), 2);
|
|
227
|
+
|
|
228
|
+
await assert.rejects(() => fs.stat(root), /ENOENT/, "the store left its root behind");
|
|
229
|
+
} finally {
|
|
230
|
+
await fs.rm(root, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
231
|
+
}
|
|
232
|
+
});
|
|
@@ -140,7 +140,7 @@ test("what nobody has read for long enough goes, and time is the only judge", (t
|
|
|
140
140
|
clock += 60_000;
|
|
141
141
|
assert.deepEqual(
|
|
142
142
|
store.enforce({ idleMs: 3_600_000, maxBytes: 1e9 }),
|
|
143
|
-
{ droppedIdle: 0, droppedForRoom: 0, bytes: 32 }
|
|
143
|
+
{ droppedIdle: 0, droppedForRoom: 0, segmentsRemoved: 0, bytes: 32 }
|
|
144
144
|
);
|
|
145
145
|
|
|
146
146
|
clock += 7_200_000;
|
|
@@ -59,7 +59,7 @@ test("a stored piece comes back byte for byte", async () => {
|
|
|
59
59
|
assert.deepEqual(read, piece(0));
|
|
60
60
|
} finally {
|
|
61
61
|
await new Promise((resolve) => store.destroy(resolve));
|
|
62
|
-
await fs.rm(directory, { recursive: true, force: true });
|
|
62
|
+
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
63
63
|
}
|
|
64
64
|
});
|
|
65
65
|
|
|
@@ -79,7 +79,7 @@ test("pieces past the memory budget spill to disk and read back intact", async (
|
|
|
79
79
|
}
|
|
80
80
|
} finally {
|
|
81
81
|
await new Promise((resolve) => store.destroy(resolve));
|
|
82
|
-
await fs.rm(directory, { recursive: true, force: true });
|
|
82
|
+
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
83
83
|
}
|
|
84
84
|
});
|
|
85
85
|
|
|
@@ -99,7 +99,7 @@ test("the short last piece keeps its own length", async () => {
|
|
|
99
99
|
assert.deepEqual(read, tail);
|
|
100
100
|
} finally {
|
|
101
101
|
await new Promise((resolve) => store.destroy(resolve));
|
|
102
|
-
await fs.rm(directory, { recursive: true, force: true });
|
|
102
|
+
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
103
103
|
}
|
|
104
104
|
});
|
|
105
105
|
|
|
@@ -118,7 +118,7 @@ test("a buffer handed out survives later writes to the same slot", async () => {
|
|
|
118
118
|
assert.deepEqual(held, piece(0), "the buffer changed under its holder");
|
|
119
119
|
} finally {
|
|
120
120
|
await new Promise((resolve) => store.destroy(resolve));
|
|
121
|
-
await fs.rm(directory, { recursive: true, force: true });
|
|
121
|
+
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
122
122
|
}
|
|
123
123
|
});
|
|
124
124
|
|
|
@@ -139,7 +139,7 @@ test("a pinned piece is not evicted to make room", async () => {
|
|
|
139
139
|
store.unpin(0);
|
|
140
140
|
} finally {
|
|
141
141
|
await new Promise((resolve) => store.destroy(resolve));
|
|
142
|
-
await fs.rm(directory, { recursive: true, force: true });
|
|
142
|
+
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
143
143
|
}
|
|
144
144
|
});
|
|
145
145
|
|
|
@@ -160,7 +160,7 @@ test("refuses to make room when every resident piece is being read", async () =>
|
|
|
160
160
|
store.unpin(0);
|
|
161
161
|
store.unpin(1);
|
|
162
162
|
await new Promise((resolve) => store.destroy(resolve));
|
|
163
|
-
await fs.rm(directory, { recursive: true, force: true });
|
|
163
|
+
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
164
164
|
}
|
|
165
165
|
});
|
|
166
166
|
|
|
@@ -179,7 +179,7 @@ test("a piece revived from disk is readable by offset again", async () => {
|
|
|
179
179
|
assert.deepEqual(view, piece(0));
|
|
180
180
|
} finally {
|
|
181
181
|
await new Promise((resolve) => store.destroy(resolve));
|
|
182
|
-
await fs.rm(directory, { recursive: true, force: true });
|
|
182
|
+
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
183
183
|
}
|
|
184
184
|
});
|
|
185
185
|
|
|
@@ -205,7 +205,7 @@ test("a range within a piece is served correctly from memory and from disk", asy
|
|
|
205
205
|
);
|
|
206
206
|
} finally {
|
|
207
207
|
await new Promise((resolve) => store.destroy(resolve));
|
|
208
|
-
await fs.rm(directory, { recursive: true, force: true });
|
|
208
|
+
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
209
209
|
}
|
|
210
210
|
});
|
|
211
211
|
|
|
@@ -230,7 +230,7 @@ test("takes memory as it needs it, not the whole budget up front", async () => {
|
|
|
230
230
|
assert.deepEqual(await get(store, 0), piece(0), "an early piece was disturbed");
|
|
231
231
|
} finally {
|
|
232
232
|
await new Promise((resolve) => store.destroy(resolve));
|
|
233
|
-
await fs.rm(directory, { recursive: true, force: true });
|
|
233
|
+
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
234
234
|
}
|
|
235
235
|
});
|
|
236
236
|
|
|
@@ -257,7 +257,7 @@ test("counts where reads were served from, so the budget can be judged", async (
|
|
|
257
257
|
assert.equal(stats.capacity, 2);
|
|
258
258
|
} finally {
|
|
259
259
|
await new Promise((resolve) => store.destroy(resolve));
|
|
260
|
-
await fs.rm(directory, { recursive: true, force: true });
|
|
260
|
+
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
261
261
|
}
|
|
262
262
|
});
|
|
263
263
|
|
|
@@ -275,7 +275,7 @@ test("counts a refusal caused by pinned pieces", async () => {
|
|
|
275
275
|
store.unpin(0);
|
|
276
276
|
store.unpin(1);
|
|
277
277
|
await new Promise((resolve) => store.destroy(resolve));
|
|
278
|
-
await fs.rm(directory, { recursive: true, force: true });
|
|
278
|
+
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
279
279
|
}
|
|
280
280
|
});
|
|
281
281
|
|
|
@@ -292,5 +292,5 @@ test("destroy removes the spill file", async () => {
|
|
|
292
292
|
const after = await fs.readdir(directory);
|
|
293
293
|
assert.equal(after.length, 0, "the spill file outlived the store");
|
|
294
294
|
|
|
295
|
-
await fs.rm(directory, { recursive: true, force: true });
|
|
295
|
+
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
296
296
|
});
|