Skip to content

Instantly share code, notes, and snippets.

@voku
Forked from MrPunyapal/TimeoutGuard.php
Created June 14, 2025 01:38
Show Gist options
  • Save voku/e13e26adfbe473d32b7048940af90de3 to your computer and use it in GitHub Desktop.
Save voku/e13e26adfbe473d32b7048940af90de3 to your computer and use it in GitHub Desktop.
PHP Tick-Based Timeout Wrapper (No pcntl Required)
<?php
// ⏱️ v1 | Simple, automatic timeout with default polling interval
timeoutForEach(5, range(1, 3), static fn (int $value) => doThing());
// ⏱️ v2 | Simple, automatic timeout with default polling interval
$upper = timeoutMap(0.5, ['alice', 'bob', 'charlie'], static fn ($name) => strtoupper($name));
//\PHPStan\dumpType($upper); // Dumped type: list<uppercase-string>
// 👁️ Timeout-aware loop, low CPU overhead
timeout(2.5, static function (TimeoutGuard $guard): void {
foreach (range(1, 10000000) as $i) {
$guard->maybeCheck();
if ($i % 1000000 === 0) {
echo "Processing 👁: {$i}\n";
}
}
});
// 💣 Force stricter interval checks (e.g. every x iterations)
timeout(2.5, static function (TimeoutGuard $guard): void {
foreach (range(1, 10000000) as $i) {
if ($i % 100000 === 0) {
$guard->checkNow();
}
if ($i % 1000000 === 0) {
echo "Processing 💣: {$i}\n";
}
}
});
// 📈 Report diagnostic stats (elapsed time, checks, etc.)
$report = null;
try {
timeout(2.0, static function (TimeoutGuard $guard) use (&$report): void {
foreach (range(1, 10_000_000) as $i) {
$guard->maybeCheck();
if ($i % 2_000_000 === 0) {
echo "Processing 📈: {$i}\n";
}
}
$guard->stop();
$report = $guard->report();
}, 0.001);
echo "✅ Finished within time.\n";
} catch (TimeoutException $e) {
echo "⛔ Timeout: {$e->getMessage()}\n";
}
if ($report !== null) {
echo "\n📋 Timeout Report:\n";
foreach ($report as $key => $value) {
echo " - {$key}: {$value}\n";
}
}
// --------------------
/**
* Run a closure with timeout protection.
*
* @template TClosureResult
*
* @param float $seconds
* @param Closure(TimeoutGuard): TClosureResult $callback
* @param float $checkInterval
*
* @return TClosureResult
*
* @throws TimeoutException
*/
function timeout(
float $seconds,
Closure $callback,
float $checkInterval = 0.01
): mixed {
return TimeoutGuard::run($seconds, $callback, $checkInterval);
}
/**
* Timeout-aware array_map.
*
* @template TIn
* @template TOut
*
* @param float $timeoutSeconds
* @param iterable<TIn> $items
* @param Closure(TIn, int, TimeoutGuard): TOut $callback
* @param float $checkInterval
*
* @return list<TOut>
*
* @throws TimeoutException
*/
function timeoutMap(
float $timeoutSeconds,
iterable $items,
Closure $callback,
float $checkInterval = 0.01
): array {
return TimeoutGuard::run($timeoutSeconds, function (TimeoutGuard $guard) use ($items, $callback): array {
$result = [];
$i = 0;
foreach ($items as $item) {
$guard->maybeCheck();
$result[] = $callback($item, $i++, $guard);
}
return $result;
}, $checkInterval);
}
/**
* Timeout-aware foreach loop.
*
* @template TValue
*
* @param float $timeoutSeconds
* @param iterable<TValue> $items
* @param Closure(TValue, int, TimeoutGuard): void $callback
* @param float $checkInterval
*
* @throws TimeoutException
*/
function timeoutForEach(
float $timeoutSeconds,
iterable $items,
Closure $callback,
float $checkInterval = 0.01
): void {
TimeoutGuard::run($timeoutSeconds, function (TimeoutGuard $guard) use ($items, $callback): void {
$i = 0;
foreach ($items as $item) {
$guard->maybeCheck();
$callback($item, $i++, $guard);
}
}, $checkInterval);
}
// --------------------
final class TimeoutGuard
{
private float $deadline;
private float $startTime;
private float $checkInterval;
private float $nextCheckTime;
private int $checkCount = 0;
private bool $running = false;
private float $finalElapsedTime = 0.0;
private const DEFAULT_CHECK_INTERVAL = 0.01;
private const MIN_CHECK_INTERVAL = 0.001;
/**
* Run a closure with timeout protection.
*
* @template TClosureResult
*
* @param float $timeoutSeconds
* @param Closure(TimeoutGuard): TClosureResult $callback
* @param float $checkIntervalSeconds
*
* @return TClosureResult
*
* @throws TimeoutException
*/
public static function run(
float $timeoutSeconds,
Closure $callback,
float $checkIntervalSeconds = self::DEFAULT_CHECK_INTERVAL
): mixed {
$guard = new self($timeoutSeconds, $checkIntervalSeconds);
try {
return $callback($guard);
} catch (Throwable $e) {
throw $e;
} finally {
$guard->stop();
}
}
private function __construct(
readonly private float $timeoutSeconds,
float $checkIntervalSeconds = self::DEFAULT_CHECK_INTERVAL
) {
if ($timeoutSeconds <= 0) {
throw new RuntimeException('Timeout must be a positive number.');
}
if ($checkIntervalSeconds > $timeoutSeconds) {
throw new RuntimeException('Check interval must not exceed total timeout.');
}
$this->checkInterval = max($checkIntervalSeconds, self::MIN_CHECK_INTERVAL);
$this->startTime = microtime(true);
$this->deadline = $this->startTime + $this->timeoutSeconds;
$this->nextCheckTime = $this->startTime + $this->checkInterval;
$this->running = true;
}
public function stop(): void
{
$this->finalElapsedTime = microtime(true) - $this->startTime;
$this->running = false;
}
public function checkNow(): void
{
if (! $this->running) {
throw new RuntimeException('TimeoutGuard is not running.');
}
$now = microtime(true);
$this->checkCount++;
if ($now >= $this->deadline) {
$elapsed = $now - $this->startTime;
throw new TimeoutException(sprintf('Execution timed out after %.4f seconds', $elapsed));
}
$this->nextCheckTime = $now + $this->checkInterval;
}
public function maybeCheck(): void
{
if (! $this->running) {
throw new RuntimeException('TimeoutGuard is not running.');
}
if (microtime(true) >= $this->nextCheckTime) {
$this->checkNow();
}
}
public function getElapsedTime(): float
{
return $this->running
? microtime(true) - $this->startTime
: $this->finalElapsedTime;
}
public function getCheckCount(): int
{
return $this->checkCount;
}
public function extend(float $seconds): void
{
if (! $this->running) {
throw new RuntimeException('Cannot extend a stopped TimeoutGuard.');
}
if ($seconds <= 0) {
throw new RuntimeException('Extension duration must be positive.');
}
$this->deadline += $seconds;
}
/**
* @return array{
* timeoutSeconds: float,
* elapsedTime: float,
* checkCount: int,
* checkInterval: float,
* running: 'YES'|'NO'
* }
*/
public function report(): array
{
return [
'timeoutSeconds' => $this->timeoutSeconds,
'elapsedTime' => $this->getElapsedTime(),
'checkCount' => $this->checkCount,
'checkInterval' => $this->checkInterval,
'running' => $this->running ? 'YES' : 'NO',
];
}
}
final class TimeoutException extends RuntimeException
{
}
// -----------------
function doThing(): void { sleep(1); }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment