Skip to content

Commit d8d9c94

Browse files
committed
Added custom fetch exception handler to Porter.
Added durability tests. Replaced "Retry error handlers" with "Retry exception handlers" library.
1 parent 6c4dcd9 commit d8d9c94

File tree

11 files changed

+154
-24
lines changed

11 files changed

+154
-24
lines changed

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"php": "^5.5|^7",
1313
"scriptfusion/static-class": "^1",
1414
"scriptfusion/retry": "^1.1",
15-
"scriptfusion/retry-error-handlers": "^1",
15+
"scriptfusion/retry-exception-handlers": "^1",
1616
"eloquent/enumeration": "^5",
1717
"psr/cache": "^1",
1818
"zendframework/zend-uri": "^2"
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
namespace ScriptFUSION\Porter\Connector;
3+
4+
/**
5+
* The exception that is thrown when a non-fatal error occurs during Connector::fetch.
6+
*/
7+
class RecoverableConnectorException extends \RuntimeException
8+
{
9+
// Intentionally empty.
10+
}
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
<?php
22
namespace ScriptFUSION\Porter\Net\Http;
33

4+
use ScriptFUSION\Porter\Connector\RecoverableConnectorException;
5+
46
/**
57
* The exception that is thrown when an HTTP connection error occurs.
68
*/
7-
class HttpConnectionException extends \RuntimeException
9+
class HttpConnectionException extends RecoverableConnectorException
810
{
911
// Intentionally empty.
1012
}

src/Net/Http/HttpConnector.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,8 @@ public function fetchFreshData($source, EncapsulatedOptions $options = null)
6868
$code = explode(' ', $http_response_header[0], 3)[1];
6969
if ($code < 200 || $code >= 400) {
7070
throw new HttpServerException(
71-
"HTTP server responded with error: \"$http_response_header[0]\".\n\n$response"
71+
"HTTP server responded with error: \"$http_response_header[0]\".\n\n$response",
72+
$code
7273
);
7374
}
7475

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
<?php
22
namespace ScriptFUSION\Porter\Net\Http;
33

4+
use ScriptFUSION\Porter\Connector\RecoverableConnectorException;
5+
46
/**
57
* The exception that is thrown when the server responds with an error code.
68
*/
7-
class HttpServerException extends \RuntimeException
9+
class HttpServerException extends RecoverableConnectorException
810
{
911
// Intentionally empty.
1012
}

src/Net/Soap/SoapConnector.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
namespace ScriptFUSION\Porter\Net\Soap;
33

44
use ScriptFUSION\Porter\Connector\CachingConnector;
5+
use ScriptFUSION\Porter\Connector\RecoverableConnectorException;
56
use ScriptFUSION\Porter\Options\EncapsulatedOptions;
67
use ScriptFUSION\Porter\Type\ObjectType;
78

@@ -32,7 +33,13 @@ public function fetchFreshData($source, EncapsulatedOptions $options = null)
3233

3334
$params = array_merge($this->options->getParameters(), $options ? $options->getParameters() : []);
3435

35-
return ObjectType::toArray($this->getOrCreateClient()->$source($params));
36+
try {
37+
$response = $this->getOrCreateClient()->$source($params);
38+
} catch (\Exception $exception) {
39+
throw new RecoverableConnectorException($exception->getMessage(), $exception->getCode(), $exception);
40+
}
41+
42+
return ObjectType::toArray($response);
3643
}
3744

3845
private function getOrCreateClient()

src/Porter.php

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,14 @@
1414
use ScriptFUSION\Porter\Collection\PorterRecords;
1515
use ScriptFUSION\Porter\Collection\ProviderRecords;
1616
use ScriptFUSION\Porter\Collection\RecordCollection;
17+
use ScriptFUSION\Porter\Connector\RecoverableConnectorException;
1718
use ScriptFUSION\Porter\Mapper\PorterMapper;
1819
use ScriptFUSION\Porter\Provider\ObjectNotCreatedException;
1920
use ScriptFUSION\Porter\Provider\Provider;
2021
use ScriptFUSION\Porter\Provider\ProviderFactory;
2122
use ScriptFUSION\Porter\Provider\Resource\ProviderResource;
2223
use ScriptFUSION\Porter\Specification\ImportSpecification;
23-
use ScriptFUSION\Retry\ErrorHandler\ExponentialBackoffErrorHandler;
24+
use ScriptFUSION\Retry\ExceptionHandler\ExponentialBackoffExceptionHandler;
2425

2526
/**
2627
* Imports data according to an ImportSpecification.
@@ -54,9 +55,15 @@ class Porter
5455
*/
5556
private $maxFetchAttempts = self::DEFAULT_FETCH_ATTEMPTS;
5657

58+
/**
59+
* @var callable
60+
*/
61+
private $fetchExceptionHandler;
62+
5763
public function __construct()
5864
{
5965
$this->defaultCacheAdvice = CacheAdvice::SHOULD_NOT_CACHE();
66+
$this->fetchExceptionHandler = new ExponentialBackoffExceptionHandler;
6067
}
6168

6269
/**
@@ -65,6 +72,8 @@ public function __construct()
6572
* @param ImportSpecification $specification Import specification.
6673
*
6774
* @return PorterRecords
75+
*
76+
* @throws ImportException Provider failed to return an iterator.
6877
*/
6978
public function import(ImportSpecification $specification)
7079
{
@@ -100,7 +109,7 @@ public function importOne(ImportSpecification $specification)
100109
$results = $this->import($specification);
101110

102111
if (!$results->valid()) {
103-
return;
112+
return null;
104113
}
105114

106115
$one = $results->current();
@@ -135,13 +144,24 @@ private function fetch(ProviderResource $resource, CacheAdvice $cacheAdvice = nu
135144
$provider = $this->getProvider($resource->getProviderClassName(), $resource->getProviderTag());
136145
$this->applyCacheAdvice($provider, $cacheAdvice ?: $this->defaultCacheAdvice);
137146

138-
return \ScriptFUSION\Retry\retry(
139-
$this->maxFetchAttempts,
147+
if (($records = \ScriptFUSION\Retry\retry(
148+
$this->getMaxFetchAttempts(),
140149
function () use ($provider, $resource) {
141150
return $provider->fetch($resource);
142151
},
143-
new ExponentialBackoffErrorHandler
144-
);
152+
function (\Exception $exception) {
153+
// Throw exception if unrecoverable.
154+
if (!$exception instanceof RecoverableConnectorException) {
155+
throw $exception;
156+
}
157+
158+
call_user_func($this->getFetchExceptionHandler(), $exception);
159+
}
160+
)) instanceof \Iterator) {
161+
return $records;
162+
}
163+
164+
throw new ImportException(get_class($provider) . '::fetch() did not return an Iterator.');
145165
}
146166

147167
private function filter(ProviderRecords $records, callable $predicate, $context)
@@ -327,4 +347,20 @@ public function setMaxFetchAttempts($attempts)
327347

328348
return $this;
329349
}
350+
351+
/**
352+
* @return callable
353+
*/
354+
private function getFetchExceptionHandler()
355+
{
356+
return $this->fetchExceptionHandler;
357+
}
358+
359+
/**
360+
* @param callable $fetchExceptionHandler
361+
*/
362+
public function setFetchExceptionHandler(callable $fetchExceptionHandler)
363+
{
364+
$this->fetchExceptionHandler = $fetchExceptionHandler;
365+
}
330366
}

test/Functional/Porter/Net/Http/HttpConnectorTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
use ScriptFUSION\Porter\Net\Http\HttpConnector;
77
use ScriptFUSION\Porter\Net\Http\HttpOptions;
88
use ScriptFUSION\Porter\Net\Http\HttpServerException;
9-
use ScriptFUSION\Retry\ErrorHandler\ExponentialBackoffErrorHandler;
9+
use ScriptFUSION\Retry\ExceptionHandler\ExponentialBackoffExceptionHandler;
1010
use Symfony\Component\Process\Process;
1111

1212
final class HttpConnectorTest extends \PHPUnit_Framework_TestCase
@@ -82,7 +82,7 @@ private function startServer($script)
8282
$this->fetch();
8383
}, function (\Exception $exception) {
8484
static $handler;
85-
$handler = $handler ?: new ExponentialBackoffErrorHandler;
85+
$handler = $handler ?: new ExponentialBackoffExceptionHandler();
8686

8787
if (!$exception instanceof HttpConnectionException) {
8888
return false;

test/Integration/Porter/PorterTest.php

Lines changed: 57 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use ScriptFUSION\Porter\Collection\MappedRecords;
1414
use ScriptFUSION\Porter\Collection\PorterRecords;
1515
use ScriptFUSION\Porter\Collection\ProviderRecords;
16+
use ScriptFUSION\Porter\Connector\RecoverableConnectorException;
1617
use ScriptFUSION\Porter\ImportException;
1718
use ScriptFUSION\Porter\Porter;
1819
use ScriptFUSION\Porter\Provider\Provider;
@@ -22,6 +23,7 @@
2223
use ScriptFUSION\Porter\ProviderNotFoundException;
2324
use ScriptFUSION\Porter\Specification\ImportSpecification;
2425
use ScriptFUSION\Porter\Specification\StaticDataImportSpecification;
26+
use ScriptFUSION\Retry\ExceptionHandler\ExponentialBackoffExceptionHandler;
2527
use ScriptFUSION\Retry\FailingTooHardException;
2628
use ScriptFUSIONTest\MockFactory;
2729

@@ -211,19 +213,11 @@ public function testImportTaggedResource()
211213
self::assertSame($output, $records->current());
212214
}
213215

214-
public function testOneTry()
215-
{
216-
$this->setExpectedException(FailingTooHardException::class, '1');
217-
218-
$this->provider->shouldReceive('fetch')->once()->andThrow(\Exception::class);
219-
$this->porter->setMaxFetchAttempts(1)->import($this->specification);
220-
}
221-
222-
public function testDefaultTries()
216+
public function testImportFailure()
223217
{
224-
$this->setExpectedException(FailingTooHardException::class, (string)Porter::DEFAULT_FETCH_ATTEMPTS);
218+
$this->provider->shouldReceive('fetch')->andReturn(null);
225219

226-
$this->provider->shouldReceive('fetch')->times(Porter::DEFAULT_FETCH_ATTEMPTS)->andThrow(\Exception::class);
220+
$this->setExpectedException(ImportException::class, get_class($this->provider));
227221
$this->porter->import($this->specification);
228222
}
229223

@@ -258,6 +252,58 @@ public function testImportOneOfMany()
258252

259253
#endregion
260254

255+
#region Durability
256+
257+
public function testOneTry()
258+
{
259+
$this->provider->shouldReceive('fetch')->once()->andThrow(RecoverableConnectorException::class);
260+
261+
$this->setExpectedException(FailingTooHardException::class, '1');
262+
$this->porter->setMaxFetchAttempts(1)->import($this->specification);
263+
}
264+
265+
public function testDerivedRecoverableException()
266+
{
267+
$this->provider->shouldReceive('fetch')->once()->andThrow(\Mockery::mock(RecoverableConnectorException::class));
268+
269+
$this->setExpectedException(FailingTooHardException::class);
270+
$this->porter->setMaxFetchAttempts(1)->import($this->specification);
271+
}
272+
273+
public function testDefaultTries()
274+
{
275+
$this->provider->shouldReceive('fetch')->times(Porter::DEFAULT_FETCH_ATTEMPTS)
276+
->andThrow(RecoverableConnectorException::class);
277+
278+
$this->setExpectedException(FailingTooHardException::class, (string)Porter::DEFAULT_FETCH_ATTEMPTS);
279+
$this->porter->import($this->specification);
280+
}
281+
282+
public function testUnrecoverableException()
283+
{
284+
$this->provider->shouldReceive('fetch')->once()->andThrow(\Exception::class);
285+
286+
$this->setExpectedException(\Exception::class);
287+
$this->porter->import($this->specification);
288+
}
289+
290+
public function testCustomFetchExceptionHandler()
291+
{
292+
$this->porter->setFetchExceptionHandler(
293+
\Mockery::mock(ExponentialBackoffExceptionHandler::class)
294+
->shouldReceive('__invoke')
295+
->times(Porter::DEFAULT_FETCH_ATTEMPTS - 1)
296+
->getMock()
297+
);
298+
$this->provider->shouldReceive('fetch')->times(Porter::DEFAULT_FETCH_ATTEMPTS)
299+
->andThrow(RecoverableConnectorException::class);
300+
301+
$this->setExpectedException(FailingTooHardException::class);
302+
$this->porter->import($this->specification);
303+
}
304+
305+
#endregion
306+
261307
public function testFilter()
262308
{
263309
$this->provider->shouldReceive('fetch')->andReturn(new \ArrayIterator(range(1, 10)));
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
namespace ScriptFUSIONTest\Unit\Porter\Net\Http;
3+
4+
use ScriptFUSION\Porter\Connector\RecoverableConnectorException;
5+
use ScriptFUSION\Porter\Net\Http\HttpConnectionException;
6+
7+
final class HttpConnectionExceptionTest extends \PHPUnit_Framework_TestCase
8+
{
9+
public function testRecoverable()
10+
{
11+
self::assertInstanceOf(RecoverableConnectorException::class, new HttpConnectionException);
12+
}
13+
}

0 commit comments

Comments
 (0)