kissmint3395 / aegis
A PHP 8.2+ resilience library combining Retry, Circuit Breaker, Timeout, and more into a composable pipeline. Inspired by Resilience4j and .NET Polly.
Requires
- php: ^8.2
- psr/event-dispatcher: ^1.0
- psr/simple-cache: ^3.0
Requires (Dev)
- phpstan/extension-installer: ^1.3
- phpstan/phpstan: ^1.10
- phpstan/phpstan-strict-rules: ^1.5
- phpunit/phpunit: ^11.0
This package is auto-updated.
Last update: 2026-04-28 07:33:55 UTC
README
A PHP 8.2+ resilience library that combines Retry, Circuit Breaker, Timeout, Rate Limiter, and Bulkhead into a single composable pipeline.
Inspired by Resilience4j and .NET Polly — the PHP ecosystem's missing equivalent.
$pipeline = ResiliencePipeline::builder() ->circuitBreaker('payment-api', failureThreshold: 5) ->retry(maxAttempts: 3, backoff: ExponentialBackoff::withJitter(Duration::milliseconds(100))) ->timeout(Duration::seconds(5)) ->build(); $result = $pipeline->execute(fn() => $httpClient->post('/charge', $payload));
Why Aegis?
PHP has several individual resilience libraries, but none that combine them into a composable pipeline:
| Library | Retry | Circuit Breaker | Timeout | Rate Limiter | Bulkhead | Composable | PHP 8.2+ |
|---|---|---|---|---|---|---|---|
ackintosh/ganesha |
✗ | ✅ | ✗ | ✗ | ✗ | ✗ | ✅ |
yohang/finite |
✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✅ |
cline/retry |
✅ | ✗ | ✗ | ✗ | ✗ | ✗ | ✅ |
| Aegis | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
Requirements
- PHP 8.2+
psr/event-dispatcher^1.0psr/simple-cache^3.0 (optional, for persistent Circuit Breaker, Rate Limiter, and Bulkhead state)
Installation
composer require kissmint3395/aegis
Usage
Retry
Retry a failing operation with configurable backoff.
use Aegis\ResiliencePipeline; use Aegis\Backoff\ExponentialBackoff; use Aegis\Duration; $pipeline = ResiliencePipeline::builder() ->retry( maxAttempts: 3, backoff: ExponentialBackoff::withJitter(Duration::milliseconds(100)), retryOn: [\RuntimeException::class], ) ->build(); $result = $pipeline->execute(fn() => $api->fetch());
Backoff strategies:
use Aegis\Backoff\FixedBackoff; use Aegis\Backoff\ExponentialBackoff; // Fixed delay new FixedBackoff(Duration::milliseconds(200)) // Exponential: 100ms → 200ms → 400ms → ... ExponentialBackoff::create(Duration::milliseconds(100)) // Exponential with jitter: randomised between 50%–100% of each step ExponentialBackoff::withJitter(Duration::milliseconds(100), maxDelay: Duration::seconds(5))
Conditional retry:
->retry( retryIf: fn(\Throwable $e) => $e->getCode() >= 500, )
Circuit Breaker
Stop cascading failures by blocking calls when a service is unhealthy.
use Aegis\ResiliencePipeline; use Aegis\Duration; $pipeline = ResiliencePipeline::builder() ->circuitBreaker( name: 'inventory-api', failureThreshold: 5, // Open after 5 consecutive failures successThreshold: 2, // Close after 2 consecutive successes in HalfOpen resetAfter: Duration::seconds(30), ) ->build();
State transitions:
Closed ──(5 failures)──► Open ──(30s elapsed)──► HalfOpen
▲ │
└──────────(2 successes)───────────────────────────┘
(1 failure) ──► Open
Persistent state across requests (Redis, APCu, etc.):
use Aegis\Strategy\CircuitBreaker\Storage\Psr16Storage; $pipeline = ResiliencePipeline::builder() ->circuitBreaker('payment-api', storage: new Psr16Storage($redisCache)) ->build();
Ignore specific exceptions (e.g. validation errors should not trip the circuit):
->circuitBreaker('api', ignoreExceptions: [\InvalidArgumentException::class])
Timeout
Enforce a maximum execution duration.
->timeout(Duration::seconds(5))
Note: On Unix systems with the
pcntlextension, Aegis usesSIGALRMfor true preemptive interruption. On Windows and environments withoutpcntl, elapsed time is checked after execution — useful for limiting total retry budgets.
Rate Limiter
Limit the number of calls within a fixed time window.
use Aegis\ResiliencePipeline; use Aegis\Duration; $pipeline = ResiliencePipeline::builder() ->rateLimit( name: 'payment-api', limit: 100, // Max 100 calls per window window: Duration::seconds(60), ) ->build();
Persistent rate limiting across requests (Redis, APCu, etc.):
use Aegis\Strategy\RateLimiter\Storage\Psr16Storage; $pipeline = ResiliencePipeline::builder() ->rateLimit('payment-api', limit: 100, storage: new Psr16Storage($redisCache)) ->build();
Note: The default
InMemoryStorageis process-scoped. For rate limiting across PHP-FPM workers, usePsr16Storagebacked by Redis or APCu.
Bulkhead
Limit the number of concurrent executions to prevent resource exhaustion.
use Aegis\ResiliencePipeline; $pipeline = ResiliencePipeline::builder() ->bulkhead( name: 'database', maxConcurrent: 10, // Allow at most 10 concurrent calls ) ->build();
Persistent concurrency tracking across requests (Redis, APCu, etc.):
use Aegis\Strategy\Bulkhead\Storage\Psr16Storage; $pipeline = ResiliencePipeline::builder() ->bulkhead('database', maxConcurrent: 10, storage: new Psr16Storage($redisCache)) ->build();
Note: The default
InMemoryStorageis process-scoped. For cross-worker concurrency limiting, usePsr16Storagebacked by Redis or APCu.
Composing strategies
Strategies wrap each other in the order they are added (first = outermost). The recommended order is: Bulkhead → Rate Limiter → Timeout → Circuit Breaker → Retry.
$pipeline = ResiliencePipeline::builder() ->bulkhead('svc', maxConcurrent: 10) // 1. Concurrency gate ->rateLimit('svc', limit: 100) // 2. Rate gate ->timeout(Duration::seconds(10)) // 3. Total time budget ->circuitBreaker('svc', failureThreshold: 5) // 4. Block if unhealthy ->retry(maxAttempts: 3) // 5. Retry transient failures ->build();
PSR-14 Events
Observe what happens inside the pipeline by wiring up a PSR-14 event dispatcher.
use Aegis\Event\RetryAttempted; use Aegis\Event\CircuitOpened; use Aegis\Event\CircuitClosed; use Aegis\Event\CircuitHalfOpened; use Aegis\Event\RateLimitExceeded; use Aegis\Event\BulkheadRejected; $pipeline = ResiliencePipeline::builder() ->withEventDispatcher($dispatcher) ->circuitBreaker('api') ->retry(maxAttempts: 3) ->build(); // Example: log every retry attempt // Note: listener registration API (listen/addListener/subscribeTo) depends on your PSR-14 implementation. $dispatcher->listen(RetryAttempted::class, function (RetryAttempted $e) use ($logger): void { $logger->warning('Retry attempt', [ 'attempt' => $e->attempt, 'max' => $e->maxAttempts, 'delay_ms' => $e->delayMs, 'error' => $e->cause->getMessage(), ]); });
| Event | Fired when |
|---|---|
RetryAttempted |
A retry is about to be delayed and re-attempted |
CircuitOpened |
Circuit transitions Closed → Open |
CircuitClosed |
Circuit transitions HalfOpen → Closed |
CircuitHalfOpened |
Circuit transitions Open → HalfOpen |
RateLimitExceeded |
A call is rejected because the rate limit is reached |
BulkheadRejected |
A call is rejected because the bulkhead is full |
Custom strategies
Implement StrategyInterface to plug in your own logic.
use Aegis\Contract\StrategyInterface; final class LoggingStrategy implements StrategyInterface { public function __construct(private readonly LoggerInterface $logger) {} public function execute(callable $next): mixed { $start = microtime(true); try { $result = $next(); $this->logger->info('OK', ['ms' => (int)((microtime(true) - $start) * 1000)]); return $result; } catch (\Throwable $e) { $this->logger->error($e->getMessage()); throw $e; } } } $pipeline = ResiliencePipeline::builder() ->addStrategy(new LoggingStrategy($logger)) ->retry(maxAttempts: 3) ->build();
Exceptions
| Exception | Thrown when |
|---|---|
RetryExhaustedException |
All retry attempts failed. getPrevious() returns the last cause. |
CircuitOpenException |
A call is made while the circuit is Open. |
TimeoutExceededException |
Execution exceeded the configured duration. |
RateLimitExceededException |
The rate limit for the window has been reached. |
BulkheadFullException |
The maximum number of concurrent calls is already reached. |
PHPStan integration
Aegis ships with a PHPStan rule that catches misuse at analysis time.
Rule: retryOn must contain Throwable subclasses
// PHPStan error: "stdClass" does not implement Throwable new RetryOptions(retryOn: [\stdClass::class]);
Install the extension via phpstan/extension-installer (automatic) or add manually:
# phpstan.neon includes: - vendor/kissmint3395/aegis/phpstan/extension.neon
Development
composer install # Tests ./vendor/bin/phpunit # Static analysis ./vendor/bin/phpstan analyse
Roadmap
- Rate Limiter
- Bulkhead (concurrency limiting)
- PHPStan 2.x upgrade
- Fallback strategy
License
MIT
日本語ドキュメント
PHP 8.2+ 向けのレジリエンスライブラリです。リトライ・サーキットブレーカー・タイムアウト・レートリミッター・バルクヘッドを単一のコンポーザブルなパイプラインとして組み合わせられます。
Resilience4j(Java)や.NET Polly に相当するものが PHP エコシステムに存在しなかったため作成しました。
$pipeline = ResiliencePipeline::builder() ->circuitBreaker('payment-api', failureThreshold: 5) ->retry(maxAttempts: 3, backoff: ExponentialBackoff::withJitter(Duration::milliseconds(100))) ->timeout(Duration::seconds(5)) ->build(); $result = $pipeline->execute(fn() => $httpClient->post('/charge', $payload));
なぜ Aegis?
PHP には個別のレジリエンスライブラリが存在しますが、それらをパイプラインとして合成できるものはありませんでした。
| ライブラリ | リトライ | サーキットブレーカー | タイムアウト | レートリミッター | バルクヘッド | 合成可能 | PHP 8.2+ |
|---|---|---|---|---|---|---|---|
ackintosh/ganesha |
✗ | ✅ | ✗ | ✗ | ✗ | ✗ | ✅ |
yohang/finite |
✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✅ |
cline/retry |
✅ | ✗ | ✗ | ✗ | ✗ | ✗ | ✅ |
| Aegis | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
要件
- PHP 8.2+
psr/event-dispatcher^1.0psr/simple-cache^3.0 (任意。サーキットブレーカー・レートリミッター・バルクヘッドの状態を永続化する場合)
インストール
composer require kissmint3395/aegis
使い方
リトライ
失敗した処理をバックオフ戦略付きで再試行します。
use Aegis\ResiliencePipeline; use Aegis\Backoff\ExponentialBackoff; use Aegis\Duration; $pipeline = ResiliencePipeline::builder() ->retry( maxAttempts: 3, // 最大試行回数(初回 + リトライ) backoff: ExponentialBackoff::withJitter(Duration::milliseconds(100)), // ジッター付き指数バックオフ retryOn: [\RuntimeException::class], // リトライ対象の例外 ) ->build(); $result = $pipeline->execute(fn() => $api->fetch());
バックオフ戦略一覧:
use Aegis\Backoff\FixedBackoff; use Aegis\Backoff\ExponentialBackoff; // 固定遅延 new FixedBackoff(Duration::milliseconds(200)) // 指数バックオフ: 100ms → 200ms → 400ms → ... ExponentialBackoff::create(Duration::milliseconds(100)) // ジッター付き指数バックオフ: 各ステップの 50〜100% をランダムで選択 ExponentialBackoff::withJitter(Duration::milliseconds(100), maxDelay: Duration::seconds(5))
条件付きリトライ:
->retry( retryIf: fn(\Throwable $e) => $e->getCode() >= 500, // HTTP 5xx のみリトライ )
サーキットブレーカー
サービスが不健全なときに呼び出しをブロックし、カスケード障害を防ぎます。
use Aegis\ResiliencePipeline; use Aegis\Duration; $pipeline = ResiliencePipeline::builder() ->circuitBreaker( name: 'inventory-api', failureThreshold: 5, // 5 回連続失敗でオープン successThreshold: 2, // HalfOpen で 2 回連続成功でクローズ resetAfter: Duration::seconds(30), // オープン後 30 秒でリセット試行 ) ->build();
状態遷移:
Closed ──(5回失敗)──► Open ──(30秒経過)──► HalfOpen
▲ │
└──────────(2回成功)────────────────────────────┘
(1回失敗)──► Open
| 状態 | 意味 |
|---|---|
| Closed | 通常運転。失敗をカウント |
| Open | 全呼び出しを即座にブロック(CircuitOpenException) |
| HalfOpen | 回復を試行中。限定的に呼び出しを通す |
リクエスト間で状態を永続化(Redis・APCu など):
use Aegis\Strategy\CircuitBreaker\Storage\Psr16Storage; $pipeline = ResiliencePipeline::builder() ->circuitBreaker('payment-api', storage: new Psr16Storage($redisCache)) ->build();
特定の例外をカウント対象外にする(例:バリデーションエラーは障害扱いしない):
->circuitBreaker('api', ignoreExceptions: [\InvalidArgumentException::class])
タイムアウト
処理の最大実行時間を設定します。
->timeout(Duration::seconds(5))
注意:
pcntl拡張が利用可能な Unix 環境ではSIGALRMによるプリエンプティブな割り込みを行います。Windows やpcntlが使えない環境では、実行後に経過時間をチェックする方式にフォールバックします(リトライ全体の時間制限として機能します)。
レートリミッター
固定ウィンドウ内の呼び出し回数を制限します。
use Aegis\ResiliencePipeline; use Aegis\Duration; $pipeline = ResiliencePipeline::builder() ->rateLimit( name: 'payment-api', limit: 100, // ウィンドウあたり最大 100 回 window: Duration::seconds(60), ) ->build();
リクエスト間で状態を永続化(Redis・APCu など):
use Aegis\Strategy\RateLimiter\Storage\Psr16Storage; $pipeline = ResiliencePipeline::builder() ->rateLimit('payment-api', limit: 100, storage: new Psr16Storage($redisCache)) ->build();
注意: デフォルトの
InMemoryStorageはプロセス単位のスコープです。PHP-FPM の複数ワーカーをまたいでレート制限するには、Redis や APCu をバックエンドとしたPsr16Storageを使用してください。
バルクヘッド
同時実行数を制限してリソース枯渇を防ぎます。
use Aegis\ResiliencePipeline; $pipeline = ResiliencePipeline::builder() ->bulkhead( name: 'database', maxConcurrent: 10, // 最大 10 並行まで許可 ) ->build();
リクエスト間で状態を永続化(Redis・APCu など):
use Aegis\Strategy\Bulkhead\Storage\Psr16Storage; $pipeline = ResiliencePipeline::builder() ->bulkhead('database', maxConcurrent: 10, storage: new Psr16Storage($redisCache)) ->build();
注意: デフォルトの
InMemoryStorageはプロセス単位のスコープです。複数ワーカーをまたいだ同時実行数の制御にはPsr16Storageを使用してください。
戦略の合成
戦略は追加した順に外側から適用されます(最初に追加 = 最も外側)。 推奨順序は Bulkhead → Rate Limiter → Timeout → Circuit Breaker → Retry です。
$pipeline = ResiliencePipeline::builder() ->bulkhead('svc', maxConcurrent: 10) // 1. 同時実行数ゲート ->rateLimit('svc', limit: 100) // 2. レートゲート ->timeout(Duration::seconds(10)) // 3. 全体の時間制限 ->circuitBreaker('svc', failureThreshold: 5) // 4. 不健全なら即ブロック ->retry(maxAttempts: 3) // 5. 一時的な失敗をリトライ ->build();
PSR-14 イベント
PSR-14 のイベントディスパッチャーを接続して、パイプライン内部の動作を観測できます。
use Aegis\Event\RetryAttempted; use Aegis\Event\CircuitOpened; use Aegis\Event\CircuitClosed; use Aegis\Event\CircuitHalfOpened; use Aegis\Event\RateLimitExceeded; use Aegis\Event\BulkheadRejected; $pipeline = ResiliencePipeline::builder() ->withEventDispatcher($dispatcher) ->circuitBreaker('api') ->retry(maxAttempts: 3) ->build(); // リトライ発生時にログを記録する例 // 注意: リスナー登録の API(listen / addListener / subscribeTo)は PSR-14 実装によって異なります $dispatcher->listen(RetryAttempted::class, function (RetryAttempted $e) use ($logger): void { $logger->warning('リトライ実行', [ 'attempt' => $e->attempt, 'max' => $e->maxAttempts, 'delay_ms' => $e->delayMs, 'error' => $e->cause->getMessage(), ]); });
| イベント | 発火タイミング |
|---|---|
RetryAttempted |
リトライ待機前 |
CircuitOpened |
Closed → Open に遷移したとき |
CircuitClosed |
HalfOpen → Closed に遷移したとき |
CircuitHalfOpened |
Open → HalfOpen に遷移したとき |
RateLimitExceeded |
レート制限に達して呼び出しを拒否したとき |
BulkheadRejected |
バルクヘッドが満杯で呼び出しを拒否したとき |
カスタム戦略
StrategyInterface を実装して独自の戦略を追加できます。
use Aegis\Contract\StrategyInterface; final class LoggingStrategy implements StrategyInterface { public function __construct(private readonly LoggerInterface $logger) {} public function execute(callable $next): mixed { $start = microtime(true); try { $result = $next(); $this->logger->info('成功', ['ms' => (int)((microtime(true) - $start) * 1000)]); return $result; } catch (\Throwable $e) { $this->logger->error($e->getMessage()); throw $e; } } } $pipeline = ResiliencePipeline::builder() ->addStrategy(new LoggingStrategy($logger)) ->retry(maxAttempts: 3) ->build();
例外一覧
| 例外 | 発生タイミング |
|---|---|
RetryExhaustedException |
全リトライが失敗。getPrevious() で最後の原因を取得可能 |
CircuitOpenException |
サーキットがオープン状態のときに呼び出しを行った |
TimeoutExceededException |
設定した時間内に処理が完了しなかった |
RateLimitExceededException |
ウィンドウ内のレート制限に達した |
BulkheadFullException |
最大同時実行数に達している |
PHPStan 連携
Aegis には静的解析時に誤用を検出する PHPStan ルールが同梱されています。
ルール: retryOn には Throwable のサブクラスのみ指定可能
// PHPStan エラー: "stdClass" は Throwable を実装していない new RetryOptions(retryOn: [\stdClass::class]);
phpstan/extension-installer 経由で自動的に有効化されます。手動で追加する場合:
# phpstan.neon includes: - vendor/kissmint3395/aegis/phpstan/extension.neon
開発
composer install # テスト実行 ./vendor/bin/phpunit # 静的解析 ./vendor/bin/phpstan analyse
ロードマップ
- Rate Limiter(レート制限)
- Bulkhead(同時実行数制限)
- PHPStan 2.x 対応
- Fallback(フォールバック)戦略