Caching and Redis Strategy¶
XC_VM uses a dual-layer caching strategy:
- File-based cache (igbinary) — primary layer, used by both streaming and admin paths
- Redis/KeyDB — optional high-performance layer for connection state and advanced operations
The streaming path reads exclusively from file cache (no database queries). The admin path reads from the database with optional short-lived cache.
Cache Interface¶
File: src/Core/Cache/CacheInterface.php
$ttl = 0means cache forever (until manual deletion or flush).$maxAgechecks file modification time for freshness (FileCache only).
FileCache¶
File: src/Core/Cache/FileCache.php
Default cache implementation. Stores igbinary-serialized data as flat files.
$cache = new FileCache(CACHE_TMP_PATH);
$cache->set('my_key', $data, 3600);
$data = $cache->get('my_key', 120); // only if < 2 min old
Static convenience API (backward compatibility):
Characteristics:
- Serialization: igbinary (if available) or PHP serialize fallback.
- Locking:
LOCK_EXon write to prevent corruption. - File location:
{basePath}/{key}(no subdirectories for core keys). - Corruption recovery: detects bad data, auto-deletes corrupted files.
RedisCache¶
File: src/Core/Cache/RedisCache.php
Optional high-performance implementation.
$redis = new RedisCache('127.0.0.1', 6379, $password, 'prefix:');
$redis->set($key, $data, 600); // 10-minute TTL via SETEX
$redis->getConnection(); // raw phpredis for sorted sets, pipelines
- Lazy connection: connects on first operation.
- Native TTL support via Redis
SETEX. - Used primarily for
ConnectionTracker(sorted sets for live connection state).
Redis Connection Management¶
File: src/Infrastructure/Redis/RedisManager.php
Singleton lifecycle:
RedisManager::instance() // get active Redis or null
RedisManager::ensureConnected() // connect if not already
RedisManager::isConnected() // health check
RedisManager::closeInstance() // disconnect
Health check pings Redis every 30 seconds (debounced). Auto-reconnects on failure. Returns null on connection failure (graceful degradation).
Configuration:
| Setting | Source | Default |
|---|---|---|
hostname |
config.ini |
— |
port |
hardcoded | 6379 |
password |
settings.redis_password |
— |
read_timeout |
hardcoded | 2.0s |
tcp_keepalive |
hardcoded | 60s |
Surviving idle disconnects (long-lived daemons)¶
Short-lived requests (PHP-FPM stream/admin) open a fresh connection per process
and are unaffected by idle timeouts. Long-lived daemons — the watchdog loop,
fanout_sync — instead hold one connection through the singleton for their
whole lifetime, which exposes two failure modes on a busy or cross-server
(LB → MAIN) link:
- Server idle-close. Redis closes any client idle past its
timeout(300sin the bundledbin/redis/redis.conf). phpredis then transparently re-opens the socket on the next command without replaying AUTH, so a later command answersNOAUTH— or simply returnsfalse. - Debounced health-check gap.
instance()only pings every 30s, so between pings a dropped connection is not yet noticed.
Guards in place:
instance()treats any non-PONGping reply (the silent-reconnect /NOAUTHstate) as a dead connection and forces a full, re-authenticated reconnect via\XC_VM::redis_connect()— not just a socket-level retry.- Call sites that pipeline commands validate the pipeline object. For example
ConnectionTracker::getCapacity()checks that$redis->multi()returned a\Redis(a broken socket returnsfalse, and callingzCard()on that bool would fatal outside the reconnect path) and throws so its retry loop reconnects.
The server-side alternative (timeout 0) is deliberately not used — the
client is made resilient instead, and tcp-keepalive still reaps dead peers.
Cache Population¶
Cache files are generated by two cron jobs:
Lightweight cache (CacheCronJob)¶
Runs every cron cycle. Rebuilds fast-changing data (~1 second):
settings— panel settingsservers— server listbouquets— channel packagescategories— stream categories- Blocklists:
blocked_isp,blocked_ua,blocked_ips,blocked_servers allowed_ips,output_formats,hmac_keys,rtmp_ips
Heavy cache (CacheEngineCronJob)¶
Rebuilds stream, line, and series data. Throttled to once per 5 minutes via heavy_cache_built marker:
STREAMS_TMP_PATH/stream_{id}— individual stream metadataLINES_TMP_PATH/line_i_{user_id}— user account dataLINES_TMP_PATH/line_c_{username_password}— username → user_id lookupLINES_TMP_PATH/line_t_{access_token}— token → user_id lookupSERIES_TMP_PATH/series_{id}— series metadata
Change detection mode (if cache_changes enabled): compares DB updated timestamp vs file mtime, rebuilds only changed items.
Full rebuild mode: regenerates all entries. Controlled by cache_thread_count setting.
Cache readiness¶
A cache_complete file is written after each full cache build. The streaming path checks for this file and exits with an error if missing.
Cold-cache safety¶
The streaming bootstrap (LegacyInitializer::initStreaming()) reads servers,
the blocklists and proxy_servers from the file cache. Before the first build
(fresh boot, cleared tmp) those files do not exist and CacheReader::get()
returns null, so every such global is defaulted to an empty array. A cold cache
therefore fails closed — a request finds no servers and shows "not on air" —
instead of a foreach(null) warning or an in_array($x, null) fatal (PHP 8)
downstream. A genuinely broken cache build still surfaces separately via
FileCache's write-failure warning, so this default masks only the transient
cold-start window, not a real failure.
Cache Key Conventions¶
System keys (CACHE_TMP_PATH)¶
| Key | Contents |
|---|---|
settings |
panel settings array |
servers |
array[server_id] → server config |
bouquets |
array[bouquet_id] → bouquet definition |
categories |
array[category_id] → category data |
bouquet_map |
array[stream_id] → array[bouquet_id] |
category_map |
array[bouquet_id] → array[category_id] |
permissions_{group_id} |
group permission set |
cache_complete |
time() timestamp of last full build |
Stream keys (STREAMS_TMP_PATH)¶
| Key | Contents |
|---|---|
stream_{id} |
stream info + bouquets + per-server state |
channels_categories |
array[stream_id] → array[category_id] |
Line keys (LINES_TMP_PATH)¶
| Key | Contents |
|---|---|
line_i_{user_id} |
full user record |
line_c_{username_password} |
user_id (credential lookup) |
line_t_{access_token} |
user_id (token lookup) |
Series keys (SERIES_TMP_PATH)¶
| Key | Contents |
|---|---|
series_{id} |
series metadata |
series_map |
array[stream_id] → series_id |
episodes_{series_id} |
array[season_num] → episode list |
Invalidation Patterns¶
| Trigger | Affected keys | Mechanism |
|---|---|---|
| Admin edits stream | stream_{id}, bouquet_map |
signal → next cron:cache_engine |
| Admin edits line | line_i_*, line_c_*, line_t_* |
next cron:cache_engine |
| Settings changed | settings, categories, blocklists |
SettingsManager::clearCache() + cron |
| Server list updated | servers, bouquet_map |
cron |
| Stream start (FFprobe) | {md5(source)} |
5-minute TTL via file mtime check |
| Admin flush button | all files in CACHE_TMP_PATH |
rm -rf |
Streaming vs Admin Path¶
Streaming path (www/stream/*)¶
cached: trueby default.- Reads from file cache exclusively (no DB queries).
- Raw igbinary deserialization:
igbinary_unserialize(file_get_contents(...)). - If
cache_completemissing: exit with error.
Admin path (Public/Controllers/Admin/*)¶
cached: falseby default.- Reads from database directly via domain services.
- Optional short-lived cache (example from
BouquetService::getAll()):
$rCache = FileCache::getCache('bouquets', 60); // only if < 60s old
if (!empty($rCache)) {
return $rCache;
}
// miss: query database and write cache
FileCache::setCache('bouquets', $rOutput);
Cache File Layout¶
/home/xc_vm/tmp/cache/
├── settings
├── servers
├── bouquets
├── categories
├── bouquet_map
├── category_map
├── cache_complete
├── heavy_cache_built
├── streams/
│ ├── stream_{id}
│ └── channels_categories
├── lines/
│ ├── line_i_{user_id}
│ ├── line_c_{username_password}
│ └── line_t_{access_token}
└── series/
├── series_{id}
├── series_map
└── episodes_{series_id}
Related files¶
| File | Purpose |
|---|---|
src/Core/Cache/CacheInterface.php |
cache contract |
src/Core/Cache/FileCache.php |
file-based cache implementation |
src/Core/Cache/RedisCache.php |
Redis cache implementation |
src/Infrastructure/Redis/RedisManager.php |
Redis connection singleton |
src/Infrastructure/Cache/CacheReader.php |
legacy cache reader bridge |
src/Cli/CronJobs/CacheCronJob.php |
lightweight cache generation |
src/Cli/CronJobs/CacheEngineCronJob.php |
heavy cache generation (streams, lines, series) |
src/Domain/Bouquet/BouquetService.php |
example of admin-path caching |
src/Domain/Stream/ConnectionTracker.php |
Redis sorted sets for connection state |