- NodeSelector: array_values inside cache closure (fix key gaps) - GatewayRegistry: prune expired entries from live_list on access - Removed orphaned lethean::names view (replaced by names::index) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
117 lines
3.4 KiB
PHP
117 lines
3.4 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Mod\Gateway\Services;
|
|
|
|
use Illuminate\Support\Facades\Cache;
|
|
|
|
/**
|
|
* Tracks paired gateways — their capacity, status, and last heartbeat.
|
|
*
|
|
* $registry = app(GatewayRegistry::class);
|
|
* $registry->register('charon', [...]);
|
|
* $gateway = $registry->get('charon');
|
|
* $live = $registry->liveGateways('proxy');
|
|
*/
|
|
class GatewayRegistry
|
|
{
|
|
private const TTL = 300; // 5 min — gateway must heartbeat within this
|
|
|
|
public function register(string $name, array $data): void
|
|
{
|
|
$entry = [
|
|
'name' => $name,
|
|
'capabilities' => $data['capabilities'] ?? [],
|
|
'region' => $data['region'] ?? 'unknown',
|
|
'bandwidth_mbps' => $data['bandwidth_mbps'] ?? 0,
|
|
'max_connections' => $data['max_connections'] ?? 0,
|
|
'current_connections' => 0,
|
|
'current_load' => 0,
|
|
'wireguard_endpoint' => $data['wireguard_endpoint'] ?? '',
|
|
'proxy_endpoint' => $data['proxy_endpoint'] ?? '',
|
|
'paired_at' => now()->toIso8601String(),
|
|
'last_heartbeat' => now()->toIso8601String(),
|
|
'status' => 'online',
|
|
];
|
|
|
|
Cache::put("gw:node:{$name}", $entry, self::TTL);
|
|
|
|
// Track in the live gateway list
|
|
$list = Cache::get('gw:live_list', []);
|
|
$list[$name] = true;
|
|
Cache::put('gw:live_list', $list, 86400);
|
|
}
|
|
|
|
public function heartbeat(string $name, array $stats = []): bool
|
|
{
|
|
$entry = Cache::get("gw:node:{$name}");
|
|
if (! $entry) {
|
|
return false;
|
|
}
|
|
|
|
$entry['last_heartbeat'] = now()->toIso8601String();
|
|
$entry['current_connections'] = $stats['connections'] ?? $entry['current_connections'];
|
|
$entry['current_load'] = $stats['load'] ?? $entry['current_load'];
|
|
$entry['bytes_served'] = ($entry['bytes_served'] ?? 0) + ($stats['bytes_since_last'] ?? 0);
|
|
$entry['status'] = 'online';
|
|
|
|
Cache::put("gw:node:{$name}", $entry, self::TTL);
|
|
|
|
return true;
|
|
}
|
|
|
|
public function get(string $name): ?array
|
|
{
|
|
return Cache::get("gw:node:{$name}");
|
|
}
|
|
|
|
/**
|
|
* Get all live (heartbeat within TTL) gateways, optionally filtered by capability.
|
|
*/
|
|
public function liveGateways(string $capability = ''): array
|
|
{
|
|
$list = Cache::get('gw:live_list', []);
|
|
$result = [];
|
|
|
|
$pruned = false;
|
|
foreach (array_keys($list) as $name) {
|
|
$entry = Cache::get("gw:node:{$name}");
|
|
if (! $entry) {
|
|
unset($list[$name]);
|
|
$pruned = true;
|
|
continue; // expired — gateway missed heartbeat
|
|
}
|
|
|
|
if ($capability && ! in_array($capability, $entry['capabilities'] ?? [])) {
|
|
continue;
|
|
}
|
|
|
|
$result[] = $entry;
|
|
}
|
|
|
|
// Prune expired entries from the live list
|
|
if ($pruned) {
|
|
Cache::put('gw:live_list', $list, 86400);
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Select best gateway for a connection — least loaded first.
|
|
*/
|
|
public function selectBest(string $capability): ?array
|
|
{
|
|
$gateways = $this->liveGateways($capability);
|
|
|
|
if (empty($gateways)) {
|
|
return null;
|
|
}
|
|
|
|
// Sort by current load (ascending)
|
|
usort($gateways, fn ($a, $b) => ($a['current_load'] ?? 0) <=> ($b['current_load'] ?? 0));
|
|
|
|
return $gateways[0];
|
|
}
|
|
}
|