Files
XC_VM/tests/Unit/DaemonModeChangeTest.php
T
obscuremind 2de06fda03 fix(cluster): the watchdog keeps running on a node with TELEMETRY
With the TELEMETRY flow on, or in mode 2, a watchdog pass wrote its local
sample and then left its loop (break), so restartDaemon() booted a new
PHP process after every pass: on the 1-CPU test load balancer six
generations in 30 s, about a quarter of its CPU. The pass now continues
in the same process, as the legacy one always did; a move across mode 2,
which the restart used to pick up, ends the generation instead.
2026-10-07 16:35:45 +00:00

95 lines
4.0 KiB
PHP

<?php
use PHPUnit\Framework\TestCase;
use XcVm\Cli\DaemonTrait;
use XcVm\Core\Cluster\NodeFlows;
use XcVm\Core\Cluster\NodeRole;
/**
* A daemon decides once, at its start, whether it reads MAIN's database, and
* keeps the connection it opened. After a move to mode 2 the signals, queue
* and scanner daemons went on reading MAIN's MySQL and Redis over it until
* something else restarted them; after a move back they read nothing. They
* leave their loop when the node crosses mode 2 (DaemonTrait::modeChanged),
* and their next generation decides again.
*/
final class DaemonModeChangeTest extends TestCase {
private string $rFile;
protected function setUp(): void {
$this->rFile = sys_get_temp_dir() . '/xcvm-daemon-mode-' . bin2hex(random_bytes(4)) . '.json';
NodeRole::useMainBuild(false);
}
protected function tearDown(): void {
NodeRole::useMainBuild(null);
NodeFlows::usePath(null);
@unlink($this->rFile);
}
private function mode(int $rMode): void {
file_put_contents($this->rFile, json_encode(['mode' => $rMode, 'flows' => 255, 'state' => 'active']));
NodeFlows::usePath($this->rFile, true);
clearstatcache();
}
public function testADaemonLeavesItsLoopWhenTheNodeCrossesModeTwo(): void {
$rDaemon = new class {
use DaemonTrait;
public function start(): void {
$this->initDaemonMD5();
}
public function changed(): bool {
return $this->modeChanged();
}
};
$this->mode(1);
$this->assertFalse($rDaemon->changed(), 'not started: nothing to compare with');
$rDaemon->start();
$this->assertFalse(NodeRole::refusesConnects());
$this->assertFalse($rDaemon->changed());
$this->mode(0);
$this->assertFalse($rDaemon->changed(), 'modes 0 and 1 both read MAIN\'s database');
$this->mode(2);
$this->assertTrue(NodeRole::refusesConnects());
$this->assertTrue($rDaemon->changed(), 'started reading MAIN\'s database, and the node no longer may');
$rDaemon->start(); // its next generation
$this->assertFalse($rDaemon->changed());
$this->mode(1);
$this->assertTrue($rDaemon->changed(), 'and back: it would read nothing');
}
/**
* Each daemon that holds a connection asks every pass, not with its
* minute's settings refresh: signals, queue and fanout_sync through
* refreshOrBreak(), whose first statement it is; the scanner in its loop's
* condition (it sleeps a minute a pass); the on-demand daemon, which has
* no DaemonTrait and pings the connection it opened, by itself.
*/
public function testTheDaemonsThatKeepAConnectionCheckItEveryPass(): void {
$rTrait = (string) file_get_contents(MAIN_HOME . 'Cli/DaemonTrait.php');
$this->assertMatchesRegularExpression('/function refreshOrBreak\(\): bool \{\s*(?:\/\/[^\n]*\s*)*if \(\$this->modeChanged\(\)\) \{/', $rTrait, 'before the settings\' timer');
$rCommand = static fn(string $rName): string => (string) file_get_contents(MAIN_HOME . 'Cli/Commands/' . $rName . 'Command.php');
foreach (['Signals', 'Queue', 'FanoutSync'] as $rDaemon) {
$this->assertStringContainsString('$this->refreshOrBreak()', $rCommand($rDaemon), $rDaemon);
}
$this->assertStringContainsString('while (!$this->modeChanged() && (', $rCommand('Scanner'));
$this->assertMatchesRegularExpression('/while \(true\) \{\s*(?:\/\/[^\n]*\s*)*if \(\$rApi !== NodeRole::refusesConnects\(\)\) \{/', $rCommand('Watchdog'), 'the watchdog, which runs pass after pass in mode 2');
$this->assertMatchesRegularExpression('/\$rRefuses = NodeRole::refusesConnects\(\);.*if \(\$rRefuses !== NodeRole::refusesConnects\(\) \|\|/s', $rCommand('Ondemand'));
}
/**
* fanout_sync is started by `service` alone, so a generation that dies at
* its start is gone for good: on a node in mode 2 it opens no Redis of
* MAIN's (the Redis handler on), which that node is refused.
*/
public function testFanoutSyncOpensNoRedisOnANodeThatRefuses(): void {
$this->assertMatchesRegularExpression('/if \(!NodeRole::refusesConnects\(\)\) \{\s*\$this->initRedisIfEnabled\(\);/', (string) file_get_contents(MAIN_HOME . 'Cli/Commands/FanoutSyncCommand.php'));
}
}