Skip to content

Commit 808a432

Browse files
committed
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.
1 parent fcf48ed commit 808a432

File tree

2 files changed

+113
-10
lines changed

2 files changed

+113
-10
lines changed

src/Connector/ImportConnector.php

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use ScriptFUSION\Porter\Cache\CacheUnavailableException;
88
use ScriptFUSION\Porter\Connector\Recoverable\RecoverableExceptionHandler;
99
use ScriptFUSION\Porter\Connector\Recoverable\StatelessRecoverableExceptionHandler;
10+
use ScriptFUSION\Porter\ExceptionDescriptor;
1011

1112
/**
1213
* Connector whose lifecycle is synchronised with an import operation. Ensures correct ConnectionContext is delivered
@@ -27,21 +28,26 @@ final class ImportConnector implements ConnectorWrapper
2728
*
2829
* @var RecoverableExceptionHandler
2930
*/
30-
private $userReh;
31+
private $userExceptionHandler;
3132

3233
/**
3334
* Resource-defined exception handler called when a recoverable exception is thrown by Connector::fetch().
3435
*
3536
* @var RecoverableExceptionHandler
3637
*/
37-
private $resourceReh;
38+
private $resourceExceptionHandler;
3839

3940
private $maxFetchAttempts;
4041

42+
/**
43+
* @var ExceptionDescriptor[]
44+
*/
45+
private $recoverableExceptionDescriptors;
46+
4147
/**
4248
* @param Connector|AsyncConnector $connector Wrapped connector.
4349
* @param ConnectionContext $connectionContext Connection context.
44-
* @param RecoverableExceptionHandler $recoverableExceptionHandler
50+
* @param RecoverableExceptionHandler $recoverableExceptionHandler User's recoverable exception handler.
4551
* @param int $maxFetchAttempts
4652
*/
4753
public function __construct(
@@ -56,7 +62,7 @@ public function __construct(
5662

5763
$this->connector = clone $connector;
5864
$this->connectionContext = $connectionContext;
59-
$this->userReh = $recoverableExceptionHandler;
65+
$this->userExceptionHandler = $recoverableExceptionHandler;
6066
$this->maxFetchAttempts = $maxFetchAttempts;
6167
}
6268

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

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

99105
// Call resource's exception handler, if defined.
100-
if ($this->resourceReh) {
101-
self::invokeHandler($this->resourceReh, $exception, $resourceHandlerCloned);
106+
if ($this->resourceExceptionHandler) {
107+
self::invokeHandler($this->resourceExceptionHandler, $exception, $resourceHandlerCloned);
102108
}
103109

104110
// Call user's exception handler.
105-
self::invokeHandler($this->userReh, $exception, $userHandlerCloned);
111+
self::invokeHandler($this->userExceptionHandler, $exception, $userHandlerCloned);
106112
};
107113
}
108114

115+
private function isRecoverable(\Exception $exception): bool
116+
{
117+
if ($exception instanceof RecoverableConnectorException) {
118+
return true;
119+
}
120+
121+
foreach ($this->recoverableExceptionDescriptors as $exceptionDescriptor) {
122+
if ($exceptionDescriptor->matches($exception)) {
123+
return true;
124+
}
125+
}
126+
127+
return false;
128+
}
129+
109130
/**
110131
* Invokes the specified fetch exception handler, cloning it if required.
111132
*
@@ -163,10 +184,20 @@ public function findBaseConnector()
163184
*/
164185
public function setRecoverableExceptionHandler(RecoverableExceptionHandler $recoverableExceptionHandler): void
165186
{
166-
if ($this->resourceReh !== null) {
187+
if ($this->resourceExceptionHandler !== null) {
167188
throw new \LogicException('Cannot set resource\'s recoverable exception handler: already set!');
168189
}
169190

170-
$this->resourceReh = $recoverableExceptionHandler;
191+
$this->resourceExceptionHandler = $recoverableExceptionHandler;
192+
}
193+
194+
/**
195+
* Adds the specified exception descriptor, designating it as a recoverable exception type.
196+
*
197+
* @param ExceptionDescriptor $descriptor
198+
*/
199+
public function addRecoverableExceptionDescriptor(ExceptionDescriptor $descriptor): void
200+
{
201+
$this->recoverableExceptionDescriptors[] = $descriptor;
171202
}
172203
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace ScriptFUSIONTest\Integration\Porter;
5+
6+
use PHPUnit\Framework\TestCase;
7+
use ScriptFUSION\Porter\ExceptionDescriptor;
8+
9+
final class ExceptionDescriptorTest extends TestCase
10+
{
11+
/**
12+
* @dataProvider provideMatches
13+
*/
14+
public function testMatches(ExceptionDescriptor $descriptor, \Exception $exception): void
15+
{
16+
self::assertTrue($descriptor->matches($exception));
17+
}
18+
19+
public function provideMatches(): array
20+
{
21+
return [
22+
'Class match' => [new ExceptionDescriptor(\LogicException::class), new \LogicException],
23+
'Subclass match' => [new ExceptionDescriptor(\LogicException::class), new \DomainException],
24+
'Message exact match' => [
25+
(new ExceptionDescriptor(\Exception::class))
26+
->setMessage('foo'),
27+
new \Exception('foo')
28+
],
29+
'Null message' => [
30+
(new ExceptionDescriptor(\Exception::class))
31+
->setMessage(null),
32+
new \Exception('foo')
33+
],
34+
];
35+
}
36+
37+
/**
38+
* @dataProvider provideNonMatches
39+
*/
40+
public function testNonMatches(ExceptionDescriptor $descriptor, \Exception $exception): void
41+
{
42+
self::assertFalse($descriptor->matches($exception));
43+
}
44+
45+
public function provideNonMatches(): array
46+
{
47+
return [
48+
'Class mismatch' => [new ExceptionDescriptor(\LogicException::class), new \RuntimeException],
49+
'Superclass mismatch' => [new ExceptionDescriptor(\DomainException::class), new \LogicException],
50+
'Message mismatch' => [
51+
(new ExceptionDescriptor(\Exception::class))
52+
->setMessage('foo'),
53+
new \Exception('bar')
54+
],
55+
'Message blank' => [
56+
(new ExceptionDescriptor(\Exception::class))
57+
->setMessage(''),
58+
new \Exception('foo')
59+
],
60+
'Message partial match' => [
61+
(new ExceptionDescriptor(\Exception::class))
62+
->setMessage('foo'),
63+
new \Exception('foo ')
64+
],
65+
'Message case mismatch' => [
66+
(new ExceptionDescriptor(\Exception::class))
67+
->setMessage('foo'),
68+
new \Exception('Foo')
69+
]
70+
];
71+
}
72+
}

0 commit comments

Comments
 (0)