Releases: bruor/streamcache
Release list
v2.0.1
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
v1.0.2
v1.0.1
v1.0
Fixed a few bugs!
Playback sessions would cause the cache to be lazy filled as the player requested new ranges from the server. Decoupled the reader/writer processes so cache fills are always immediate and full speed.
There was a global download timer baked into the code which caused downloads that took longer than 15 minutes to be evicted instead of committed to the cache. In progress download TTLs are refreshed while actively being downloaded now and the global TTL is now configurable and set to a default of 4 hours.
Emby's back to back calls against the proxy would happen so fast that the initial ffprobe request would cause a cache fill request, but the second would be processed as "no-cache" causing 2 provider connections to be consumed. This was caused because the second request would cause a race condition and try to read the on-disk cache before it had started to be filled. There is now a configurable backoff delay for requests for a URL that have an active worker that hasn't yet committed data to the cache.
A note about RAM. This container uses a lot of memory, in-progress downloads count against container ram usage because of the way that container file writes are viewed by the OS. As soon as file writes are completed the container ram is released. I have some ideas and will attempt to look into this in a future release if needed, but now that it isn't double downloading RAM usage should effectively be halved.
Logic Refactored
Refactored upstrem fetching logic take place in the "tee" process. The proxy now mimics the player user-agnet and hides all proxy headers from the server upstream. It also does range request translation, so if a client does a non-range request for the media, the proxy will make multiple range requests to the provider to mimic a player (but it seeks the entire file as fast as it can). If the provider doesn't support range requests it will fall back to non-range mode. Increased the default value of the range-request detection to 5MiB because 1MiB was not aggressive enough. VLC sends 4MB+ as a starting point.
Bugfix for Tee functionality
range requests were being ignored to to environment variable propagation issues. This has been corrected.
Initial beta
Add tee control
Trigger cache fill when initial request from client starts within configurable byte range.
Also service range requests from clients from in-flight cache instead of proxying to up-stream server.