Skip to content

IdleReaper

Applies to: Hub (phlix-hub)

Periodic cleanup of stale server tunnels that have exceeded the idle threshold without receiving any frames.

Overview

The IdleReaper runs on a configurable interval (default 60 seconds) and checks each tunnel's lastFrameAt timestamp. Tunnels idle for longer than the stale threshold (default 90 seconds) are closed with reason "timeout" and removed from the TunnelManager.

TunnelManager → [IdleReaper] → closeTunnel(serverId, 'timeout')

                  Workerman Timer

IdleReaper

Phlix\Hub\Relay\IdleReaper

Construction

php
use Phlix\Hub\Relay\IdleReaper;
use Phlix\Hub\Relay\TunnelManager;
use Phlix\Hub\Common\Logger\StructuredLogger;

$reaper = new IdleReaper(
    tunnelManager: $manager,
    logger: $logger,
    intervalSeconds: 60,           // Optional, default 60
    staleThresholdSeconds: 90,       // Optional, default 90
);

Configuration

ParameterTypeDefaultDescription
intervalSecondsint60Interval between reaper scans.
staleThresholdSecondsint90Seconds without frames before a tunnel is considered stale.

Constants

ConstantValueDescription
DEFAULT_INTERVAL_SECONDS60Default scan interval.
DEFAULT_STALE_THRESHOLD_SECONDS90Default stale threshold.

Lifecycle

start(): int

Register a Workerman Timer that calls tick() every $intervalSeconds. The timer persists until the worker stops.

php
$timerId = $reaper->start();

// $timerId can be passed to Timer::del() to cancel (not exposed by IdleReaper itself)

Returns the Workerman timer ID.

tick(): int

Perform a single reaper scan. Public so it can be called directly by tests or manually triggered.

php
$reapedCount = $reaper->tick();
// Returns number of tunnels closed (0 if none were stale)

Behavior:

  1. Iterates all active tunnels via TunnelManager::allTunnels().
  2. Collects tunnels where $tunnel->isStale($threshold) returns true.
  3. Calls $tunnelManager->closeTunnel($serverId, 'timeout') for each stale tunnel.

Note: Tunnels are collected first to avoid concurrent modification during iteration when closeTunnel() modifies the tunnel collection.

php
// Manual trigger (e.g., from a test)
$reaper = new IdleReaper($manager, $logger, 60, 90);
$reaper->tick();

Getters

php
$reaper->getIntervalSeconds();         // e.g., 60
$reaper->getStaleThresholdSeconds(); // e.g., 90

Integration with TunnelManager

The IdleReaper is wired into the tunnel lifecycle at startup:

php
use Phlix\Hub\Relay\IdleReaper;
use Phlix\Hub\Relay\TunnelManager;
use React\EventLoop\Loop;

$manager = new TunnelManager($sessionManager, $codec, $logger);
$reaper = new IdleReaper(
    tunnelManager: $manager,
    logger: $logger,
    intervalSeconds: 60,
    staleThresholdSeconds: 90,
);

// Start reaper — timer runs until worker stops
$reaper->start();

The reaper does not expose a stop() method. The timer persists for the lifetime of the worker process. To restart with new settings, create a new IdleReaper instance with updated configuration.

Interaction with Tunnel isStale()

IdleReaper::tick() delegates staleness detection to Tunnel::isStale():

php
// Tunnel.php
public function isStale(int $staleThresholdSeconds = 90): bool
{
    return (time() - $this->lastFrameAt) > $staleThresholdSeconds;
}

A tunnel with lastFrameAt older than the threshold is considered stale and eligible for reaping.

Error Handling

ScenarioBehavior
No stale tunnelstick() returns 0, no log entries.
All tunnels activetick() returns 0.
Multiple stale tunnelsAll are closed in a single pass.
Empty tunnel listtick() returns 0.

Each reaped tunnel logs at INFO level:

php
$this->logger->info('Relay: reaping stale tunnel', [
    'server_id' => $serverId,
    'tunnel_id' => $tunnel->getTunnelId(),
    'last_frame_at' => $tunnel->getLastFrameAt(),
    'stale_threshold_seconds' => $this->staleThresholdSeconds,
    'reason' => 'timeout',
]);

A summary is logged when any tunnels were reaped:

php
$this->logger->info('Relay: idle reaper scan complete', [
    'reaped_count' => $reapedCount,
]);

BSD-3-Clause