diff --git a/src/Cli/Commands/CacheHandlerCommand.php b/src/Cli/Commands/CacheHandlerCommand.php index b374abf1..ac307935 100644 --- a/src/Cli/Commands/CacheHandlerCommand.php +++ b/src/Cli/Commands/CacheHandlerCommand.php @@ -7,7 +7,6 @@ use XcVm\Cli\DaemonTrait; use XcVm\Core\Config\SettingsManager; use XcVm\Core\Config\SettingsRepository; use XcVm\Domain\Line\LineService; -use XcVm\Domain\Server\ServerRepository; use XcVm\Infrastructure\Signal\SignalQueue; /** @@ -58,9 +57,11 @@ class CacheHandlerCommand implements CommandInterface { break; } + if ($this->serversRefreshDue()) { + $this->refreshServers(); + } if ($this->shouldRefreshSettings()) { SettingsManager::set(SettingsRepository::getAll(true)); - ServerRepository::getAll(true); if (!SettingsManager::get('enable_cache')) { echo "Cache disabled! Break.\n"; break; diff --git a/src/Cli/Commands/SignalsCommand.php b/src/Cli/Commands/SignalsCommand.php index 7932af38..ce321677 100644 --- a/src/Cli/Commands/SignalsCommand.php +++ b/src/Cli/Commands/SignalsCommand.php @@ -57,8 +57,10 @@ class SignalsCommand implements CommandInterface { if (!$this->refreshOrBreak()) { break; } - if ($this->rLastCheck) { - $rServers = ServerRepository::getAll(true); + // Was every pass (four times a second): a `SELECT * FROM servers` + // against MAIN from every node, all day. + if ($this->serversRefreshDue()) { + $rServers = $this->refreshServers(); } // Stop if Redis required but dead. checkRedisHealth() catches diff --git a/src/Cli/Commands/WatchdogCommand.php b/src/Cli/Commands/WatchdogCommand.php index 849baa86..865c59bf 100644 --- a/src/Cli/Commands/WatchdogCommand.php +++ b/src/Cli/Commands/WatchdogCommand.php @@ -80,6 +80,10 @@ class WatchdogCommand implements CommandInterface { $this->attemptRedisRestart(); } + if ($this->serversRefreshDue()) { + $rServers = $this->refreshServers(); + } + if ($this->shouldRefreshSettings()) { if (!ProcessManager::isNginxRunning()) { echo "Not running! Break.\n"; @@ -89,7 +93,6 @@ class WatchdogCommand implements CommandInterface { echo "File changed! Break.\n"; break; } - $rServers = ServerRepository::getAll(true); SettingsManager::set(SettingsRepository::getAll(true)); ConnectionTracker::getCapacity(true); ConnectionTracker::getCapacity(false); diff --git a/src/Cli/DaemonTrait.php b/src/Cli/DaemonTrait.php index e2af520e..6d1d52a9 100644 --- a/src/Cli/DaemonTrait.php +++ b/src/Cli/DaemonTrait.php @@ -5,6 +5,7 @@ namespace XcVm\Cli; use XcVm\Core\Config\SettingsManager; use XcVm\Core\Config\SettingsRepository; use XcVm\Core\Process\ProcessManager; +use XcVm\Domain\Server\ServerRepository; use XcVm\Infrastructure\Redis\RedisManager; /** @@ -30,6 +31,9 @@ trait DaemonTrait { /** @var int Интервал обновления настроек (секунды) */ protected $rRefreshInterval = 60; + /** @var int|null When the servers list was last reloaded from the DB. */ + protected $rServersRefreshedAt = null; + /** @var resource|null Файловый lock для singleton-демона */ protected $rDaemonLockHandle; @@ -74,6 +78,31 @@ trait DaemonTrait { /** * Проверить нужно ли обновить настройки (по таймеру). */ + /** + * Reload the servers list at most this often. Often enough that routing, + * ports and node state are never more than five seconds stale; rare + * enough that a tight loop does not re-read `servers` on every pass. + */ + protected function serversRefreshInterval(): int { + return 5; + } + + /** Is the servers list due for a reload? */ + protected function serversRefreshDue(?int $rNow = null): bool { + return $this->rServersRefreshedAt === null + || ($rNow ?? time()) - $this->rServersRefreshedAt >= $this->serversRefreshInterval(); + } + + /** + * Reload the servers list from the DB (which also rewrites its cache). + * + * @return array> + */ + protected function refreshServers(): array { + $this->rServersRefreshedAt = time(); + return ServerRepository::getAll(true); + } + protected function shouldRefreshSettings(): bool { return !$this->rLastCheck || $this->rRefreshInterval <= time() - $this->rLastCheck; } diff --git a/tests/Unit/SignalsLoopQueryRateTest.php b/tests/Unit/SignalsLoopQueryRateTest.php new file mode 100644 index 00000000..83f1ac08 --- /dev/null +++ b/tests/Unit/SignalsLoopQueryRateTest.php @@ -0,0 +1,61 @@ +serversRefreshDue($rNow); + } + + public function markRefreshed(int $rAt): void { + $this->rServersRefreshedAt = $rAt; + } + }; + } + + public function testReloadAtMostEveryFiveSeconds(): void { + $rDaemon = $this->daemon(); + $this->assertTrue($rDaemon->due(1000), 'first pass loads'); + $rDaemon->markRefreshed(1000); + $this->assertFalse($rDaemon->due(1000)); + $this->assertFalse($rDaemon->due(1004)); + $this->assertTrue($rDaemon->due(1005)); + } + + public function testAQuarterSecondLoopReadsServersTwelveTimesAMinuteNotTwoHundredForty(): void { + $rDaemon = $this->daemon(); + $rReads = 0; + for ($rTick = 0; $rTick < 240; $rTick++) { // one minute at usleep(250000) + $rNow = 1000 + intdiv($rTick, 4); + if ($rDaemon->due($rNow)) { + $rReads++; + $rDaemon->markRefreshed($rNow); + } + } + $this->assertSame(12, $rReads); + } + + /** @dataProvider daemons */ + public function testDaemonsUseTheTimer(string $rFile): void { + $rSource = (string) file_get_contents(dirname(__DIR__, 2) . '/src/Cli/Commands/' . $rFile); + $this->assertStringContainsString('$this->serversRefreshDue()', $rSource); + $this->assertStringNotContainsString('ServerRepository::getAll(true)', $rSource); + } + + /** @return array */ + public static function daemons(): array { + return ['signals' => ['SignalsCommand.php'], 'watchdog' => ['WatchdogCommand.php'], 'cache handler' => ['CacheHandlerCommand.php']]; + } +}