diff --git a/.env.example b/.env.example index 579a880b4..6cb409a82 100644 --- a/.env.example +++ b/.env.example @@ -349,6 +349,9 @@ TINYMCE_API_KEY= TMUX_USE_POWERLINE=true TMUX_USE_NERD_FONTS=true TMUX_TERMINAL=tmux-256color +# Main monitor loop delay and slow-statistics refresh interval, in seconds. +TMUX_MONITOR_DELAY=10 +TMUX_REFRESH_INTERVAL=60 # ────────────────────────────────────────────────────────────── # Docker / Sail diff --git a/app/Console/Commands/TmuxMonitor.php b/app/Console/Commands/TmuxMonitor.php index 9d0480028..ed476480a 100644 --- a/app/Console/Commands/TmuxMonitor.php +++ b/app/Console/Commands/TmuxMonitor.php @@ -97,7 +97,7 @@ class TmuxMonitor extends Command // Increment iteration and sleep $this->monitor->incrementIteration(); - sleep(10); + sleep(max(1, (int) config('tmux.monitor.delay', 10))); } $this->info('🛑 Monitor stopped by exit flag'); diff --git a/app/Services/AdditionalProcessing/AdditionalCandidateQuery.php b/app/Services/AdditionalProcessing/AdditionalCandidateQuery.php index 3db5e8044..b2a3791ce 100644 --- a/app/Services/AdditionalProcessing/AdditionalCandidateQuery.php +++ b/app/Services/AdditionalProcessing/AdditionalCandidateQuery.php @@ -235,14 +235,25 @@ final class AdditionalCandidateQuery */ public static function backlogCounts(): array { - $counts = ['total' => 0, 'available' => 0]; + $query = self::baseBuilder(includeClaimed: true) + ->selectRaw('COUNT(*) AS total_count'); - foreach (self::bucketBacklog() as $backlog) { - $counts['total'] += $backlog['total']; - $counts['available'] += $backlog['available']; + if (self::supportsClaims()) { + $query->selectRaw( + 'SUM(CASE WHEN r.'.self::CLAIMED_AT_COLUMN.' IS NULL OR r.'.self::CLAIMED_AT_COLUMN.' < ? THEN 1 ELSE 0 END) AS available_count', + [self::claimStaleBefore()], + ); + } else { + $query->selectRaw('COUNT(*) AS available_count'); } - return $counts; + /** @var object{total_count: int|string|null, available_count: int|string|null}|null $counts */ + $counts = $query->toBase()->first(); + + return [ + 'total' => (int) ($counts->total_count ?? 0), + 'available' => (int) ($counts->available_count ?? 0), + ]; } /** diff --git a/app/Services/Tmux/Tmux.php b/app/Services/Tmux/Tmux.php index 9e7bbe231..cee954440 100644 --- a/app/Services/Tmux/Tmux.php +++ b/app/Services/Tmux/Tmux.php @@ -49,53 +49,51 @@ class Tmux return $runVar['connections']; } - public function getUSPConnections(string $which, mixed $connections): mixed + /** + * @param array $connections + * @return array + */ + public function getUSPConnections(string $which, array $connections, ?string $socketSnapshot = null): array { - switch ($which) { - case 'alternate': - $ip = 'ip_a'; - $port = 'port_a'; - break; - case 'primary': - default: - $ip = 'ip'; - $port = 'port'; - break; + [$ipKey, $portKey] = $which === 'alternate' + ? ['ip_a', 'port_a'] + : ['ip', 'port']; + + $ip = (string) ($connections[$ipKey] ?? ''); + $port = (string) ($connections[$portKey] ?? ''); + $needles = array_values(array_filter([ + $ip !== '' && $port !== '' ? $ip.':'.$port : null, + $ip !== '' ? $ip.':https' : null, + $port !== '' ? $port : null, + $ip !== '' ? $ip : null, + ])); + $lines = preg_split('/\R/', $socketSnapshot ?? $this->getSocketSnapshot()) ?: []; + + foreach ($needles as $needle) { + $matchingLines = array_filter( + $lines, + static fn (string $line): bool => str_contains($line, $needle), + ); + + if ($matchingLines !== []) { + return [ + $which => [ + 'active' => count(array_filter( + $matchingLines, + static fn (string $line): bool => str_contains($line, 'ESTAB'), + )), + 'total' => count($matchingLines), + ], + ]; + } } - // Initialize result structure - $runVar['conncounts'][$which]['active'] = '0'; - $runVar['conncounts'][$which]['total'] = '0'; + return [$which => ['active' => 0, 'total' => 0]]; + } - // 1) Try exact host:port - if (! empty($connections[$ip]) && ! empty($connections[$port])) { - $needle = escapeshellarg($connections[$ip].':'.$connections[$port]); - $runVar['conncounts'][$which]['active'] = str_replace("\n", '', shell_exec("ss -n 2>/dev/null | grep -- $needle | grep -c -- ESTAB")) ?: '0'; - $runVar['conncounts'][$which]['total'] = str_replace("\n", '', shell_exec("ss -n 2>/dev/null | grep -c -- $needle")) ?: '0'; - } - - // 2) Fallback to host:https - if ((int) $runVar['conncounts'][$which]['active'] === 0 && (int) $runVar['conncounts'][$which]['total'] === 0 && ! empty($connections[$ip])) { - $needleHttps = escapeshellarg($connections[$ip].':https'); - $runVar['conncounts'][$which]['active'] = str_replace("\n", '', shell_exec("ss -n 2>/dev/null | grep -- $needleHttps | grep -c -- ESTAB")) ?: '0'; - $runVar['conncounts'][$which]['total'] = str_replace("\n", '', shell_exec("ss -n 2>/dev/null | grep -c -- $needleHttps")) ?: '0'; - } - - // 3) Fallback to port only - if ((int) $runVar['conncounts'][$which]['active'] === 0 && (int) $runVar['conncounts'][$which]['total'] === 0 && ! empty($connections[$port])) { - $needlePort = escapeshellarg((string) $connections[$port]); - $runVar['conncounts'][$which]['active'] = str_replace("\n", '', shell_exec("ss -n 2>/dev/null | grep -- $needlePort | grep -c -- ESTAB")) ?: '0'; - $runVar['conncounts'][$which]['total'] = str_replace("\n", '', shell_exec("ss -n 2>/dev/null | grep -c -- $needlePort")) ?: '0'; - } - - // 4) Fallback to host only - if ((int) $runVar['conncounts'][$which]['active'] === 0 && (int) $runVar['conncounts'][$which]['total'] === 0 && ! empty($connections[$ip])) { - $needleIp = escapeshellarg($connections[$ip]); - $runVar['conncounts'][$which]['active'] = str_replace("\n", '', shell_exec("ss -n 2>/dev/null | grep -- $needleIp | grep -c -- ESTAB")) ?: '0'; - $runVar['conncounts'][$which]['total'] = str_replace("\n", '', shell_exec("ss -n 2>/dev/null | grep -c -- $needleIp")) ?: '0'; - } - - return $runVar['conncounts']; + public function getSocketSnapshot(): string + { + return (string) shell_exec('ss -nH 2>/dev/null'); } /** @@ -377,10 +375,6 @@ class Tmux " SELECT (SELECT TABLE_ROWS FROM information_schema.TABLES WHERE table_name = 'predb' AND TABLE_SCHEMA = %1\$s) AS predb, - (SELECT TABLE_ROWS FROM information_schema.TABLES WHERE table_name = 'missed_parts' AND TABLE_SCHEMA = %1\$s) AS missed_parts_table, - (SELECT TABLE_ROWS FROM information_schema.TABLES WHERE table_name = 'parts' AND TABLE_SCHEMA = %1\$s) AS parts_table, - (SELECT TABLE_ROWS FROM information_schema.TABLES WHERE table_name = 'binaries' AND TABLE_SCHEMA = %1\$s) AS binaries_table, - (SELECT TABLE_ROWS FROM information_schema.TABLES WHERE table_name = 'collections' AND TABLE_SCHEMA = %1\$s) AS releases, (SELECT COUNT(id) FROM usenet_groups WHERE first_record IS NOT NULL AND backfill = 1 AND (now() - INTERVAL backfill_target DAY) < first_record_postdate ) AS backfill_groups_days, @@ -449,7 +443,7 @@ class Tmux { return DB::select( " - SELECT TABLE_NAME AS name + SELECT TABLE_NAME AS name, TABLE_ROWS AS row_count FROM information_schema.TABLES WHERE TABLE_SCHEMA = (SELECT DATABASE()) AND TABLE_NAME REGEXP {escapeString('^(multigroup_)?(collections|binaries|parts|missed_parts)(_[0-9]+)?$')} diff --git a/app/Services/Tmux/TmuxMonitorService.php b/app/Services/Tmux/TmuxMonitorService.php index bea3cd6d5..df7ac4671 100644 --- a/app/Services/Tmux/TmuxMonitorService.php +++ b/app/Services/Tmux/TmuxMonitorService.php @@ -27,6 +27,10 @@ class TmuxMonitorService protected bool $shouldContinue = true; + protected float $lastOperationalRefreshAt = 0.0; + + protected float $lastSlowRefreshAt = 0.0; + public function __construct() { $this->tmux = new Tmux; @@ -124,46 +128,52 @@ class TmuxMonitorService */ public function collectStatistics(): array { - // Refresh settings periodically - $monitorDelay = (int) ($this->runVar['settings']['monitor'] ?? 60); - $timeSinceLastRefresh = time() - ($this->runVar['timers']['timer2'] ?? 0); + $now = microtime(true); + $monitorDelay = max(1, (int) ($this->runVar['settings']['monitor'] ?? 60)); + $slowRefreshDelay = max($monitorDelay, (int) config('tmux.monitor.refresh_interval', 60)); + $statisticsChanged = false; - if ($this->iterations === 1 || $timeSinceLastRefresh >= $monitorDelay) { - $this->refreshStatistics(); - $this->runVar['timers']['timer2'] = time(); + if ($this->refreshIsDue($this->lastOperationalRefreshAt, $monitorDelay, $now)) { + $this->refreshOperationalStatistics(); + $this->lastOperationalRefreshAt = $now; + $this->runVar['timers']['timer2'] = (int) $now; + $statisticsChanged = true; } - // Update connection counts - $this->updateConnectionCounts(); + if ($this->refreshIsDue($this->lastSlowRefreshAt, $slowRefreshDelay, $now)) { + $this->refreshSlowStatistics(); + $this->lastSlowRefreshAt = $now; + $statisticsChanged = true; + } - // Set killswitches + if ($statisticsChanged) { + $this->calculateStatistics(); + } + + $this->updateConnectionCounts(); $this->setKillswitches(); return $this->runVar; } - /** - * Refresh all statistics from database - */ - protected function refreshStatistics(): void + protected function refreshOperationalStatistics(): void { - $timer = time(); - - // Refresh settings + $timer = microtime(true); $this->runVar['settings'] = $this->tmux->getMonitorSettings(); - $this->runVar['timers']['query']['tmux_time'] = time() - $timer; + $this->runVar['timers']['query']['tmux_time'] = microtime(true) - $timer; - // Get category counts - $this->getCategoryCounts(); - - // Get process counts $this->getProcessCounts(); + } - // Get table counts + protected function refreshSlowStatistics(): void + { + $this->getCategoryCounts(); $this->getTableCounts(); + } - // Calculate diffs and percentages - $this->calculateStatistics(); + protected function refreshIsDue(float $lastRefreshAt, int $interval, float $now): bool + { + return $lastRefreshAt === 0.0 || ($now - $lastRefreshAt) >= $interval; } /** @@ -171,41 +181,46 @@ class TmuxMonitorService */ protected function getCategoryCounts(): void { - $timer = time(); + $timer = microtime(true); + $bindings = []; + $aggregates = []; - $this->runVar['counts']['now']['tv'] = Release::query() - ->whereBetween('categories_id', [Category::TV_ROOT, Category::TV_OTHER]) - ->count('id'); + foreach ($this->categoryRanges() as $name => [$minimum, $maximum]) { + $aggregates[] = "SUM(CASE WHEN categories_id BETWEEN ? AND ? THEN 1 ELSE 0 END) AS {$name}"; + $bindings[] = $minimum; + $bindings[] = $maximum; + } - $this->runVar['counts']['now']['movies'] = Release::query() - ->whereBetween('categories_id', [Category::MOVIE_ROOT, Category::MOVIE_OTHER]) - ->count('id'); + try { + $counts = Release::query()->selectRaw(implode(', ', $aggregates), $bindings)->first(); - $this->runVar['counts']['now']['audio'] = Release::query() - ->whereBetween('categories_id', [Category::MUSIC_ROOT, Category::MUSIC_OTHER]) - ->count('id'); + if ($counts !== null) { + foreach (array_keys($this->categoryRanges()) as $name) { + $this->runVar['counts']['now'][$name] = (int) $counts->getAttribute($name); + } + } + } catch (\Exception $e) { + logger()->error('Error collecting category counts: '.$e->getMessage()); + } - $this->runVar['counts']['now']['books'] = Release::query() - ->whereBetween('categories_id', [Category::BOOKS_ROOT, Category::BOOKS_UNKNOWN]) - ->count('id'); + $this->runVar['timers']['query']['init_time'] = microtime(true) - $timer; + } - $this->runVar['counts']['now']['console'] = Release::query() - ->whereBetween('categories_id', [Category::GAME_ROOT, Category::GAME_OTHER]) - ->count('id'); - - $this->runVar['counts']['now']['pc'] = Release::query() - ->whereBetween('categories_id', [Category::PC_ROOT, Category::PC_PHONE_ANDROID]) - ->count('id'); - - $this->runVar['counts']['now']['xxx'] = Release::query() - ->whereBetween('categories_id', [Category::XXX_ROOT, Category::XXX_OTHER]) - ->count('id'); - - $this->runVar['counts']['now']['misc'] = Release::query() - ->whereBetween('categories_id', [Category::OTHER_ROOT, Category::OTHER_HASHED]) - ->count('id'); - - $this->runVar['timers']['query']['init_time'] = time() - $timer; + /** + * @return array + */ + protected function categoryRanges(): array + { + return [ + 'tv' => [Category::TV_ROOT, Category::TV_OTHER], + 'movies' => [Category::MOVIE_ROOT, Category::MOVIE_OTHER], + 'audio' => [Category::MUSIC_ROOT, Category::MUSIC_OTHER], + 'books' => [Category::BOOKS_ROOT, Category::BOOKS_UNKNOWN], + 'console' => [Category::GAME_ROOT, Category::GAME_OTHER], + 'pc' => [Category::PC_ROOT, Category::PC_PHONE_ANDROID], + 'xxx' => [Category::XXX_ROOT, Category::XXX_OTHER], + 'misc' => [Category::OTHER_ROOT, Category::OTHER_HASHED], + ]; } /** @@ -213,7 +228,7 @@ class TmuxMonitorService */ protected function getProcessCounts(): void { - $timer = time(); + $timer = microtime(true); $this->runVar['counts']['now']['work'] = $this->runVar['counts']['now']['work'] ?? 0; $this->runVar['counts']['now']['work_available'] = $this->runVar['counts']['now']['work_available'] ?? 0; @@ -229,10 +244,10 @@ class TmuxMonitorService } } - $this->runVar['timers']['query']['proc1_time'] = time() - $timer; + $this->runVar['timers']['query']['proc1_time'] = microtime(true) - $timer; // Process 2 - $timer2 = time(); + $timer2 = microtime(true); $maxSize = $this->runVar['settings']['maxsize_pp'] ?? ''; $minSize = $this->runVar['settings']['minsize_pp'] ?? ''; @@ -253,7 +268,7 @@ class TmuxMonitorService $this->runVar['counts']['now']['work'] = $additionalBacklog['total']; $this->runVar['counts']['now']['work_available'] = $additionalBacklog['available']; - $this->runVar['timers']['query']['proc2_time'] = time() - $timer2; + $this->runVar['timers']['query']['proc2_time'] = microtime(true) - $timer2; } catch (\Exception $e) { logger()->error('Error collecting process counts: '.$e->getMessage()); @@ -265,77 +280,65 @@ class TmuxMonitorService */ protected function getTableCounts(): void { - $timer = time(); + $timer = microtime(true); try { $this->runVar['counts']['now']['collections_table'] = Collection::query()->count(); + $this->runVar['counts']['now']['releases'] = Release::query()->count(); - // Get binaries/parts counts from information_schema or use approximation - $dbName = config('nntmux.db_name'); - $tables = $this->tmux->cbpmTableQuery(); - - $this->runVar['counts']['now']['binaries_table'] = 0; - $this->runVar['counts']['now']['parts_table'] = 0; - $this->runVar['counts']['now']['missed_parts_table'] = 0; - - foreach ($tables as $table) { - $tableName = $table->name; - $count = $this->getTableRowCount($tableName); - - if (str_contains($tableName, 'binaries')) { - $this->runVar['counts']['now']['binaries_table'] += $count; - } elseif (str_contains($tableName, 'missed_parts')) { - $this->runVar['counts']['now']['missed_parts_table'] += $count; - } elseif (str_contains($tableName, 'parts')) { - $this->runVar['counts']['now']['parts_table'] += $count; - } + foreach ($this->aggregateTableRowEstimates($this->tmux->cbpmTableQuery()) as $key => $count) { + $this->runVar['counts']['now'][$key] = $count; } - $this->runVar['timers']['query']['tpg_time'] = time() - $timer; + $this->runVar['timers']['query']['tpg_time'] = microtime(true) - $timer; - // Get additional table counts (query 4) - $timer4 = time(); - $proc4Query = $this->tmux->proc_query(4, $dbName, ''); - $proc4Result = DB::selectOne($proc4Query); + foreach ([4, 6] as $queryNumber) { + $result = DB::selectOne($this->tmux->proc_query($queryNumber, (string) config('nntmux.db_name'), '')); + if ($result === null) { + continue; + } - if ($proc4Result) { - foreach ((array) $proc4Result as $key => $value) { - $this->runVar['counts']['now'][$key] = $value; + $target = $queryNumber === 4 ? 'counts' : 'timers'; + $section = $queryNumber === 4 ? 'now' : 'newOld'; + foreach ((array) $result as $key => $value) { + $this->runVar[$target][$section][$key] = $value; } } - - // Get newest/oldest data (query 6) - $timer6 = time(); - $proc6Query = $this->tmux->proc_query(6, $dbName, ''); - $proc6Result = DB::selectOne($proc6Query); - - if ($proc6Result) { - foreach ((array) $proc6Result as $key => $value) { - $this->runVar['timers']['newOld'][$key] = $value; - } - } - } catch (\Exception $e) { logger()->error('Error collecting table counts: '.$e->getMessage()); } } /** - * Get row count for a table + * @param array $tables + * @return array{binaries_table: int, parts_table: int, missed_parts_table: int} */ - protected function getTableRowCount(string $tableName): int + protected function aggregateTableRowEstimates(array $tables): array { - try { - $result = DB::selectOne( - 'SELECT TABLE_ROWS AS count FROM information_schema.TABLES - WHERE TABLE_NAME = ? AND TABLE_SCHEMA = DATABASE()', - [$tableName] - ); + $counts = [ + 'binaries_table' => 0, + 'parts_table' => 0, + 'missed_parts_table' => 0, + ]; - return (int) ($result->count ?? 0); - } catch (\Exception $e) { - return 0; + foreach ($tables as $table) { + if (! is_object($table)) { + continue; + } + + $tableName = (string) ($table->name ?? ''); + $count = (int) ($table->row_count ?? 0); + + if (str_contains($tableName, 'binaries')) { + $counts['binaries_table'] += $count; + } elseif (str_contains($tableName, 'missed_parts')) { + $counts['missed_parts_table'] += $count; + } elseif (str_contains($tableName, 'parts')) { + $counts['parts_table'] += $count; + } } + + return $counts; } /** @@ -485,15 +488,18 @@ class TmuxMonitorService */ protected function updateConnectionCounts(): void { + $socketSnapshot = $this->tmux->getSocketSnapshot(); $this->runVar['conncounts'] = $this->tmux->getUSPConnections( 'primary', - $this->runVar['connections'] + $this->runVar['connections'], + $socketSnapshot, ); if ((int) ($this->runVar['constants']['alternate_nntp'] ?? 0) === 1) { $alternateConns = $this->tmux->getUSPConnections( 'alternate', - $this->runVar['connections'] + $this->runVar['connections'], + $socketSnapshot, ); $this->runVar['conncounts'] = array_merge($this->runVar['conncounts'], $alternateConns); } diff --git a/app/Services/Tmux/TmuxOutput.php b/app/Services/Tmux/TmuxOutput.php index 8e0285dc0..d5c4bfa7d 100644 --- a/app/Services/Tmux/TmuxOutput.php +++ b/app/Services/Tmux/TmuxOutput.php @@ -20,6 +20,10 @@ class TmuxOutput extends Tmux private mixed $tmpMasks; + private ?string $gitVersion = null; + + private ?string $gitBranch = null; + /** * TmuxOutput constructor. * @@ -112,8 +116,11 @@ class TmuxOutput extends Tmux { $buffer = ''; $state = ((int) $this->runVar['settings']['is_running'] === 1) ? 'Running' : 'Disabled'; - $version = str_replace(["\n", "\r"], '', Process::run('git describe --tags')->output()); - $branch = str_replace(["\n", "\r"], '', Process::run('git branch --show-current')->output()); + $this->gitVersion ??= trim(Process::run('git describe --tags')->output()); + $this->gitBranch ??= trim(Process::run('git branch --show-current')->output()); + + $version = $this->gitVersion; + $branch = $this->gitBranch; $buffer .= sprintf( $this->tmpMasks[2], @@ -395,52 +402,34 @@ class TmuxOutput extends Tmux protected function _getPaths(): string { - $buffer = ''; + $paths = array_values(array_unique(array_filter([ + $this->runVar['settings']['monitor_path'] ?? null, + $this->runVar['settings']['monitor_path_a'] ?? null, + $this->runVar['settings']['monitor_path_b'] ?? null, + ], static fn (mixed $path): bool => is_string($path) && $path !== '' && file_exists($path)))); - // assign timers from tmux table - $monitor_path = $this->runVar['settings']['monitor_path']; - $monitor_path_a = $this->runVar['settings']['monitor_path_a']; - $monitor_path_b = $this->runVar['settings']['monitor_path_b']; + if ($paths === []) { + return PHP_EOL; + } - if (($monitor_path !== null && file_exists($monitor_path)) - || ($monitor_path_a !== null && file_exists($monitor_path_a)) - || ($monitor_path_b !== null && file_exists($monitor_path_b))) { - $buffer .= "\n"; - $buffer .= sprintf($this->tmpMasks[3], 'File System', 'Used', 'Free'); - $buffer .= $this->_getSeparator(); + $buffer = PHP_EOL; + $buffer .= sprintf($this->tmpMasks[3], 'File System', 'Used', 'Free'); + $buffer .= $this->_getSeparator(); - if (! empty($monitor_path) && file_exists($monitor_path)) { - $disk_use = $this->decodeSize(disk_total_space($monitor_path) - disk_free_space($monitor_path)); - $disk_free = $this->decodeSize(disk_free_space($monitor_path)); - if (basename($monitor_path) === '') { - $show = '/'; - } else { - $show = basename($monitor_path); - } - $buffer .= sprintf($this->tmpMasks[4], $show, $disk_use, $disk_free); + foreach ($paths as $path) { + $total = disk_total_space($path); + $free = disk_free_space($path); + if ($total === false || $free === false) { + continue; } - if (! empty($monitor_path_a) && file_exists($monitor_path_a)) { - $disk_use = $this->decodeSize(disk_total_space($monitor_path_a) - disk_free_space($monitor_path_a)); - $disk_free = $this->decodeSize(disk_free_space($monitor_path_a)); - if (basename($monitor_path_a) === '') { - $show = '/'; - } else { - $show = basename($monitor_path_a); - } - $buffer .= sprintf($this->tmpMasks[4], $show, $disk_use, $disk_free); - } - - if (! empty($monitor_path_b) && file_exists($monitor_path_b)) { - $disk_use = $this->decodeSize(disk_total_space($monitor_path_b) - disk_free_space($monitor_path_b)); - $disk_free = $this->decodeSize(disk_free_space($monitor_path_b)); - if (basename($monitor_path_b) === '') { - $show = '/'; - } else { - $show = basename($monitor_path_b); - } - $buffer .= sprintf($this->tmpMasks[4], $show, $disk_use, $disk_free); - } + $name = basename($path) ?: '/'; + $buffer .= sprintf( + $this->tmpMasks[4], + $name, + $this->decodeSize($total - $free), + $this->decodeSize($free), + ); } return $buffer.PHP_EOL; @@ -455,7 +444,7 @@ class TmuxOutput extends Tmux $this->tmpMasks[4], 'Combined', sprintf( - '%d %d %d %d %d %d %d', + '%.3f %.3f %.3f %.3f %.3f %.3f %.3f', $this->runVar['timers']['query']['tmux_time'], $this->runVar['timers']['query']['split_time'], $this->runVar['timers']['query']['init_time'], @@ -465,7 +454,7 @@ class TmuxOutput extends Tmux $this->runVar['timers']['query']['tpg_time'] ), sprintf( - '%d %d %d %d %d %d %d', + '%.3f %.3f %.3f %.3f %.3f %.3f %.3f', $this->runVar['timers']['query']['tmux_time'], $this->runVar['timers']['query']['split1_time'], $this->runVar['timers']['query']['init1_time'], diff --git a/tests/Unit/TmuxMonitorServiceTest.php b/tests/Unit/TmuxMonitorServiceTest.php index c20476bef..5bf9e6e8b 100644 --- a/tests/Unit/TmuxMonitorServiceTest.php +++ b/tests/Unit/TmuxMonitorServiceTest.php @@ -4,7 +4,9 @@ declare(strict_types=1); namespace Tests\Unit; +use App\Services\Tmux\Tmux; use App\Services\Tmux\TmuxMonitorService; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use ReflectionClass; use ReflectionMethod; @@ -47,4 +49,74 @@ class TmuxMonitorServiceTest extends TestCase $this->assertSame('0', $updatedRunVar['counts']['diff']['work_available']); $this->assertSame(2, $updatedRunVar['counts']['now']['total_work']); } + + #[DataProvider('refreshScheduleProvider')] + public function test_refresh_schedule(float $lastRefreshAt, int $interval, float $now, bool $expected): void + { + $reflection = new ReflectionClass(TmuxMonitorService::class); + $monitor = $reflection->newInstanceWithoutConstructor(); + $refreshIsDue = new ReflectionMethod(TmuxMonitorService::class, 'refreshIsDue'); + + $this->assertSame($expected, $refreshIsDue->invoke($monitor, $lastRefreshAt, $interval, $now)); + } + + /** + * @return array + */ + public static function refreshScheduleProvider(): array + { + return [ + 'first refresh' => [0.0, 60, 100.0, true], + 'before interval' => [100.0, 60, 159.999, false], + 'at interval' => [100.0, 60, 160.0, true], + ]; + } + + public function test_collection_table_estimates_do_not_contribute_to_release_total(): void + { + $reflection = new ReflectionClass(TmuxMonitorService::class); + $monitor = $reflection->newInstanceWithoutConstructor(); + $aggregate = new ReflectionMethod(TmuxMonitorService::class, 'aggregateTableRowEstimates'); + + $counts = $aggregate->invoke($monitor, [ + (object) ['name' => 'collections', 'row_count' => 242650], + (object) ['name' => 'binaries', 'row_count' => 120], + (object) ['name' => 'multigroup_parts_1', 'row_count' => 80], + (object) ['name' => 'missed_parts', 'row_count' => 5], + ]); + + $this->assertSame([ + 'binaries_table' => 120, + 'parts_table' => 80, + 'missed_parts_table' => 5, + ], $counts); + $this->assertArrayNotHasKey('releases', $counts); + } + + public function test_connection_counts_are_derived_from_one_socket_snapshot(): void + { + $reflection = new ReflectionClass(Tmux::class); + /** @var Tmux $tmux */ + $tmux = $reflection->newInstanceWithoutConstructor(); + $snapshot = implode("\n", [ + 'ESTAB 0 0 10.0.0.2:40000 192.0.2.10:119', + 'CLOSE-WAIT 0 0 10.0.0.2:40001 192.0.2.10:119', + 'ESTAB 0 0 10.0.0.2:40002 192.0.2.20:563', + ]); + $connections = [ + 'ip' => '192.0.2.10', + 'port' => 119, + 'ip_a' => '192.0.2.20', + 'port_a' => 563, + ]; + + $this->assertSame( + ['primary' => ['active' => 1, 'total' => 2]], + $tmux->getUSPConnections('primary', $connections, $snapshot), + ); + $this->assertSame( + ['alternate' => ['active' => 1, 'total' => 1]], + $tmux->getUSPConnections('alternate', $connections, $snapshot), + ); + } }