Files
XC_VM/tests/Unit/ServerCapacityTest.php
T
obscuremindandClaude Opus 5.5 02ea95f1c2 fix(connections): a silent store, a reused worker pid, and reads that cost every viewer
From a review of how the panel uses Redis for its viewers.

Viewers cut or kept wrongly:
- A streaming worker's five-minute check-in ended the stream when the store
  could not be asked: "Redis did not answer" read as "the connection is
  gone", so a Redis restart cut every TS, VOD and timeshift viewer whose
  check-in fell in it. ConnectionTracker::checkIn() goes on, asks again in
  30 s, and stops after ten minutes of silence.
- The sweep closed a connection that went silent with a kill -9 of the pid
  its record named. That pid is a PHP-FPM worker which could not write its
  close and serves another viewer since. The sweep's close kills nothing.
- The on-demand watcher read "Redis did not answer" as "no viewers" and
  could stop a watched stream. It skips the pass.
- The enable-time copy of lines_live into Redis deleted the rows without
  looking at the transaction's answer, and loaded them over what Redis kept
  from when the handler was last on: that snapshot is emptied first when the
  handler really was off.

Work done for nothing:
- With the viewers in MySQL (the default) the limiter still connected to
  Redis on every viewer request. Redis is asked for only where they are.
- fanout_sync read one record per viewer every ten seconds; cron:streams
  read all of a server's viewers again for each direct-proxy stream; the
  on-demand watcher read and decoded every record of every running stream
  each pass; the proxies page decoded every record of the parent servers
  for one number. Each reads once, in batches, or the set's size.
- Batched reads went as transactions (a round trip a command) and counts
  fetched whole member lists: a pipeline, and ZCARD.
- Each playback start counted every server's viewers and wrote a file that
  nothing read. The balancer takes the load as it was last counted while
  that is three seconds old (ConnectionTracker::capacity()).
- LINE_ALL# and CONNECTIONS were written for every connection and read by
  nothing: no longer written (still cleared, for what an older node wrote).
- A stored user agent is 255 bytes, as the table holds it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 17:41:59 +00:00

69 lines
3.3 KiB
PHP

<?php
use PHPUnit\Framework\TestCase;
use XcVm\Core\Cache\FileCache;
use XcVm\Domain\Stream\ConnectionTracker;
use XcVm\Infrastructure\Database\DatabaseFactory;
use XcVm\Infrastructure\Redis\RedisManager;
/**
* The servers' load, where nothing set the global servers: the cluster API,
* whose rtmp_auth places a load balancer's RTMP viewer (StreamRedirector)
* and threw on array_keys(null) with the Redis connection handler on.
*/
final class ServerCapacityTest extends TestCase {
public function testItReadsTheServersWhereNoGlobalOnesAreSet(): void {
$rDb = new TestDb();
$rDb->exec('CREATE TABLE `lines_live` (`server_id` int, `proxy_id` int DEFAULT 0, `hls_end` int DEFAULT 0)');
$rDb->exec('INSERT INTO `lines_live` (`server_id`) VALUES (2), (2)');
DatabaseFactory::set($rDb);
RedisManager::closeInstance();
RedisManager::useConnector(static fn() => null);
FileCache::setCache('servers', [2 => ['server_online' => 1, 'server_hardware' => '{}', 'network_guaranteed_speed' => 1000, 'watchdog' => ['bytes_sent' => 0], 'total_clients' => 10]]);
$rWas = [$GLOBALS['rSettings'] ?? null, $GLOBALS['rServers'] ?? null];
$GLOBALS['rSettings'] = ['redis_handler' => 0, 'split_by' => 'band'];
unset($GLOBALS['rServers']);
try {
$rRows = ConnectionTracker::getCapacity();
} finally {
[$GLOBALS['rSettings'], $GLOBALS['rServers']] = $rWas;
DatabaseFactory::reset();
RedisManager::useConnector(null);
FileCache::deleteCache('servers');
}
$this->assertSame(2, (int) $rRows[2]['online_clients']);
$this->assertSame(0.0, $rRows[2]['capacity'], 'nothing sent yet, on its guaranteed speed');
}
/**
* A viewer is placed by the load as it was last counted, while that is a
* few seconds old (the watchdog counts and leaves the file): each playback
* start counted every server's viewers itself, and the file was read by nothing.
*/
public function testAViewerIsPlacedByTheLoadLastCounted(): void {
defined('CACHE_TMP_PATH') || define('CACHE_TMP_PATH', sys_get_temp_dir() . '/xcvm-capacity-' . getmypid() . '/');
is_dir(CACHE_TMP_PATH) || mkdir(CACHE_TMP_PATH, 0775, true);
$rFile = CACHE_TMP_PATH . 'servers_capacity';
$rDb = new TestDb();
$rDb->exec('CREATE TABLE `lines_live` (`server_id` int, `proxy_id` int DEFAULT 0, `hls_end` int DEFAULT 0)');
$rDb->exec('INSERT INTO `lines_live` (`server_id`) VALUES (2), (2), (2)');
DatabaseFactory::set($rDb);
RedisManager::closeInstance();
$rWas = [$GLOBALS['rSettings'] ?? null, $GLOBALS['rServers'] ?? null];
$GLOBALS['rSettings'] = ['redis_handler' => 0, 'split_by' => 'conn'];
$GLOBALS['rServers'] = [2 => ['server_online' => 1]];
try {
file_put_contents($rFile, json_encode([2 => ['server_id' => '2', 'online_clients' => '1', 'capacity' => '1']]));
$this->assertSame('1', ConnectionTracker::capacity()[2]['online_clients'], 'counted a moment ago: taken as it is');
touch($rFile, time() - ConnectionTracker::CAPACITY_SECONDS - 1);
$this->assertSame(3, (int) ConnectionTracker::capacity()[2]['online_clients'], 'older: counted now');
$this->assertSame(3, (int) json_decode((string) file_get_contents($rFile), true)[2]['online_clients'], 'and left for the next viewer');
} finally {
[$GLOBALS['rSettings'], $GLOBALS['rServers']] = $rWas;
DatabaseFactory::reset();
@unlink($rFile);
}
}
}