mirror of
https://github.com/Vateron-Media/XC_VM.git
synced 2026-10-31 11:03:22 +01:00
Seven unit tests started redis-server on a random port in 20000-40000 (it overlaps the kernel's ephemeral range, so it can be taken) and gave it 2.5 s to listen, then went on whether or not it did. On a busy runner ConnectionIngestIdempotencyTest's redis cases failed with "Connection refused" (PR #319's CI). ConnectionDivergenceTest already had the fix; it now lives in tests/Support/RedisServer: a port the kernel picks free, up to 10 s to listen, and a server that never listens is redis not available, so its tests skip instead of erroring. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CWvQvZGYnikZ2bx17R6fN6
189 lines
9.4 KiB
PHP
189 lines
9.4 KiB
PHP
<?php
|
|
|
|
use PHPUnit\Framework\TestCase;
|
|
use XcVm\Core\Cluster\AgentConnections;
|
|
use XcVm\Core\Cluster\StoredConnections;
|
|
use XcVm\Core\Config\SettingsManager;
|
|
use XcVm\Domain\Cluster\ConnectionDigest;
|
|
use XcVm\Domain\Cluster\ConnectionIngest;
|
|
use XcVm\Infrastructure\Database\DatabaseFactory;
|
|
use XcVm\Infrastructure\Redis\RedisManager;
|
|
use XcVm\Tests\Support\RedisServer;
|
|
|
|
/**
|
|
* The one reader of a node's connections in MAIN's store and the one line
|
|
* identity rule (cluster plan, Phase 6): the digest check and a snapshot's
|
|
* removals read the open ones, cluster:seed-connections reads them all, and
|
|
* ingest keeps a record under the identity its admission reserved.
|
|
*/
|
|
final class StoredConnectionsTest extends TestCase {
|
|
private static ?string $rRedisDir = null;
|
|
|
|
private static int $rRedisPort = 0;
|
|
|
|
/** @var resource|null */
|
|
private static $rRedisProc = null;
|
|
|
|
private TestDb $rDb;
|
|
|
|
public static function setUpBeforeClass(): void {
|
|
if (!class_exists(\Redis::class) || !function_exists('igbinary_serialize') || trim((string) shell_exec('command -v redis-server')) === '') {
|
|
return;
|
|
}
|
|
self::$rRedisDir = sys_get_temp_dir() . '/xcvm-redis-' . bin2hex(random_bytes(4));
|
|
mkdir(self::$rRedisDir);
|
|
[self::$rRedisProc, self::$rRedisPort] = RedisServer::start(self::$rRedisDir) ?? [null, 0];
|
|
}
|
|
|
|
public static function tearDownAfterClass(): void {
|
|
if (self::$rRedisProc !== null) {
|
|
proc_terminate(self::$rRedisProc);
|
|
proc_close(self::$rRedisProc);
|
|
self::$rRedisProc = null;
|
|
}
|
|
if (self::$rRedisDir !== null) {
|
|
exec('rm -rf ' . escapeshellarg(self::$rRedisDir));
|
|
}
|
|
}
|
|
|
|
protected function setUp(): void {
|
|
$this->rDb = new TestDb();
|
|
$this->rDb->exec('CREATE TABLE `lines_live` (`activity_id` INTEGER PRIMARY KEY AUTO_INCREMENT, `user_id` int, `stream_id` int, `server_id` int, `proxy_id` int, `user_agent` text, `user_ip` text, `container` text, `pid` int, `date_start` int, `geoip_country_code` text, `isp` text, `external_device` text, `hls_last_read` int, `hls_end` int DEFAULT 0, `hmac_id` int, `hmac_identifier` text, `uuid` text, `divergence` int DEFAULT 0)');
|
|
DatabaseFactory::set($this->rDb);
|
|
SettingsManager::set(['redis_handler' => 0]);
|
|
}
|
|
|
|
protected function tearDown(): void {
|
|
SettingsManager::set([]);
|
|
DatabaseFactory::reset();
|
|
$this->redis(null);
|
|
}
|
|
|
|
/** Point RedisManager at the test server (or nowhere). */
|
|
private function redis(?\Redis $rRedis): void {
|
|
(new \ReflectionProperty(RedisManager::class, 'instance'))->setValue(null, $rRedis);
|
|
(new \ReflectionProperty(RedisManager::class, 'lastPingCheck'))->setValue(null, time());
|
|
}
|
|
|
|
private function connectRedis(): \Redis {
|
|
if (self::$rRedisProc === null) {
|
|
$this->markTestSkipped('redis-server, phpredis or igbinary not available');
|
|
}
|
|
$rRedis = new \Redis();
|
|
$rRedis->connect('127.0.0.1', self::$rRedisPort, 2.0, null, 0, 2.0);
|
|
$rRedis->flushAll();
|
|
$this->redis($rRedis);
|
|
return $rRedis;
|
|
}
|
|
|
|
private function row(?string $rUUID, int $rServer, ?int $rUser, int $rEnd = 0, ?int $rHMAC = null, ?string $rIdentifier = null): void {
|
|
$this->rDb->query('INSERT INTO `lines_live` (`uuid`, `server_id`, `user_id`, `hls_end`, `hmac_id`, `hmac_identifier`, `divergence`) VALUES (?, ?, ?, ?, ?, ?, 40)', $rUUID, $rServer, $rUser, $rEnd, $rHMAC, $rIdentifier);
|
|
}
|
|
|
|
public function testTheIdentityIsTheLineOrTheHmacPair(): void {
|
|
$this->assertSame('7', StoredConnections::identity(['user_id' => 7, 'hmac_id' => 3, 'hmac_identifier' => 'dev']));
|
|
$this->assertSame('7', StoredConnections::identity(['user_id' => '7']));
|
|
$this->assertSame('3_dev', StoredConnections::identity(['user_id' => null, 'hmac_id' => 3, 'hmac_identifier' => 'dev']));
|
|
$this->assertSame('3_' . str_repeat('i', 255), StoredConnections::identity(['hmac_id' => 3, 'hmac_identifier' => str_repeat('i', 300)]), 'the identifier cut to 255 bytes, as a token\'s and a node\'s request both give it');
|
|
$this->assertSame('3_dev', StoredConnections::identity(['user_id' => '0', 'hmac_id' => '3', 'hmac_identifier' => 'dev']), 'a line id of 0 is no line');
|
|
$this->assertSame('3_', StoredConnections::identity(['hmac_id' => 3]));
|
|
$this->assertSame('0_', StoredConnections::identity([]));
|
|
}
|
|
|
|
public function testTheIdentityReadsBothIdsAsTheDigestsOwnerDoes(): void {
|
|
// A non-canonical id is the owner's integer (ConnectionDigest::owner()),
|
|
// as the admission that reserved the viewer computed it.
|
|
foreach ([['user_id' => '042'], ['user_id' => 42.0], ['hmac_id' => '03', 'hmac_identifier' => 'x'], ['hmac_id' => 3, 'hmac_identifier' => 'x']] as $rRecord) {
|
|
$rOwner = ConnectionDigest::owner($rRecord);
|
|
$rIdentity = StoredConnections::identity($rRecord);
|
|
$this->assertSame(str_starts_with($rOwner, 'u:') ? substr($rOwner, 2) : str_replace(':', '_', substr($rOwner, 2)), $rIdentity);
|
|
}
|
|
$this->assertSame('42', StoredConnections::identity(['user_id' => '042']));
|
|
$this->assertSame('3_x', StoredConnections::identity(['hmac_id' => '03', 'hmac_identifier' => 'x']));
|
|
}
|
|
|
|
public function testTheTableReaderTakesTheOpenOnesOrAllOfTheNodes(): void {
|
|
$this->row('a', 5, 7);
|
|
$this->row('h', 5, null, 0, 3, 'dev');
|
|
$this->row('e', 5, 9, 1); // ended
|
|
$this->row('x', 6, 7); // another node's
|
|
$this->row(null, 5, 8); // no uuid
|
|
$this->row('', 5, 8);
|
|
|
|
$rOpen = StoredConnections::ofServer(5, true);
|
|
$this->assertSame(['a', 'h'], array_column($rOpen, 'uuid'));
|
|
$this->assertSame(['7', '3_dev'], array_column($rOpen, 'identity'));
|
|
|
|
$rAll = StoredConnections::ofServer(5, false);
|
|
$this->assertSame(['a', 'h', 'e'], array_column($rAll, 'uuid'), 'ended HLS rows too, as the registry holds them');
|
|
$this->assertSame(['7', '3_dev', '9'], array_column($rAll, 'identity'));
|
|
$rKeys = array_keys($rAll[0]);
|
|
sort($rKeys);
|
|
$rWant = array_values(array_diff(AgentConnections::RECORD_KEYS, ['on_demand']));
|
|
sort($rWant);
|
|
$this->assertSame($rWant, $rKeys, 'only the registry record\'s keys, never the line\'s other columns');
|
|
|
|
$rDigest = ConnectionDigest::stored(5);
|
|
$this->assertSame(['a', 'h'], array_keys($rDigest), 'the digest reads the open ones, by uuid');
|
|
$this->assertSame(['uuid', 'user_id', 'hmac_id', 'hmac_identifier', 'hls_end'], array_keys($rDigest['a']), 'only what the digest reads of them');
|
|
}
|
|
|
|
public function testTheReaderTakesOnlyTheKeysAskedFor(): void {
|
|
$this->row('h', 5, null, 0, 3, 'dev');
|
|
$this->row(null, 5, 8);
|
|
$this->row('', 5, 8);
|
|
$this->assertSame([['uuid' => 'h', 'identity' => '3_dev']], StoredConnections::ofServer(5, false, ['uuid', 'identity']), 'the identity from the owner\'s columns, which are not asked for');
|
|
$this->assertSame([['hls_end' => 0]], StoredConnections::ofServer(5, true, ['hls_end']), 'rows without a uuid stay out when the uuid is not asked for');
|
|
}
|
|
|
|
public function testWithoutItsDatabaseTheReaderThrows(): void {
|
|
DatabaseFactory::reset();
|
|
$this->expectExceptionMessage('no database');
|
|
StoredConnections::ofServer(5, false);
|
|
}
|
|
|
|
public function testWithoutRedisTheReaderThrows(): void {
|
|
SettingsManager::set(['redis_handler' => 1]);
|
|
RedisManager::closeInstance();
|
|
RedisManager::useConnector(static fn() => null); // Redis unreachable
|
|
$this->expectExceptionMessage('redis unavailable');
|
|
try {
|
|
ConnectionDigest::stored(5);
|
|
} finally {
|
|
RedisManager::useConnector(null);
|
|
}
|
|
}
|
|
|
|
public function testTheRedisReaderTakesTheNodesRecordsAsStored(): void {
|
|
$rRedis = $this->connectRedis();
|
|
SettingsManager::set(['redis_handler' => 1]);
|
|
$rBase = ['stream_id' => 100, 'proxy_id' => null, 'user_agent' => 'VLC', 'user_ip' => '10.0.0.9', 'container' => 'hls', 'pid' => 0, 'date_start' => 1800000000, 'hls_last_read' => 1800000000, 'hls_end' => 0];
|
|
$this->assertTrue(ConnectionIngest::upsert(5, ['uuid' => 'a', 'user_id' => '042'] + $rBase));
|
|
$this->assertTrue(ConnectionIngest::upsert(5, ['uuid' => 'h', 'hmac_id' => 3, 'hmac_identifier' => 'dev'] + $rBase));
|
|
$this->assertTrue(ConnectionIngest::upsert(5, ['uuid' => 'e', 'user_id' => 9, 'hls_end' => 1] + $rBase));
|
|
$this->assertTrue(ConnectionIngest::upsert(6, ['uuid' => 'x', 'user_id' => 7] + $rBase));
|
|
$rStray = ['uuid' => 'y', 'server_id' => 6, 'user_id' => 8, 'divergence' => 40];
|
|
$rRedis->set('y', igbinary_serialize($rStray));
|
|
$rRedis->zAdd('SERVER#5', 1, 'y'); // listed under node 5, but another node's record
|
|
|
|
$this->assertSame(['a'], $rRedis->zRange('LINE#42', 0, -1), 'kept under the line\'s integer id, where its admission reserved it');
|
|
$this->assertSame([], $rRedis->zRange('LINE#042', 0, -1));
|
|
|
|
$this->assertContains('e', $rRedis->zRange('SERVER#5', 0, -1), 'a record upserted as ended is listed under its node');
|
|
|
|
$rAll = StoredConnections::ofServer(5, false);
|
|
usort($rAll, static fn($rA, $rB) => strcmp($rA['uuid'], $rB['uuid']));
|
|
$this->assertSame(['a', 'e', 'h'], array_column($rAll, 'uuid'), 'the ended one too, as the registry holds it');
|
|
$this->assertSame(['42', '9', '3_dev'], array_column($rAll, 'identity'), 'the identity the record was stored under');
|
|
$this->assertSame([], array_diff(array_keys($rAll[0]), AgentConnections::RECORD_KEYS));
|
|
|
|
$rOpen = array_column(StoredConnections::ofServer(5, true), 'uuid');
|
|
sort($rOpen);
|
|
$this->assertSame(['a', 'h'], $rOpen, 'the open ones only, as in lines_live');
|
|
$rDigest = ConnectionDigest::stored(5);
|
|
ksort($rDigest);
|
|
$this->assertSame(['a', 'h'], array_keys($rDigest), 'the digest reads the open ones, by uuid');
|
|
$this->assertSame([], array_diff(array_keys($rDigest['a']), ['uuid', 'user_id', 'hmac_id', 'hmac_identifier', 'hls_end']), 'only what the digest reads of them');
|
|
}
|
|
}
|