Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added mechanism to designate arbitrary exceptions as recoverable #56

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 41 additions & 10 deletions src/Connector/ImportConnector.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use ScriptFUSION\Porter\Cache\CacheUnavailableException;
use ScriptFUSION\Porter\Connector\Recoverable\RecoverableExceptionHandler;
use ScriptFUSION\Porter\Connector\Recoverable\StatelessRecoverableExceptionHandler;
use ScriptFUSION\Porter\ExceptionDescriptor;

/**
* Connector whose lifecycle is synchronised with an import operation. Ensures correct ConnectionContext is delivered
Expand All @@ -27,21 +28,26 @@ final class ImportConnector implements ConnectorWrapper
*
* @var RecoverableExceptionHandler
*/
private $userReh;
private $userExceptionHandler;

/**
* Resource-defined exception handler called when a recoverable exception is thrown by Connector::fetch().
*
* @var RecoverableExceptionHandler
*/
private $resourceReh;
private $resourceExceptionHandler;

private $maxFetchAttempts;

/**
* @var ExceptionDescriptor[]
*/
private $recoverableExceptionDescriptors = [];

/**
* @param Connector|AsyncConnector $connector Wrapped connector.
* @param ConnectionContext $connectionContext Connection context.
* @param RecoverableExceptionHandler $recoverableExceptionHandler
* @param RecoverableExceptionHandler $recoverableExceptionHandler User's recoverable exception handler.
* @param int $maxFetchAttempts
*/
public function __construct(
Expand All @@ -56,7 +62,7 @@ public function __construct(

$this->connector = clone $connector;
$this->connectionContext = $connectionContext;
$this->userReh = $recoverableExceptionHandler;
$this->userExceptionHandler = $recoverableExceptionHandler;
$this->maxFetchAttempts = $maxFetchAttempts;
}

Expand Down Expand Up @@ -92,20 +98,35 @@ private function createExceptionHandler(): \Closure

return function (\Exception $exception) use (&$userHandlerCloned, &$resourceHandlerCloned): void {
// Throw exception instead of retrying, if unrecoverable.
if (!$exception instanceof RecoverableConnectorException) {
if (!$this->isRecoverable($exception)) {
throw $exception;
}

// Call resource's exception handler, if defined.
if ($this->resourceReh) {
self::invokeHandler($this->resourceReh, $exception, $resourceHandlerCloned);
if ($this->resourceExceptionHandler) {
self::invokeHandler($this->resourceExceptionHandler, $exception, $resourceHandlerCloned);
}

// Call user's exception handler.
self::invokeHandler($this->userReh, $exception, $userHandlerCloned);
self::invokeHandler($this->userExceptionHandler, $exception, $userHandlerCloned);
};
}

private function isRecoverable(\Exception $exception): bool
{
if ($exception instanceof RecoverableConnectorException) {
return true;
}

foreach ($this->recoverableExceptionDescriptors as $exceptionDescriptor) {
if ($exceptionDescriptor->matches($exception)) {
return true;
}
}

return false;
}

/**
* Invokes the specified fetch exception handler, cloning it if required.
*
Expand Down Expand Up @@ -163,10 +184,20 @@ public function findBaseConnector()
*/
public function setRecoverableExceptionHandler(RecoverableExceptionHandler $recoverableExceptionHandler): void
{
if ($this->resourceReh !== null) {
if ($this->resourceExceptionHandler !== null) {
throw new \LogicException('Cannot set resource\'s recoverable exception handler: already set!');
}

$this->resourceReh = $recoverableExceptionHandler;
$this->resourceExceptionHandler = $recoverableExceptionHandler;
}

/**
* Adds the specified exception descriptor, designating it as a recoverable exception type.
*
* @param ExceptionDescriptor $descriptor
*/
public function addRecoverableExceptionDescriptor(ExceptionDescriptor $descriptor): void
{
$this->recoverableExceptionDescriptors[] = $descriptor;
}
}
49 changes: 49 additions & 0 deletions src/ExceptionDescriptor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);

namespace ScriptFUSION\Porter;

/**
* Matches exception attributes against exception instances.
*/
final class ExceptionDescriptor
{
private $type;

private $message;

public function __construct(string $type)
{
$this->type = $type;
}

public function matches(\Exception $exception): bool
{
if (!is_a($exception, $this->type)) {
return false;
}

if ($this->message !== null && $exception->getMessage() !== $this->message) {
return false;
}

return true;
}

public function getType(): string
{
return $this->type;
}

public function setMessage(?string $message): self
{
$this->message = $message;

return $this;
}

public function getMessage(): ?string
{
return $this->message;
}
}
72 changes: 72 additions & 0 deletions test/Integration/Porter/ExceptionDescriptorTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);

namespace ScriptFUSIONTest\Integration\Porter;

use PHPUnit\Framework\TestCase;
use ScriptFUSION\Porter\ExceptionDescriptor;

final class ExceptionDescriptorTest extends TestCase
{
/**
* @dataProvider provideMatches
*/
public function testMatches(ExceptionDescriptor $descriptor, \Exception $exception): void
{
self::assertTrue($descriptor->matches($exception));
}

public function provideMatches(): array
{
return [
'Class match' => [new ExceptionDescriptor(\LogicException::class), new \LogicException],
'Subclass match' => [new ExceptionDescriptor(\LogicException::class), new \DomainException],
'Message exact match' => [
(new ExceptionDescriptor(\Exception::class))
->setMessage('foo'),
new \Exception('foo')
],
'Null message' => [
(new ExceptionDescriptor(\Exception::class))
->setMessage(null),
new \Exception('foo')
],
];
}

/**
* @dataProvider provideNonMatches
*/
public function testNonMatches(ExceptionDescriptor $descriptor, \Exception $exception): void
{
self::assertFalse($descriptor->matches($exception));
}

public function provideNonMatches(): array
{
return [
'Class mismatch' => [new ExceptionDescriptor(\LogicException::class), new \RuntimeException],
'Superclass mismatch' => [new ExceptionDescriptor(\DomainException::class), new \LogicException],
'Message mismatch' => [
(new ExceptionDescriptor(\Exception::class))
->setMessage('foo'),
new \Exception('bar')
],
'Message blank' => [
(new ExceptionDescriptor(\Exception::class))
->setMessage(''),
new \Exception('foo')
],
'Message partial match' => [
(new ExceptionDescriptor(\Exception::class))
->setMessage('foo'),
new \Exception('foo ')
],
'Message case mismatch' => [
(new ExceptionDescriptor(\Exception::class))
->setMessage('foo'),
new \Exception('Foo')
]
];
}
}
2 changes: 1 addition & 1 deletion test/Unit/Porter/Connector/ImportConnectorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ public function testStatelessExceptionHandlerNotCloned(): void
$handler,
\Closure::bind(
function (): RecoverableExceptionHandler {
return $this->userReh;
return $this->userExceptionHandler;
},
$connector,
$connector
Expand Down