summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorEwan Dennis <ewandennis@users.noreply.github.com>2017-07-10 16:13:28 +0100
committerGitHub <noreply@github.com>2017-07-10 16:13:28 +0100
commit342ecf62947cfe3b31f2253fb427b1b4a613907b (patch)
tree0a7ddf7a0dee31d895479ce8af589477b93d8bd4
parent4de9c54c7a3554192e50fee9734fa96bf3e7bec3 (diff)
parent7beccdecb04d24cd055c91cf9b652ccc58eeaf60 (diff)
downloadphp-sparkpost-342ecf62947cfe3b31f2253fb427b1b4a613907b.zip
php-sparkpost-342ecf62947cfe3b31f2253fb427b1b4a613907b.tar.gz
php-sparkpost-342ecf62947cfe3b31f2253fb427b1b4a613907b.tar.bz2
Merge pull request #169 from SparkPost/issue-168
Issue-168: Optional automatic retry on 5xx
-rw-r--r--README.md5
-rw-r--r--examples/message-events/get_message_events_with_retry_logic.php33
-rw-r--r--lib/SparkPost/SparkPost.php41
-rw-r--r--test/unit/SparkPostTest.php76
4 files changed, 151 insertions, 4 deletions
diff --git a/README.md b/README.md
index 14a52cb..97947db 100644
--- a/README.md
+++ b/README.md
@@ -102,6 +102,11 @@ $sparky = new SparkPost($httpClient, ['key'=>'YOUR_API_KEY']);
* Type: `Boolean`
* Default: `true`
* `async` defines if the `request` function sends an asynchronous or synchronous request. If your client does not support async requests set this to `false`
+* `options.retries`
+ * Required: No
+ * Type: `Number`
+ * Default: `0`
+ * `retries` controls how many API call attempts the client makes after receiving a 5xx response
* `options.debug`
* Required: No
* Type: `Boolean`
diff --git a/examples/message-events/get_message_events_with_retry_logic.php b/examples/message-events/get_message_events_with_retry_logic.php
new file mode 100644
index 0000000..68225cb
--- /dev/null
+++ b/examples/message-events/get_message_events_with_retry_logic.php
@@ -0,0 +1,33 @@
+<?php
+
+namespace Examples\Templates;
+
+require dirname(__FILE__).'/../bootstrap.php';
+
+use SparkPost\SparkPost;
+use GuzzleHttp\Client;
+use Http\Adapter\Guzzle6\Client as GuzzleAdapter;
+
+$httpClient = new GuzzleAdapter(new Client());
+
+$sparky = new SparkPost($httpClient, ["key" => "YOUR_API_KEY", "retries" => 3]);
+
+$promise = $sparky->request('GET', 'message-events', [
+ 'campaign_ids' => 'CAMPAIGN_ID',
+]);
+
+/**
+ * If this fails with a 5xx it will have failed 4 times
+ */
+try {
+ $response = $promise->wait();
+ echo $response->getStatusCode()."\n";
+ print_r($response->getBody())."\n";
+} catch (\Exception $e) {
+ echo $e->getCode()."\n";
+ echo $e->getMessage()."\n";
+
+ if ($e->getCode() >= 500 && $e->getCode() <= 599) {
+ echo "Wow, this failed epically";
+ }
+}
diff --git a/lib/SparkPost/SparkPost.php b/lib/SparkPost/SparkPost.php
index 05be7f4..b3a9e36 100644
--- a/lib/SparkPost/SparkPost.php
+++ b/lib/SparkPost/SparkPost.php
@@ -41,6 +41,7 @@ class SparkPost
'version' => 'v1',
'async' => true,
'debug' => false,
+ 'retries' => 0
];
/**
@@ -97,13 +98,29 @@ class SparkPost
$requestValues = $this->buildRequestValues($method, $uri, $payload, $headers);
$request = call_user_func_array(array($this, 'buildRequestInstance'), $requestValues);
+ $retries = $this->options['retries'];
try {
- return new SparkPostResponse($this->httpClient->sendRequest($request), $this->ifDebug($requestValues));
+ if ($retries > 0) {
+ $resp = $this->syncReqWithRetry($request, $retries);
+ } else {
+ $resp = $this->httpClient->sendRequest($request);
+ }
+ return new SparkPostResponse($resp, $this->ifDebug($requestValues));
} catch (\Exception $exception) {
throw new SparkPostException($exception, $this->ifDebug($requestValues));
}
}
+ private function syncReqWithRetry($request, $retries)
+ {
+ $resp = $this->httpClient->sendRequest($request);
+ $status = $resp->getStatusCode();
+ if ($status >= 500 && $status <= 599 && $retries > 0) {
+ return $this->syncReqWithRetry($request, $retries-1);
+ }
+ return $resp;
+ }
+
/**
* Sends async request to SparkPost API.
*
@@ -120,12 +137,28 @@ class SparkPost
$requestValues = $this->buildRequestValues($method, $uri, $payload, $headers);
$request = call_user_func_array(array($this, 'buildRequestInstance'), $requestValues);
- return new SparkPostPromise($this->httpClient->sendAsyncRequest($request), $this->ifDebug($requestValues));
+ $retries = $this->options['retries'];
+ if ($retries > 0) {
+ return new SparkPostPromise($this->asyncReqWithRetry($request, $retries), $this->ifDebug($requestValues));
+ } else {
+ return new SparkPostPromise($this->httpClient->sendAsyncRequest($request), $this->ifDebug($requestValues));
+ }
} else {
throw new \Exception('Your http client does not support asynchronous requests. Please use a different client or use synchronous requests.');
}
}
+ private function asyncReqWithRetry($request, $retries)
+ {
+ return $this->httpClient->sendAsyncRequest($request)->then(function($response) use ($request, $retries) {
+ $status = $response->getStatusCode();
+ if ($status >= 500 && $status <= 599 && $retries > 0) {
+ return $this->asyncReqWithRetry($request, $retries-1);
+ }
+ return $response;
+ });
+ }
+
/**
* Builds request values from given params.
*
@@ -252,7 +285,7 @@ class SparkPost
}
$this->httpClient = $httpClient;
-
+
return $this;
}
@@ -283,7 +316,7 @@ class SparkPost
$this->options[$option] = $value;
}
}
-
+
return $this;
}
diff --git a/test/unit/SparkPostTest.php b/test/unit/SparkPostTest.php
index cb139f6..89e8e3a 100644
--- a/test/unit/SparkPostTest.php
+++ b/test/unit/SparkPostTest.php
@@ -60,6 +60,13 @@ class SparkPostTest extends \PHPUnit_Framework_TestCase
$this->responseMock->shouldReceive('getBody')->andReturn($responseBodyMock);
$responseBodyMock->shouldReceive('__toString')->andReturn(json_encode($this->responseBody));
+ $errorBodyMock = Mockery::mock();
+ $this->badResponseBody = ['errors' => []];
+ $this->badResponseMock = Mockery::mock('Psr\Http\Message\ResponseInterface');
+ $this->badResponseMock->shouldReceive('getStatusCode')->andReturn(503);
+ $this->badResponseMock->shouldReceive('getBody')->andReturn($errorBodyMock);
+ $errorBodyMock->shouldReceive('__toString')->andReturn(json_encode($this->badResponseBody));
+
// exception mock up
$exceptionResponseMock = Mockery::mock();
$this->exceptionBody = ['results' => 'failed'];
@@ -159,6 +166,35 @@ class SparkPostTest extends \PHPUnit_Framework_TestCase
}
}
+ public function testSuccessfulSyncRequestWithRetries()
+ {
+ $this->clientMock->shouldReceive('sendRequest')->
+ with(Mockery::type('GuzzleHttp\Psr7\Request'))->
+ andReturn($this->badResponseMock, $this->badResponseMock, $this->responseMock);
+
+ $this->resource->setOptions(['retries' => 2]);
+ $response = $this->resource->syncRequest('POST', 'transmissions', $this->postTransmissionPayload);
+
+ $this->assertEquals($this->responseBody, $response->getBody());
+ $this->assertEquals(200, $response->getStatusCode());
+ }
+
+ public function testUnsuccessfulSyncRequestWithRetries()
+ {
+ $this->clientMock->shouldReceive('sendRequest')->
+ once()->
+ with(Mockery::type('GuzzleHttp\Psr7\Request'))->
+ andThrow($this->exceptionMock);
+
+ $this->resource->setOptions(['retries' => 2]);
+ try {
+ $this->resource->syncRequest('POST', 'transmissions', $this->postTransmissionPayload);
+ } catch (\Exception $e) {
+ $this->assertEquals($this->exceptionBody, $e->getBody());
+ $this->assertEquals(500, $e->getCode());
+ }
+ }
+
public function testSuccessfulAsyncRequestWithWait()
{
$this->promiseMock->shouldReceive('wait')->andReturn($this->responseMock);
@@ -212,6 +248,46 @@ class SparkPostTest extends \PHPUnit_Framework_TestCase
})->wait();
}
+ public function testSuccessfulAsyncRequestWithRetries()
+ {
+ $testReq = $this->resource->buildRequest('POST', 'transmissions', $this->postTransmissionPayload, []);
+ $clientMock = Mockery::mock('Http\Adapter\Guzzle6\Client');
+ $clientMock->shouldReceive('sendAsyncRequest')->
+ with(Mockery::type('GuzzleHttp\Psr7\Request'))->
+ andReturn(
+ new GuzzleAdapterPromise(new GuzzleFulfilledPromise($this->badResponseMock), $testReq),
+ new GuzzleAdapterPromise(new GuzzleFulfilledPromise($this->badResponseMock), $testReq),
+ new GuzzleAdapterPromise(new GuzzleFulfilledPromise($this->responseMock), $testReq)
+ );
+
+ $resource = new SparkPost($clientMock, ['key' => 'SPARKPOST_API_KEY']);
+
+ $resource->setOptions(['async' => true, 'retries' => 2]);
+ $promise = $resource->asyncRequest('POST', 'transmissions', $this->postTransmissionPayload);
+ $promise->then(function($resp) {
+ $this->assertEquals(200, $resp->getStatusCode());
+ })->wait();
+ }
+
+ public function testUnsuccessfulAsyncRequestWithRetries()
+ {
+ $testReq = $this->resource->buildRequest('POST', 'transmissions', $this->postTransmissionPayload, []);
+ $rejectedPromise = new GuzzleRejectedPromise($this->exceptionMock);
+ $clientMock = Mockery::mock('Http\Adapter\Guzzle6\Client');
+ $clientMock->shouldReceive('sendAsyncRequest')->
+ with(Mockery::type('GuzzleHttp\Psr7\Request'))->
+ andReturn(new GuzzleAdapterPromise($rejectedPromise, $testReq));
+
+ $resource = new SparkPost($clientMock, ['key' => 'SPARKPOST_API_KEY']);
+
+ $resource->setOptions(['async' => true, 'retries' => 2]);
+ $promise = $resource->asyncRequest('POST', 'transmissions', $this->postTransmissionPayload);
+ $promise->then(null, function($exception) {
+ $this->assertEquals(500, $exception->getCode());
+ $this->assertEquals($this->exceptionBody, $exception->getBody());
+ })->wait();
+ }
+
public function testPromise()
{
$promise = $this->resource->asyncRequest('POST', 'transmissions', $this->postTransmissionPayload);