Milestone 5: Resiliency & Advanced State
This commit is contained in:
parent
26db488032
commit
a898a814ec
53
src/DispatchFailed.php
Normal file
53
src/DispatchFailed.php
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Phred\Beacon;
|
||||||
|
|
||||||
|
use Phred\BeaconContracts\DispatchFailedInterface;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DispatchFailed
|
||||||
|
*
|
||||||
|
* Internal event triggered when a listener throws an exception during dispatch.
|
||||||
|
* Used only when ResiliencyMode::CONTINUE is enabled.
|
||||||
|
*/
|
||||||
|
class DispatchFailed implements DispatchFailedInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param Throwable $exception The exception thrown by the listener.
|
||||||
|
* @param callable $listener The listener that failed.
|
||||||
|
* @param object $event The original event being dispatched.
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
private readonly Throwable $exception,
|
||||||
|
private readonly mixed $listener, // Mixed because it could be an object, closure, or array
|
||||||
|
private readonly object $event
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Throwable
|
||||||
|
*/
|
||||||
|
public function getException(): Throwable
|
||||||
|
{
|
||||||
|
return $this->exception;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return callable
|
||||||
|
*/
|
||||||
|
public function getListener(): callable
|
||||||
|
{
|
||||||
|
return is_callable($this->listener) ? $this->listener : fn() => null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return object
|
||||||
|
*/
|
||||||
|
public function getEvent(): object
|
||||||
|
{
|
||||||
|
return $this->event;
|
||||||
|
}
|
||||||
|
}
|
||||||
46
src/EventEnvelope.php
Normal file
46
src/EventEnvelope.php
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Phred\Beacon;
|
||||||
|
|
||||||
|
use Phred\BeaconContracts\EventEnvelopeInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* EventEnvelope
|
||||||
|
*
|
||||||
|
* A wrapper to carry immutable metadata (context) along with an event.
|
||||||
|
* Useful for traceability and cross-cutting concerns like request IDs.
|
||||||
|
*/
|
||||||
|
class EventEnvelope implements EventEnvelopeInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param object $event The original event.
|
||||||
|
* @param array<string, mixed> $context Immutable metadata associated with the event.
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
private readonly object $event,
|
||||||
|
private readonly array $context = []
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the underlying event.
|
||||||
|
*
|
||||||
|
* @return object
|
||||||
|
*/
|
||||||
|
public function getEvent(): object
|
||||||
|
{
|
||||||
|
return $this->event;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the context map.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function getContext(): array
|
||||||
|
{
|
||||||
|
return $this->context;
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/ResiliencyMode.php
Normal file
23
src/ResiliencyMode.php
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Phred\Beacon;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ResiliencyMode
|
||||||
|
*
|
||||||
|
* Defines how the dispatcher handles exceptions thrown by listeners.
|
||||||
|
*/
|
||||||
|
enum ResiliencyMode: string
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Stop dispatching immediately when a listener throws an exception.
|
||||||
|
*/
|
||||||
|
case FAIL_FAST = 'fail_fast';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log/Report the error and continue to the next listener.
|
||||||
|
*/
|
||||||
|
case CONTINUE = 'continue';
|
||||||
|
}
|
||||||
72
tests/Unit/ResiliencyAndStateTest.php
Normal file
72
tests/Unit/ResiliencyAndStateTest.php
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Phred\Beacon\Tests\Unit;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Phred\Beacon\EventDispatcher;
|
||||||
|
use Phred\Beacon\ListenerProvider;
|
||||||
|
use Phred\Beacon\ResiliencyMode;
|
||||||
|
use Phred\Beacon\DispatchFailed;
|
||||||
|
use Phred\Beacon\EventEnvelope;
|
||||||
|
|
||||||
|
class ResiliencyAndStateTest extends TestCase
|
||||||
|
{
|
||||||
|
public function test_it_throws_exception_in_fail_fast_mode(): void
|
||||||
|
{
|
||||||
|
$provider = new ListenerProvider();
|
||||||
|
$dispatcher = new EventDispatcher($provider);
|
||||||
|
$dispatcher->setResiliencyMode(ResiliencyMode::FAIL_FAST);
|
||||||
|
|
||||||
|
$provider->addListener('stdClass', function() {
|
||||||
|
throw new \RuntimeException('Test Error');
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->expectException(\RuntimeException::class);
|
||||||
|
$this->expectExceptionMessage('Test Error');
|
||||||
|
|
||||||
|
$dispatcher->dispatch(new \stdClass());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_continues_and_reports_in_continue_mode(): void
|
||||||
|
{
|
||||||
|
$provider = new ListenerProvider();
|
||||||
|
$dispatcher = new EventDispatcher($provider);
|
||||||
|
$dispatcher->setResiliencyMode(ResiliencyMode::CONTINUE);
|
||||||
|
|
||||||
|
$failedCount = 0;
|
||||||
|
$listenersRun = 0;
|
||||||
|
|
||||||
|
// Listener that fails
|
||||||
|
$provider->addListener('stdClass', function() {
|
||||||
|
throw new \RuntimeException('Test Error');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Next listener that should still run
|
||||||
|
$provider->addListener('stdClass', function() use (&$listenersRun) {
|
||||||
|
$listenersRun++;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Error reporter listener
|
||||||
|
$provider->addListener(DispatchFailed::class, function(DispatchFailed $event) use (&$failedCount) {
|
||||||
|
$failedCount++;
|
||||||
|
$this->assertEquals('Test Error', $event->getException()->getMessage());
|
||||||
|
});
|
||||||
|
|
||||||
|
$dispatcher->dispatch(new \stdClass());
|
||||||
|
|
||||||
|
$this->assertEquals(1, $listenersRun);
|
||||||
|
$this->assertEquals(1, $failedCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_event_envelope_carries_context(): void
|
||||||
|
{
|
||||||
|
$event = new \stdClass();
|
||||||
|
$context = ['request_id' => '123'];
|
||||||
|
$envelope = new EventEnvelope($event, $context);
|
||||||
|
|
||||||
|
$this->assertSame($event, $envelope->getEvent());
|
||||||
|
$this->assertEquals($context, $envelope->getContext());
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue