Винятки

Ієрархія

TrueAsync визначає спеціалізовану ієрархію винятків для різних типів помилок:

\Cancellation                              -- базовий клас скасування (нарівні з \Error та \Exception)
+-- Async\AsyncCancellation                -- скасування корутини
    +-- Async\OperationCanceledException   -- операцію перервано токеном скасування

\Error
+-- Async\DeadlockError                    -- виявлено взаємне блокування

\Exception
+-- Async\AsyncException                   -- загальна помилка асинхронної операції
|   +-- Async\ServiceUnavailableException  -- сервіс недоступний (circuit breaker)
+-- Async\InputOutputException             -- помилка вводу/виводу
+-- Async\DnsException                     -- помилка розв'язання DNS
+-- Async\TimeoutException                 -- тайм-аут операції
+-- Async\PollException                    -- помилка операції poll
+-- Async\ChannelException                 -- помилка каналу
+-- Async\PoolException                    -- помилка пулу ресурсів
+-- Async\CompositeException               -- контейнер для кількох винятків

AsyncCancellation

class Async\AsyncCancellation extends \Cancellation {}

Кидається при скасуванні корутини. \Cancellation – це третій кореневий клас Throwable нарівні з \Error та \Exception, тому звичайні блоки catch (\Exception $e) та catch (\Error $e) не перехоплюють скасування випадково.

<?php
use Async\AsyncCancellation;
use function Async\spawn;
use function Async\await;
use function Async\delay;

$coroutine = spawn(function() {
    try {
        delay(10000);
    } catch (AsyncCancellation $e) {
        // Коректно завершуємо роботу
        echo "Cancelled: " . $e->getMessage() . "\n";
    }
});

delay(100);
$coroutine->cancel();
?>

Важливо: Не перехоплюйте AsyncCancellation через catch (\Throwable $e) без повторного кидання – це порушує механізм кооперативного скасування.

OperationCanceledException

class Async\OperationCanceledException extends Async\AsyncCancellation {}

Викидається, коли очікувана операція переривається токеном скасування (cancellation token). Оригінальний виняток із токена доступний через $previous. Це дозволяє відрізнити спрацювання токена від винятку, кинутого самим awaitable-об’єктом.

Зачіпає всі операції з токеном скасування: await(), await_*(), Future::await(), Channel::send()/recv(), Scope::awaitCompletion().

<?php
use Async\OperationCanceledException;
use function Async\spawn;
use function Async\await;
use function Async\timeout;
use function Async\delay;

$coroutine = spawn(function() {
    delay(10000);
    return "result";
});

try {
    await($coroutine, timeout(1000));
} catch (OperationCanceledException $e) {
    // Токен скасування спрацював
    echo "Операцію перервано токеном\n";
    echo "Причина: " . $e->getPrevious()?->getMessage() . "\n";
} catch (\Exception $e) {
    // Помилка самої корутини
    echo "Помилка: " . $e->getMessage() . "\n";
}
?>

DeadlockError

class Async\DeadlockError extends \Error {}

Кидається, коли планувальник виявляє взаємне блокування – ситуацію, коли корутини чекають одна на одну і жодна не може продовжити виконання.

<?php
use function Async\spawn;
use function Async\await;

// Класичне взаємне блокування: дві корутини чекають одна на одну
$c1 = spawn(function() use (&$c2) {
    await($c2); // чекає на c2
});

$c2 = spawn(function() use (&$c1) {
    await($c1); // чекає на c1
});
// DeadlockError: A deadlock was detected
?>

Приклад, коли корутина очікує саму себе:

<?php
use function Async\spawn;
use function Async\await;

$coroutine = spawn(function() use (&$coroutine) {
    await($coroutine); // очікує саму себе
});
// DeadlockError
?>

AsyncException

class Async\AsyncException extends \Exception {}

Базовий виняток для загальних помилок асинхронних операцій. Використовується для помилок, які не належать до спеціалізованих категорій.

TimeoutException

class Async\TimeoutException extends \Exception {}

Кидається при перевищенні тайм-ауту всередині корутини. Коли timeout() використовується як токен скасування в await(), кидається OperationCanceledException з TimeoutException у $previous:

<?php
use Async\OperationCanceledException;
use function Async\spawn;
use function Async\await;
use function Async\timeout;
use function Async\delay;

try {
    $coroutine = spawn(function() {
        delay(10000); // Тривала операція
    });
    await($coroutine, timeout(1000)); // Тайм-аут 1 секунда
} catch (OperationCanceledException $e) {
    // Токен скасування спрацював. $e->getPrevious() — TimeoutException.
    echo "Операція не завершилася вчасно\n";
}
?>

InputOutputException

class Async\InputOutputException extends \Exception {}

Загальний виняток для помилок вводу/виводу: сокети, файли, канали та інші дескриптори вводу/виводу.

DnsException

class Async\DnsException extends \Exception {}

Кидається при помилках розв’язання DNS (gethostbyname, gethostbyaddr, gethostbynamel).

PollException

class Async\PollException extends \Exception {}

Кидається при помилках операцій poll на дескрипторах.

ServiceUnavailableException

class Async\ServiceUnavailableException extends Async\AsyncException {}

Кидається, коли circuit breaker перебуває в стані INACTIVE і запит до сервісу відхиляється без спроби виконання.

<?php
use Async\ServiceUnavailableException;

try {
    $resource = $pool->acquire();
} catch (ServiceUnavailableException $e) {
    echo "Service is temporarily unavailable\n";
}
?>

ChannelException

class Async\ChannelException extends Async\AsyncException {}

Кидається при помилках операцій з каналами: відправка в закритий канал, отримання із закритого каналу тощо.

PoolException

class Async\PoolException extends Async\AsyncException {}

Кидається при помилках операцій з пулом ресурсів.

CompositeException

final class Async\CompositeException extends \Exception
{
    public function addException(\Throwable $exception): void;
    public function getExceptions(): array;
}

Контейнер для кількох винятків. Використовується, коли кілька обробників (наприклад, finally в Scope) кидають винятки під час завершення:

<?php
use Async\Scope;
use Async\CompositeException;

$scope = new Scope();

$scope->finally(function() {
    throw new \Exception('Cleanup error 1');
});

$scope->finally(function() {
    throw new \RuntimeException('Cleanup error 2');
});

$scope->setExceptionHandler(function($scope, $coroutine, $exception) {
    if ($exception instanceof CompositeException) {
        echo "Errors: " . count($exception->getExceptions()) . "\n";
        foreach ($exception->getExceptions() as $e) {
            echo "  - " . $e->getMessage() . "\n";
        }
    }
});

$scope->dispose();
// Errors: 2
//   - Cleanup error 1
//   - Cleanup error 2
?>

Рекомендації

Правильна обробка AsyncCancellation

<?php
// Правильно: перехоплюємо конкретні винятки
try {
    await($coroutine);
} catch (\Exception $e) {
    // AsyncCancellation НЕ буде перехоплено тут -- це \Cancellation
    handleError($e);
}
<?php
// Якщо потрібно перехопити все -- завжди повторно кидайте AsyncCancellation
try {
    await($coroutine);
} catch (Async\AsyncCancellation $e) {
    throw $e; // Повторно кидаємо
} catch (\Throwable $e) {
    handleError($e);
}

Захист критичних секцій

Використовуйте protect() для операцій, які не повинні перериватися скасуванням:

<?php
use function Async\protect;

$db->beginTransaction();

protect(function() use ($db) {
    $db->exec("UPDATE accounts SET balance = balance - 100 WHERE id = 1");
    $db->exec("UPDATE accounts SET balance = balance + 100 WHERE id = 2");
    $db->commit();
});

Дивіться також