From 33b0f846667e0cb479c2f2f35e6aabbe45ee69dd Mon Sep 17 00:00:00 2001 From: Bilge Date: Mon, 23 Apr 2018 09:55:11 +0100 Subject: [PATCH] Added mechanism to designate arbitrary exceptions as recoverable. This important feature is needed because, although we have a mechanism for marking exceptions that extend RecoverableConnectorException as recoverable at the connector level, exception may need to be treated as recoverable at the resource level too. Resource implementations may not always have the luxury of creating their own exception types when relying on third party libraries. --- src/Connector/ImportConnector.php | 51 ++++++++++--- src/ExceptionDescriptor.php | 49 +++++++++++++ .../Porter/ExceptionDescriptorTest.php | 72 +++++++++++++++++++ .../Porter/Connector/ImportConnectorTest.php | 2 +- 4 files changed, 163 insertions(+), 11 deletions(-) create mode 100644 src/ExceptionDescriptor.php create mode 100644 test/Integration/Porter/ExceptionDescriptorTest.php diff --git a/src/Connector/ImportConnector.php b/src/Connector/ImportConnector.php index 1e0bb50..52171d3 100644 --- a/src/Connector/ImportConnector.php +++ b/src/Connector/ImportConnector.php @@ -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 @@ -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( @@ -56,7 +62,7 @@ public function __construct( $this->connector = clone $connector; $this->connectionContext = $connectionContext; - $this->userReh = $recoverableExceptionHandler; + $this->userExceptionHandler = $recoverableExceptionHandler; $this->maxFetchAttempts = $maxFetchAttempts; } @@ -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. * @@ -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; } } diff --git a/src/ExceptionDescriptor.php b/src/ExceptionDescriptor.php new file mode 100644 index 0000000..96bbf67 --- /dev/null +++ b/src/ExceptionDescriptor.php @@ -0,0 +1,49 @@ +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; + } +} diff --git a/test/Integration/Porter/ExceptionDescriptorTest.php b/test/Integration/Porter/ExceptionDescriptorTest.php new file mode 100644 index 0000000..8d5d964 --- /dev/null +++ b/test/Integration/Porter/ExceptionDescriptorTest.php @@ -0,0 +1,72 @@ +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') + ] + ]; + } +} diff --git a/test/Unit/Porter/Connector/ImportConnectorTest.php b/test/Unit/Porter/Connector/ImportConnectorTest.php index 2de4bab..a422b32 100644 --- a/test/Unit/Porter/Connector/ImportConnectorTest.php +++ b/test/Unit/Porter/Connector/ImportConnectorTest.php @@ -194,7 +194,7 @@ public function testStatelessExceptionHandlerNotCloned(): void $handler, \Closure::bind( function (): RecoverableExceptionHandler { - return $this->userReh; + return $this->userExceptionHandler; }, $connector, $connector