Skip to content

Commit 9c8af41

Browse files
committed
Finalized FetchExceptionHandler interface and added accompanying test.
Changed "provider" FEH name to "resource" fetch exception handler. Changed resource FEH type from callable to FetchExceptionHandler.
1 parent 3fd8a02 commit 9c8af41

File tree

11 files changed

+155
-44
lines changed

11 files changed

+155
-44
lines changed

src/Connector/ConnectionContext.php

Lines changed: 34 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,16 @@ final class ConnectionContext
1313
/**
1414
* User-defined exception handler called when a recoverable exception is thrown by Connector::fetch().
1515
*
16-
* @var callable
16+
* @var FetchExceptionHandler
1717
*/
1818
private $fetchExceptionHandler;
1919

2020
/**
21-
* Provider-defined exception handler called when a recoverable exception is thrown by Connector::fetch().
21+
* Resource-defined exception handler called when a recoverable exception is thrown by Connector::fetch().
2222
*
23-
* @var callable
23+
* @var FetchExceptionHandler
2424
*/
25-
private $providerFetchExceptionHandler;
25+
private $resourceFetchExceptionHandler;
2626

2727
private $maxFetchAttempts;
2828

@@ -52,46 +52,59 @@ public function mustCache()
5252
*/
5353
public function retry(callable $callback)
5454
{
55-
$userHandlerReset = false;
55+
$userHandlerCloned = $providerHandlerCloned = false;
5656

5757
return \ScriptFUSION\Retry\retry(
5858
$this->maxFetchAttempts,
5959
$callback,
60-
function (\Exception $exception) use (&$userHandlerReset) {
60+
function (\Exception $exception) use (&$userHandlerCloned, &$providerHandlerCloned) {
6161
// Throw exception instead of retrying, if unrecoverable.
6262
if (!$exception instanceof RecoverableConnectorException) {
6363
throw $exception;
6464
}
6565

6666
// Call provider's exception handler, if defined.
67-
if ($this->providerFetchExceptionHandler) {
68-
call_user_func($this->providerFetchExceptionHandler, $exception);
67+
if ($this->resourceFetchExceptionHandler) {
68+
self::invokeHandler($this->resourceFetchExceptionHandler, $exception, $providerHandlerCloned);
6969
}
7070

71-
if (!$userHandlerReset) {
72-
$this->fetchExceptionHandler->reset();
73-
$userHandlerReset = true;
74-
}
75-
76-
// TODO: Remove call_user_func calls when PHP 5 support dropped.
77-
call_user_func($this->fetchExceptionHandler, $exception);
71+
// Call user's exception handler.
72+
self::invokeHandler($this->fetchExceptionHandler, $exception, $userHandlerCloned);
7873
}
7974
);
8075
}
8176

77+
/**
78+
* Invokes the specified fetch exception handler, cloning it if required.
79+
*
80+
* @param FetchExceptionHandler $handler Fetch exception handler.
81+
* @param \Exception $exception Exception to pass to the handler.
82+
* @param bool $cloned False if handler requires cloning, true if handler has already been cloned.
83+
*/
84+
private static function invokeHandler(FetchExceptionHandler &$handler, \Exception $exception, &$cloned)
85+
{
86+
if (!$cloned) {
87+
$handler = clone $handler;
88+
$handler->initialize();
89+
$cloned = true;
90+
}
91+
92+
$handler($exception);
93+
}
94+
8295
/**
8396
* Sets an exception handler to be called when a recoverable exception is thrown by Connector::fetch().
8497
*
85-
* This handler is intended to be set by provider resources only and is called before the user-defined handler.
98+
* The handler is intended to be set by provider resources only once and is called before the user-defined handler.
8699
*
87-
* @param callable $providerFetchExceptionHandler Exception handler.
100+
* @param FetchExceptionHandler $resourceFetchExceptionHandler Exception handler.
88101
*/
89-
public function setProviderFetchExceptionHandler(callable $providerFetchExceptionHandler)
102+
public function setResourceFetchExceptionHandler(FetchExceptionHandler $resourceFetchExceptionHandler)
90103
{
91-
if ($this->providerFetchExceptionHandler !== null) {
92-
throw new \LogicException('Cannot set provider fetch exception handler: already set!');
104+
if ($this->resourceFetchExceptionHandler !== null) {
105+
throw new \LogicException('Cannot set resource fetch exception handler: already set!');
93106
}
94107

95-
$this->providerFetchExceptionHandler = $providerFetchExceptionHandler;
108+
$this->resourceFetchExceptionHandler = $resourceFetchExceptionHandler;
96109
}
97110
}

src/Connector/FetchExceptionHandler/ExponentialSleepFetchExceptionHandler.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ class ExponentialSleepFetchExceptionHandler implements FetchExceptionHandler
1010
{
1111
private $handler;
1212

13-
public function reset()
13+
public function initialize()
1414
{
1515
$this->handler = new ExponentialBackoffExceptionHandler;
1616
}
Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,34 @@
11
<?php
22
namespace ScriptFUSION\Porter\Connector\FetchExceptionHandler;
33

4+
/**
5+
* Provides methods for handling exceptions thrown by Connector::fetch().
6+
*
7+
* This interface supports a prototype cloning model that guarantees the object can be cloned and reset to its
8+
* initial state at any time, any number of times. This is needed because a given import can spawn any number of
9+
* subsequent fetches, some of which may execute concurrently, and all of which share the same exception handler
10+
* prototype.
11+
*
12+
* This approach is better than relying on __clone because handlers may employ generators which cannot be cloned.
13+
* If generators are part of the object's state they must be recreated during initialize().
14+
*/
415
interface FetchExceptionHandler
516
{
6-
public function reset();
17+
/**
18+
* Initializes this handler to its starting state. Should be idempotent because it may be called multiple times.
19+
*
20+
* This method must always be called before the first call to __invoke.
21+
*
22+
* @return void
23+
*/
24+
public function initialize();
725

26+
/**
27+
* Handles a fetch() exception.
28+
*
29+
* @param \Exception $exception Exception thrown by Connector::fetch().
30+
*
31+
* @return void
32+
*/
833
public function __invoke(\Exception $exception);
934
}

src/Connector/FetchExceptionHandler/StatelessFetchExceptionHandler.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ public function __construct(callable $handler)
1313
$this->handler = $handler;
1414
}
1515

16-
public function reset()
16+
public function initialize()
1717
{
1818
// Intentionally empty.
1919
}

src/Connector/ImportConnector.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,6 @@ public function getWrappedConnector()
4747
*/
4848
public function setExceptionHandler(callable $exceptionHandler)
4949
{
50-
$this->context->setProviderFetchExceptionHandler($exceptionHandler);
50+
$this->context->setResourceFetchExceptionHandler($exceptionHandler);
5151
}
5252
}

src/Specification/ImportSpecification.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -242,8 +242,7 @@ final public function setMaxFetchAttempts($attempts)
242242
*/
243243
final public function getFetchExceptionHandler()
244244
{
245-
return $this->fetchExceptionHandler ?: $this->fetchExceptionHandler
246-
= new ExponentialSleepFetchExceptionHandler;
245+
return $this->fetchExceptionHandler ?: $this->fetchExceptionHandler = new ExponentialSleepFetchExceptionHandler;
247246
}
248247

249248
/**
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
namespace ScriptFUSIONTest\Integration\Porter\Connector;
3+
4+
use ScriptFUSION\Porter\Connector\ConnectionContext;
5+
use ScriptFUSION\Porter\Connector\RecoverableConnectorException;
6+
use ScriptFUSIONTest\FixtureFactory;
7+
use ScriptFUSIONTest\Stubs\TestFetchExceptionHandler;
8+
9+
final class ConnectionContextTest extends \PHPUnit_Framework_TestCase
10+
{
11+
/**
12+
* Tests that when retry() is called multiple times, the original fetch exception handler is unmodified.
13+
* This is expected because the handler must be cloned using the prototype pattern to ensure multiple concurrent
14+
* fetches do not conflict.
15+
*
16+
* @dataProvider provideHandlerAndContext
17+
*/
18+
public function testFetchExceptionHandlerCloned(TestFetchExceptionHandler $handler, ConnectionContext $context)
19+
{
20+
$handler->initialize();
21+
$initial = $handler->getCurrent();
22+
23+
$context->retry(static function () {
24+
static $invocationCount;
25+
26+
if (!$invocationCount++) {
27+
throw new RecoverableConnectorException;
28+
}
29+
});
30+
31+
self::assertSame($initial, $handler->getCurrent());
32+
}
33+
34+
public function provideHandlerAndContext()
35+
{
36+
yield 'User exception handler' => [
37+
$handler = new TestFetchExceptionHandler,
38+
FixtureFactory::buildConnectionContext(false, $handler),
39+
];
40+
41+
$context = FixtureFactory::buildConnectionContext();
42+
// It should be OK to reuse the handler here because the whole point of the test is that it's not modified.
43+
$context->setResourceFetchExceptionHandler($handler);
44+
yield 'Resource exception handler' => [$handler, $context];
45+
}
46+
}

test/Integration/Porter/PorterTest.php

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,7 @@ public function testCustomFetchExceptionHandler()
294294
{
295295
$this->specification->setFetchExceptionHandler(
296296
\Mockery::mock(FetchExceptionHandler::class)
297-
->shouldReceive('reset')
297+
->shouldReceive('initialize')
298298
->once()
299299
->shouldReceive('__invoke')
300300
->times(ImportSpecification::DEFAULT_FETCH_ATTEMPTS - 1)
@@ -323,11 +323,13 @@ public function testCustomProviderFetchExceptionHandler()
323323
$this->resource
324324
->shouldReceive('fetch')
325325
->andReturnUsing(function (ImportConnector $connector) use ($connectorException) {
326-
$connector->setExceptionHandler(function (\Exception $exception) use ($connectorException) {
327-
self::assertSame($connectorException, $exception);
326+
$connector->setExceptionHandler(new StatelessFetchExceptionHandler(
327+
function (\Exception $exception) use ($connectorException) {
328+
self::assertSame($connectorException, $exception);
328329

329-
throw new \RuntimeException('This exception is thrown by the provider handler.');
330-
});
330+
throw new \RuntimeException('This exception is thrown by the provider handler.');
331+
}
332+
));
331333

332334
yield $connector->fetch('foo');
333335
})

test/Stubs/Invokable.php

Lines changed: 0 additions & 10 deletions
This file was deleted.
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
namespace ScriptFUSIONTest\Stubs;
3+
4+
use ScriptFUSION\Porter\Connector\FetchExceptionHandler\FetchExceptionHandler;
5+
6+
final class TestFetchExceptionHandler implements FetchExceptionHandler
7+
{
8+
/**
9+
* @var \Generator
10+
*/
11+
private $series;
12+
13+
public function initialize()
14+
{
15+
$this->series = call_user_func(function () {
16+
foreach (range(1, 10) as $value) {
17+
yield $value;
18+
}
19+
});
20+
}
21+
22+
public function __invoke(\Exception $exception)
23+
{
24+
$current = $this->getCurrent();
25+
26+
$this->series->next();
27+
28+
return $current;
29+
}
30+
31+
public function getCurrent()
32+
{
33+
return $this->series->current();
34+
}
35+
}

0 commit comments

Comments
 (0)