From 3fd8a02ed41f07d1cabc0defc48c7d0bb8d9b62e Mon Sep 17 00:00:00 2001 From: Bilge Date: Wed, 4 Apr 2018 00:09:16 +0100 Subject: [PATCH 1/3] Added FetchExceptionHandler interface. Added Stateless- and ExponentialSleep FetchExceptionHandler implementations. Changed FEH defacto type from callable to FetchExceptionHandler. --- src/Connector/ConnectionContext.php | 17 ++++++++++--- .../ExponentialSleepFetchExceptionHandler.php | 22 ++++++++++++++++ .../FetchExceptionHandler.php | 9 +++++++ .../StatelessFetchExceptionHandler.php | 25 +++++++++++++++++++ src/Specification/ImportSpecification.php | 12 +++++---- test/FixtureFactory.php | 5 ++-- test/Integration/Porter/PorterTest.php | 15 ++++++----- test/Unit/Porter/ImportSpecificationTest.php | 6 ++--- 8 files changed, 91 insertions(+), 20 deletions(-) create mode 100644 src/Connector/FetchExceptionHandler/ExponentialSleepFetchExceptionHandler.php create mode 100644 src/Connector/FetchExceptionHandler/FetchExceptionHandler.php create mode 100644 src/Connector/FetchExceptionHandler/StatelessFetchExceptionHandler.php diff --git a/src/Connector/ConnectionContext.php b/src/Connector/ConnectionContext.php index d5e6e0b..a1a50f9 100644 --- a/src/Connector/ConnectionContext.php +++ b/src/Connector/ConnectionContext.php @@ -1,6 +1,8 @@ mustCache = (bool)$mustCache; $this->fetchExceptionHandler = $fetchExceptionHandler; @@ -50,11 +52,13 @@ public function mustCache() */ public function retry(callable $callback) { + $userHandlerReset = false; + return \ScriptFUSION\Retry\retry( $this->maxFetchAttempts, $callback, - function (\Exception $exception) { - // Throw exception if unrecoverable. + function (\Exception $exception) use (&$userHandlerReset) { + // Throw exception instead of retrying, if unrecoverable. if (!$exception instanceof RecoverableConnectorException) { throw $exception; } @@ -64,7 +68,12 @@ function (\Exception $exception) { call_user_func($this->providerFetchExceptionHandler, $exception); } - // TODO Clone exception handler to avoid persisting state between calls. + if (!$userHandlerReset) { + $this->fetchExceptionHandler->reset(); + $userHandlerReset = true; + } + + // TODO: Remove call_user_func calls when PHP 5 support dropped. call_user_func($this->fetchExceptionHandler, $exception); } ); diff --git a/src/Connector/FetchExceptionHandler/ExponentialSleepFetchExceptionHandler.php b/src/Connector/FetchExceptionHandler/ExponentialSleepFetchExceptionHandler.php new file mode 100644 index 0000000..09f356f --- /dev/null +++ b/src/Connector/FetchExceptionHandler/ExponentialSleepFetchExceptionHandler.php @@ -0,0 +1,22 @@ +handler = new ExponentialBackoffExceptionHandler; + } + + public function __invoke(\Exception $exception) + { + call_user_func($this->handler, $exception); + } +} diff --git a/src/Connector/FetchExceptionHandler/FetchExceptionHandler.php b/src/Connector/FetchExceptionHandler/FetchExceptionHandler.php new file mode 100644 index 0000000..a082f11 --- /dev/null +++ b/src/Connector/FetchExceptionHandler/FetchExceptionHandler.php @@ -0,0 +1,9 @@ +handler = $handler; + } + + public function reset() + { + // Intentionally empty. + } + + public function __invoke(\Exception $exception) + { + call_user_func($this->handler, $exception); + } +} diff --git a/src/Specification/ImportSpecification.php b/src/Specification/ImportSpecification.php index 472f02b..6dba52c 100644 --- a/src/Specification/ImportSpecification.php +++ b/src/Specification/ImportSpecification.php @@ -1,9 +1,10 @@ fetchExceptionHandler ?: $this->fetchExceptionHandler = new ExponentialBackoffExceptionHandler; + return $this->fetchExceptionHandler ?: $this->fetchExceptionHandler + = new ExponentialSleepFetchExceptionHandler; } /** * Sets the exception handler invoked each time a fetch attempt fails. * - * @param callable $fetchExceptionHandler Exception handler. + * @param FetchExceptionHandler $fetchExceptionHandler Fetch exception handler. * * @return $this */ - final public function setFetchExceptionHandler(callable $fetchExceptionHandler) + final public function setFetchExceptionHandler(FetchExceptionHandler $fetchExceptionHandler) { $this->fetchExceptionHandler = $fetchExceptionHandler; diff --git a/test/FixtureFactory.php b/test/FixtureFactory.php index 0772174..496f99a 100644 --- a/test/FixtureFactory.php +++ b/test/FixtureFactory.php @@ -2,6 +2,7 @@ namespace ScriptFUSIONTest; use ScriptFUSION\Porter\Connector\ConnectionContext; +use ScriptFUSION\Porter\Connector\FetchExceptionHandler\StatelessFetchExceptionHandler; use ScriptFUSION\Porter\Specification\ImportSpecification; use ScriptFUSION\StaticClass; @@ -25,9 +26,9 @@ public static function buildConnectionContext( ) { return new ConnectionContext( $cacheAdvice, - $fetchExceptionHandler ?: function () { + $fetchExceptionHandler ?: new StatelessFetchExceptionHandler(static function () { // Intentionally empty. - }, + }), $maxFetchAttempts ); } diff --git a/test/Integration/Porter/PorterTest.php b/test/Integration/Porter/PorterTest.php index a555ca6..224cd40 100644 --- a/test/Integration/Porter/PorterTest.php +++ b/test/Integration/Porter/PorterTest.php @@ -12,6 +12,8 @@ use ScriptFUSION\Porter\Connector\ConnectionContext; use ScriptFUSION\Porter\Connector\Connector; use ScriptFUSION\Porter\Connector\ConnectorOptions; +use ScriptFUSION\Porter\Connector\FetchExceptionHandler\FetchExceptionHandler; +use ScriptFUSION\Porter\Connector\FetchExceptionHandler\StatelessFetchExceptionHandler; use ScriptFUSION\Porter\Connector\ImportConnector; use ScriptFUSION\Porter\Connector\RecoverableConnectorException; use ScriptFUSION\Porter\ImportException; @@ -25,7 +27,6 @@ use ScriptFUSION\Porter\Specification\StaticDataImportSpecification; use ScriptFUSION\Porter\Transform\FilterTransformer; use ScriptFUSION\Porter\Transform\Transformer; -use ScriptFUSION\Retry\ExceptionHandler\ExponentialBackoffExceptionHandler; use ScriptFUSION\Retry\FailingTooHardException; use ScriptFUSIONTest\MockFactory; @@ -286,15 +287,17 @@ public function testUnrecoverableException() } /** - * Tests that a when custom fetch exception handler is specified and the connector throws a recoverable exception + * Tests that when a custom fetch exception handler is specified and the connector throws a recoverable exception * type, the handler is called on each retry. */ public function testCustomFetchExceptionHandler() { $this->specification->setFetchExceptionHandler( - \Mockery::mock(ExponentialBackoffExceptionHandler::class) + \Mockery::mock(FetchExceptionHandler::class) + ->shouldReceive('reset') + ->once() ->shouldReceive('__invoke') - ->times(ImportSpecification::DEFAULT_FETCH_ATTEMPTS - 1) + ->times(ImportSpecification::DEFAULT_FETCH_ATTEMPTS - 1) ->getMock() ); @@ -310,9 +313,9 @@ public function testCustomFetchExceptionHandler() */ public function testCustomProviderFetchExceptionHandler() { - $this->specification->setFetchExceptionHandler(function () { + $this->specification->setFetchExceptionHandler(new StatelessFetchExceptionHandler(function () { throw new \LogicException('This exception must not be thrown!'); - }); + })); $this->arrangeConnectorException($connectorException = new RecoverableConnectorException('This exception is caught by the provider handler.')); diff --git a/test/Unit/Porter/ImportSpecificationTest.php b/test/Unit/Porter/ImportSpecificationTest.php index 6604d1f..dbee803 100644 --- a/test/Unit/Porter/ImportSpecificationTest.php +++ b/test/Unit/Porter/ImportSpecificationTest.php @@ -1,11 +1,11 @@ specification ->addTransformer(\Mockery::mock(Transformer::class)) ->setContext($context = (object)[]) - ->setFetchExceptionHandler($handler = new Invokable) + ->setFetchExceptionHandler($handler = \Mockery::mock(FetchExceptionHandler::class)) ; $specification = clone $this->specification; @@ -141,7 +141,7 @@ public function provideFetchAttempts() public function testExceptionHandler() { self::assertSame( - $handler = new Invokable, + $handler = \Mockery::mock(FetchExceptionHandler::class), $this->specification->setFetchExceptionHandler($handler)->getFetchExceptionHandler() ); } From 9c8af416d481f730c43795ace86de6b01975e6d8 Mon Sep 17 00:00:00 2001 From: Bilge Date: Thu, 5 Apr 2018 00:22:35 +0100 Subject: [PATCH 2/3] Finalized FetchExceptionHandler interface and added accompanying test. Changed "provider" FEH name to "resource" fetch exception handler. Changed resource FEH type from callable to FetchExceptionHandler. --- src/Connector/ConnectionContext.php | 55 ++++++++++++------- .../ExponentialSleepFetchExceptionHandler.php | 2 +- .../FetchExceptionHandler.php | 27 ++++++++- .../StatelessFetchExceptionHandler.php | 2 +- src/Connector/ImportConnector.php | 2 +- src/Specification/ImportSpecification.php | 3 +- .../Connector/ConnectionContextTest.php | 46 ++++++++++++++++ test/Integration/Porter/PorterTest.php | 12 ++-- test/Stubs/Invokable.php | 10 ---- test/Stubs/TestFetchExceptionHandler.php | 35 ++++++++++++ .../Porter/Connector/ImportConnectorTest.php | 5 +- 11 files changed, 155 insertions(+), 44 deletions(-) create mode 100644 test/Integration/Porter/Connector/ConnectionContextTest.php delete mode 100644 test/Stubs/Invokable.php create mode 100644 test/Stubs/TestFetchExceptionHandler.php diff --git a/src/Connector/ConnectionContext.php b/src/Connector/ConnectionContext.php index a1a50f9..d0530e9 100644 --- a/src/Connector/ConnectionContext.php +++ b/src/Connector/ConnectionContext.php @@ -13,16 +13,16 @@ final class ConnectionContext /** * User-defined exception handler called when a recoverable exception is thrown by Connector::fetch(). * - * @var callable + * @var FetchExceptionHandler */ private $fetchExceptionHandler; /** - * Provider-defined exception handler called when a recoverable exception is thrown by Connector::fetch(). + * Resource-defined exception handler called when a recoverable exception is thrown by Connector::fetch(). * - * @var callable + * @var FetchExceptionHandler */ - private $providerFetchExceptionHandler; + private $resourceFetchExceptionHandler; private $maxFetchAttempts; @@ -52,46 +52,59 @@ public function mustCache() */ public function retry(callable $callback) { - $userHandlerReset = false; + $userHandlerCloned = $providerHandlerCloned = false; return \ScriptFUSION\Retry\retry( $this->maxFetchAttempts, $callback, - function (\Exception $exception) use (&$userHandlerReset) { + function (\Exception $exception) use (&$userHandlerCloned, &$providerHandlerCloned) { // Throw exception instead of retrying, if unrecoverable. if (!$exception instanceof RecoverableConnectorException) { throw $exception; } // Call provider's exception handler, if defined. - if ($this->providerFetchExceptionHandler) { - call_user_func($this->providerFetchExceptionHandler, $exception); + if ($this->resourceFetchExceptionHandler) { + self::invokeHandler($this->resourceFetchExceptionHandler, $exception, $providerHandlerCloned); } - if (!$userHandlerReset) { - $this->fetchExceptionHandler->reset(); - $userHandlerReset = true; - } - - // TODO: Remove call_user_func calls when PHP 5 support dropped. - call_user_func($this->fetchExceptionHandler, $exception); + // Call user's exception handler. + self::invokeHandler($this->fetchExceptionHandler, $exception, $userHandlerCloned); } ); } + /** + * Invokes the specified fetch exception handler, cloning it if required. + * + * @param FetchExceptionHandler $handler Fetch exception handler. + * @param \Exception $exception Exception to pass to the handler. + * @param bool $cloned False if handler requires cloning, true if handler has already been cloned. + */ + private static function invokeHandler(FetchExceptionHandler &$handler, \Exception $exception, &$cloned) + { + if (!$cloned) { + $handler = clone $handler; + $handler->initialize(); + $cloned = true; + } + + $handler($exception); + } + /** * Sets an exception handler to be called when a recoverable exception is thrown by Connector::fetch(). * - * This handler is intended to be set by provider resources only and is called before the user-defined handler. + * The handler is intended to be set by provider resources only once and is called before the user-defined handler. * - * @param callable $providerFetchExceptionHandler Exception handler. + * @param FetchExceptionHandler $resourceFetchExceptionHandler Exception handler. */ - public function setProviderFetchExceptionHandler(callable $providerFetchExceptionHandler) + public function setResourceFetchExceptionHandler(FetchExceptionHandler $resourceFetchExceptionHandler) { - if ($this->providerFetchExceptionHandler !== null) { - throw new \LogicException('Cannot set provider fetch exception handler: already set!'); + if ($this->resourceFetchExceptionHandler !== null) { + throw new \LogicException('Cannot set resource fetch exception handler: already set!'); } - $this->providerFetchExceptionHandler = $providerFetchExceptionHandler; + $this->resourceFetchExceptionHandler = $resourceFetchExceptionHandler; } } diff --git a/src/Connector/FetchExceptionHandler/ExponentialSleepFetchExceptionHandler.php b/src/Connector/FetchExceptionHandler/ExponentialSleepFetchExceptionHandler.php index 09f356f..f25d36c 100644 --- a/src/Connector/FetchExceptionHandler/ExponentialSleepFetchExceptionHandler.php +++ b/src/Connector/FetchExceptionHandler/ExponentialSleepFetchExceptionHandler.php @@ -10,7 +10,7 @@ class ExponentialSleepFetchExceptionHandler implements FetchExceptionHandler { private $handler; - public function reset() + public function initialize() { $this->handler = new ExponentialBackoffExceptionHandler; } diff --git a/src/Connector/FetchExceptionHandler/FetchExceptionHandler.php b/src/Connector/FetchExceptionHandler/FetchExceptionHandler.php index a082f11..7896384 100644 --- a/src/Connector/FetchExceptionHandler/FetchExceptionHandler.php +++ b/src/Connector/FetchExceptionHandler/FetchExceptionHandler.php @@ -1,9 +1,34 @@ handler = $handler; } - public function reset() + public function initialize() { // Intentionally empty. } diff --git a/src/Connector/ImportConnector.php b/src/Connector/ImportConnector.php index 144c231..e74d7f2 100644 --- a/src/Connector/ImportConnector.php +++ b/src/Connector/ImportConnector.php @@ -47,6 +47,6 @@ public function getWrappedConnector() */ public function setExceptionHandler(callable $exceptionHandler) { - $this->context->setProviderFetchExceptionHandler($exceptionHandler); + $this->context->setResourceFetchExceptionHandler($exceptionHandler); } } diff --git a/src/Specification/ImportSpecification.php b/src/Specification/ImportSpecification.php index 6dba52c..1a2510d 100644 --- a/src/Specification/ImportSpecification.php +++ b/src/Specification/ImportSpecification.php @@ -242,8 +242,7 @@ final public function setMaxFetchAttempts($attempts) */ final public function getFetchExceptionHandler() { - return $this->fetchExceptionHandler ?: $this->fetchExceptionHandler - = new ExponentialSleepFetchExceptionHandler; + return $this->fetchExceptionHandler ?: $this->fetchExceptionHandler = new ExponentialSleepFetchExceptionHandler; } /** diff --git a/test/Integration/Porter/Connector/ConnectionContextTest.php b/test/Integration/Porter/Connector/ConnectionContextTest.php new file mode 100644 index 0000000..7b36a69 --- /dev/null +++ b/test/Integration/Porter/Connector/ConnectionContextTest.php @@ -0,0 +1,46 @@ +initialize(); + $initial = $handler->getCurrent(); + + $context->retry(static function () { + static $invocationCount; + + if (!$invocationCount++) { + throw new RecoverableConnectorException; + } + }); + + self::assertSame($initial, $handler->getCurrent()); + } + + public function provideHandlerAndContext() + { + yield 'User exception handler' => [ + $handler = new TestFetchExceptionHandler, + FixtureFactory::buildConnectionContext(false, $handler), + ]; + + $context = FixtureFactory::buildConnectionContext(); + // It should be OK to reuse the handler here because the whole point of the test is that it's not modified. + $context->setResourceFetchExceptionHandler($handler); + yield 'Resource exception handler' => [$handler, $context]; + } +} diff --git a/test/Integration/Porter/PorterTest.php b/test/Integration/Porter/PorterTest.php index 224cd40..21e3c7f 100644 --- a/test/Integration/Porter/PorterTest.php +++ b/test/Integration/Porter/PorterTest.php @@ -294,7 +294,7 @@ public function testCustomFetchExceptionHandler() { $this->specification->setFetchExceptionHandler( \Mockery::mock(FetchExceptionHandler::class) - ->shouldReceive('reset') + ->shouldReceive('initialize') ->once() ->shouldReceive('__invoke') ->times(ImportSpecification::DEFAULT_FETCH_ATTEMPTS - 1) @@ -323,11 +323,13 @@ public function testCustomProviderFetchExceptionHandler() $this->resource ->shouldReceive('fetch') ->andReturnUsing(function (ImportConnector $connector) use ($connectorException) { - $connector->setExceptionHandler(function (\Exception $exception) use ($connectorException) { - self::assertSame($connectorException, $exception); + $connector->setExceptionHandler(new StatelessFetchExceptionHandler( + function (\Exception $exception) use ($connectorException) { + self::assertSame($connectorException, $exception); - throw new \RuntimeException('This exception is thrown by the provider handler.'); - }); + throw new \RuntimeException('This exception is thrown by the provider handler.'); + } + )); yield $connector->fetch('foo'); }) diff --git a/test/Stubs/Invokable.php b/test/Stubs/Invokable.php deleted file mode 100644 index 37c24a0..0000000 --- a/test/Stubs/Invokable.php +++ /dev/null @@ -1,10 +0,0 @@ -series = call_user_func(function () { + foreach (range(1, 10) as $value) { + yield $value; + } + }); + } + + public function __invoke(\Exception $exception) + { + $current = $this->getCurrent(); + + $this->series->next(); + + return $current; + } + + public function getCurrent() + { + return $this->series->current(); + } +} diff --git a/test/Unit/Porter/Connector/ImportConnectorTest.php b/test/Unit/Porter/Connector/ImportConnectorTest.php index c6f81d3..b885367 100644 --- a/test/Unit/Porter/Connector/ImportConnectorTest.php +++ b/test/Unit/Porter/Connector/ImportConnectorTest.php @@ -4,6 +4,7 @@ use ScriptFUSION\Porter\Cache\CacheUnavailableException; use ScriptFUSION\Porter\Connector\CachingConnector; use ScriptFUSION\Porter\Connector\Connector; +use ScriptFUSION\Porter\Connector\FetchExceptionHandler\FetchExceptionHandler; use ScriptFUSION\Porter\Connector\ImportConnector; use ScriptFUSIONTest\FixtureFactory; @@ -100,9 +101,9 @@ public function testSetExceptionHandlerTwice() FixtureFactory::buildConnectionContext() ); - $connector->setExceptionHandler([$this, __FUNCTION__]); + $connector->setExceptionHandler($handler = \Mockery::mock(FetchExceptionHandler::class)); $this->setExpectedException(\LogicException::class); - $connector->setExceptionHandler([$this, __FUNCTION__]); + $connector->setExceptionHandler($handler); } } From 76d3909cb9d584946019b51876d24c5339f000f3 Mon Sep 17 00:00:00 2001 From: Bilge Date: Thu, 5 Apr 2018 14:30:29 +0100 Subject: [PATCH 3/3] Added StatelessFetchExceptionHandler cloning optimization in ConnectionContext. Fixed type hints in ImportConnector from callable -> FetchExceptionHandler. --- src/Connector/ConnectionContext.php | 3 +- .../StatelessFetchExceptionHandler.php | 2 +- src/Connector/ImportConnector.php | 5 +- src/Specification/ImportSpecification.php | 4 +- .../Connector/ConnectionContextTest.php | 57 ++++++++++++++++--- 5 files changed, 58 insertions(+), 13 deletions(-) diff --git a/src/Connector/ConnectionContext.php b/src/Connector/ConnectionContext.php index d0530e9..9b31836 100644 --- a/src/Connector/ConnectionContext.php +++ b/src/Connector/ConnectionContext.php @@ -2,6 +2,7 @@ namespace ScriptFUSION\Porter\Connector; use ScriptFUSION\Porter\Connector\FetchExceptionHandler\FetchExceptionHandler; +use ScriptFUSION\Porter\Connector\FetchExceptionHandler\StatelessFetchExceptionHandler; /** * Specifies runtime connection settings and provides utility methods. @@ -83,7 +84,7 @@ function (\Exception $exception) use (&$userHandlerCloned, &$providerHandlerClon */ private static function invokeHandler(FetchExceptionHandler &$handler, \Exception $exception, &$cloned) { - if (!$cloned) { + if (!$cloned && !$handler instanceof StatelessFetchExceptionHandler) { $handler = clone $handler; $handler->initialize(); $cloned = true; diff --git a/src/Connector/FetchExceptionHandler/StatelessFetchExceptionHandler.php b/src/Connector/FetchExceptionHandler/StatelessFetchExceptionHandler.php index 690bf0d..9d1884b 100644 --- a/src/Connector/FetchExceptionHandler/StatelessFetchExceptionHandler.php +++ b/src/Connector/FetchExceptionHandler/StatelessFetchExceptionHandler.php @@ -2,7 +2,7 @@ namespace ScriptFUSION\Porter\Connector\FetchExceptionHandler; /** - * Contains a stateless fetch exception handler that does not respond to reset() calls. + * Contains a fetch exception handler that does not have private state and therefore does not require initialization. */ final class StatelessFetchExceptionHandler implements FetchExceptionHandler { diff --git a/src/Connector/ImportConnector.php b/src/Connector/ImportConnector.php index e74d7f2..0decd8c 100644 --- a/src/Connector/ImportConnector.php +++ b/src/Connector/ImportConnector.php @@ -2,6 +2,7 @@ namespace ScriptFUSION\Porter\Connector; use ScriptFUSION\Porter\Cache\CacheUnavailableException; +use ScriptFUSION\Porter\Connector\FetchExceptionHandler\FetchExceptionHandler; /** * Connector whose lifecycle is synchronised with an import operation. Ensures correct ConnectionContext is delivered @@ -43,9 +44,9 @@ public function getWrappedConnector() /** * Sets the exception handler to be called when a recoverable exception is thrown by Connector::fetch(). * - * @param callable $exceptionHandler Exception handler. + * @param FetchExceptionHandler $exceptionHandler Fetch exception handler. */ - public function setExceptionHandler(callable $exceptionHandler) + public function setExceptionHandler(FetchExceptionHandler $exceptionHandler) { $this->context->setResourceFetchExceptionHandler($exceptionHandler); } diff --git a/src/Specification/ImportSpecification.php b/src/Specification/ImportSpecification.php index 1a2510d..53acd4b 100644 --- a/src/Specification/ImportSpecification.php +++ b/src/Specification/ImportSpecification.php @@ -44,7 +44,7 @@ class ImportSpecification private $maxFetchAttempts = self::DEFAULT_FETCH_ATTEMPTS; /** - * @var callable + * @var FetchExceptionHandler */ private $fetchExceptionHandler; @@ -68,7 +68,7 @@ function (Transformer $transformer) { )); is_object($this->context) && $this->context = clone $this->context; - is_object($this->fetchExceptionHandler) && $this->fetchExceptionHandler = clone $this->fetchExceptionHandler; + $this->fetchExceptionHandler && $this->fetchExceptionHandler = clone $this->fetchExceptionHandler; } /** diff --git a/test/Integration/Porter/Connector/ConnectionContextTest.php b/test/Integration/Porter/Connector/ConnectionContextTest.php index 7b36a69..a5296da 100644 --- a/test/Integration/Porter/Connector/ConnectionContextTest.php +++ b/test/Integration/Porter/Connector/ConnectionContextTest.php @@ -2,10 +2,14 @@ namespace ScriptFUSIONTest\Integration\Porter\Connector; use ScriptFUSION\Porter\Connector\ConnectionContext; +use ScriptFUSION\Porter\Connector\FetchExceptionHandler\StatelessFetchExceptionHandler; use ScriptFUSION\Porter\Connector\RecoverableConnectorException; use ScriptFUSIONTest\FixtureFactory; use ScriptFUSIONTest\Stubs\TestFetchExceptionHandler; +/** + * @see ConnectionContext + */ final class ConnectionContextTest extends \PHPUnit_Framework_TestCase { /** @@ -20,13 +24,7 @@ public function testFetchExceptionHandlerCloned(TestFetchExceptionHandler $handl $handler->initialize(); $initial = $handler->getCurrent(); - $context->retry(static function () { - static $invocationCount; - - if (!$invocationCount++) { - throw new RecoverableConnectorException; - } - }); + $context->retry(self::createExceptionThrowingClosure()); self::assertSame($initial, $handler->getCurrent()); } @@ -43,4 +41,49 @@ public function provideHandlerAndContext() $context->setResourceFetchExceptionHandler($handler); yield 'Resource exception handler' => [$handler, $context]; } + + /** + * Tests that when retry() is called, a stateless fetch exception handler is neither cloned nor reinitialized. + * For stateless handlers, initialization is a NOOP, so avoiding cloning is a small optimization. + */ + public function testStatelessExceptionHandlerNotCloned() + { + $context = FixtureFactory::buildConnectionContext( + false, + $handler = new StatelessFetchExceptionHandler(static function () { + // Intentionally empty. + }) + ); + + $context->retry(self::createExceptionThrowingClosure()); + + self::assertSame( + $handler, + call_user_func( + \Closure::bind( + function () { + return $this->fetchExceptionHandler; + }, + $context, + $context + ) + ) + ); + } + + /** + * Creates a closure that only throws an exception on the first invocation. + * + * @return \Closure + */ + private static function createExceptionThrowingClosure() + { + return static function () { + static $invocationCount; + + if (!$invocationCount++) { + throw new RecoverableConnectorException; + } + }; + } }