Skip to content

Commit 57ba784

Browse files
committed
fix(io): yield in StreamReader.Read when Ogg page boundary scan does not advance
The Ogg-sync branch in StreamReader.Read called FindNextPageBoundary and continued unconditionally. When the boundary returned the same offset (no progress — e.g. transcoder started before the source had captured Ogg headers, or buffer wraparound mid-page) the loop hot-spun: ReadAt -> skipped=true -> FindNextPageBoundary -> same offset -> continue, never yielding to the scheduler. With a busy auto-transcoder pool (3 source mounts x 3 mp3 bitrates = 9 transcoders, plus 3 manual = 12) every transcoder spun on the buffer mutex, driving %sy to 73% and the load average above 30 with near-zero useful work — only 2.4% us. Yield to the signal-channel select when FindNextPageBoundary cannot advance; once the source writes more bytes, signal fires and the read resumes. Empirically: tinyice CPU dropped from ~100% to ~2.7% on the same box with the same transcoder count. Also adds contrib/systemd/tinyice.service — opinionated drop-in unit with Nice / CPUWeight / sandbox flags + CAP_NET_BIND_SERVICE so a non-root tinyice user can bind ports 80 / 443.
1 parent 73606b6 commit 57ba784

2 files changed

Lines changed: 72 additions & 5 deletions

File tree

contrib/systemd/tinyice.service

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
[Unit]
2+
Description=TinyIce streaming server
3+
Documentation=https://github.com/DatanoiseTV/tinyice
4+
After=network-online.target
5+
Wants=network-online.target
6+
7+
[Service]
8+
Type=simple
9+
10+
# Run as a dedicated unprivileged user. Create with:
11+
# useradd -r -s /usr/sbin/nologin -d /var/lib/tinyice tinyice
12+
User=tinyice
13+
Group=tinyice
14+
WorkingDirectory=/var/lib/tinyice
15+
16+
# Default config path; override by editing this line or via a drop-in.
17+
ExecStart=/usr/local/bin/tinyice -config /etc/tinyice/tinyice.json
18+
Restart=on-failure
19+
RestartSec=5
20+
21+
# CPU shaping. Tinyice is largely network + light transcoding work,
22+
# so a small Nice penalty + a sub-default CPUWeight keeps a busy
23+
# transcoder pool from starving interactive system tasks. Tune up
24+
# (Nice=0 / CPUWeight=100) if tinyice is the only thing on the box.
25+
Nice=5
26+
CPUWeight=70
27+
IOSchedulingClass=best-effort
28+
IOSchedulingPriority=4
29+
30+
# Hardening — none of these are required for tinyice to run, but
31+
# they shrink the blast radius of an exploit.
32+
NoNewPrivileges=yes
33+
ProtectSystem=strict
34+
ProtectHome=yes
35+
ReadWritePaths=/var/lib/tinyice /etc/tinyice
36+
PrivateTmp=yes
37+
PrivateDevices=yes
38+
ProtectKernelTunables=yes
39+
ProtectKernelModules=yes
40+
ProtectControlGroups=yes
41+
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
42+
LockPersonality=yes
43+
RestrictRealtime=yes
44+
RestrictSUIDSGID=yes
45+
46+
# Allow binding to privileged ports (80 / 443) as the unprivileged
47+
# tinyice user. AmbientCapabilities is the cap actually granted to the
48+
# process; CapabilityBoundingSet hard-limits the upper set so a child
49+
# can't gain anything else even if the binary tried to.
50+
AmbientCapabilities=CAP_NET_BIND_SERVICE
51+
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
52+
53+
SystemCallFilter=@system-service
54+
SystemCallErrorNumber=EPERM
55+
56+
[Install]
57+
WantedBy=multi-user.target

relay/io.go

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -112,12 +112,22 @@ func (r *StreamReader) Read(p []byte) (int, error) {
112112
// Handle Ogg synchronization if enabled. Use the buffer's own
113113
// mutex rather than the stream mutex, since the slice we're
114114
// scanning is protected by the buffer's lock (not the stream's).
115+
//
116+
// If FindNextPageBoundary doesn't move us forward we MUST yield
117+
// — without this guard the loop hot-spins (skipped=true,
118+
// boundary unchanged, continue, repeat) and pegs a CPU per
119+
// transcoder. Waiting for the signal channel is correct: a
120+
// fresh page only becomes findable once the source has written
121+
// more bytes, which is exactly what `signal` tells us.
115122
if skipped && r.oggSync && r.stream != nil {
116-
r.offset = r.stream.Buffer.FindNextPageBoundaryLocked(next)
117-
continue
118-
}
119-
120-
if n > 0 {
123+
aligned := r.stream.Buffer.FindNextPageBoundaryLocked(next)
124+
if aligned > r.offset {
125+
r.offset = aligned
126+
continue
127+
}
128+
// No progress — fall through to the select below so we
129+
// block on the signal channel instead of burning CPU.
130+
} else if n > 0 {
121131
r.offset = next
122132
return n, nil
123133
}

0 commit comments

Comments
 (0)