Repository navigation
V2.0.0 - Modular Refactor
v2.0.0 — Modular Refactor
Complete rewrite from the production monolith (1,081-line streamcache.lua) into a modular architecture across 8 files while preserving all production behavior. Fixes critical bugs, reduces memory usage, and improves upstream detection avoidance.
Architecture
Refactored single-file monolith into 7 modules: sc_config, sc_utils, sc_file, sc_upstream, sc_nocache, sc_tee, sc_follower
Entry point (streamcache.lua) reduced to ~210 lines of routing logic
State flows through a ctx table — no globals
No circular dependencies; clean module dependency graph
All production features preserved: windowed tee, range-hostile fallback, 206→200 normalization, strict commit, writer/follower coordination
Bug Fixes
Incomplete files committed to cache — The copy fallback (cross-filesystem rename) used read("*a") which could OOM on multi-GB files and had no post-copy size verification. Replaced with 1MB chunked copy + size verification after both rename and copy paths.
Inflight lock expiry during slow downloads — The 900s writer lock was never refreshed. Downloads exceeding 15 minutes caused the lock to expire, allowing duplicate writers for the same file. Lock is now refreshed on every progress update.
Memory growth during downloads (OOM) — posix_fadvise(FADV_DONTNEED) was called on dirty pages, which the kernel silently ignores. Added sync_file_range() to flush dirty pages to disk before dropping them. Writer uses a pipelined strategy (start async writeback, drop previous range) every 2MB for zero additional latency.
Memory growth when client disconnects — The background writer ran at wire speed with no pacing after the client disconnected, accumulating dirty pages faster than the kernel could flush. Added ngx.sleep(0) yield when no client is consuming data.
Follower not detecting client disconnect — With lua_check_client_abort off, the follower couldn't detect when Chrome/VLC closed the connection. It would continue reading the entire .part file into page cache. Enabled lua_check_client_abort on with ngx.on_abort() for immediate disconnect detection.
Follower re-caching writer's dropped pages — Follower used io.open/f:read which brought page cache pages back after the writer dropped them. Switched to FFI open()/read() with periodic fadvise(DONTNEED) via drop_read_cache() (no sync_file_range needed for read-only pages).
Follower sending 206 to browsers — Follower always returned 206 Partial Content, even when the client sent no Range header. Chrome rejected this. Now returns 200 OK with full Content-Length when no Range was requested.
ngx.exit(504) after headers sent — When the writer died mid-transfer, the follower tried to change the HTTP status after headers were already sent. Now exits cleanly with ngx.exit(ngx.HTTP_OK).
Detection Avoidance
Real client headers in all paths — The background writer previously sent User-Agent: EmbyStreamCache/1.0. Now captures the actual client headers before starting the timer and forwards them to upstream. All code paths use Upstream.build_client_like_headers().
Browser-like fallback UA — Default User-Agent changed from EmbyStreamCache/1.0 to a generic browser string for cases where no client UA is available.
Additional header forwarding — Now forwards X-Playback-Session-Id, If-Range, If-None-Match, If-Modified-Since to upstream.
Follower HEAD probe eliminated — Writer caches origin response headers (Content-Type, ETag, etc.) in the resolved shared dict. Followers read from cache instead of making an extra HEAD request per viewer. Halves upstream request count for concurrent viewers.
Consistent response header filtering — All response paths use Utils.filter_response_headers() which strips 9 hop-by-hop headers uniformly. Production had inconsistent filtering across different code paths.
Centralized redirect following — Upstream.request() is the single place redirects are followed (up to 5 hops). Eliminates duplicated redirect loops that existed in every streaming function.
Configuration Fixes
CACHE_MAX_BYTES and CACHE_LOW_WATERMARK_BYTES restored to binary values matching production