Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Storage): Add retry conformance tests #5637

Merged
merged 55 commits into from
Apr 25, 2023
Merged
Show file tree
Hide file tree
Changes from 42 commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
205150d
feat(Storage): Add retry conformance tests for idempotent ops
saranshdhingra Nov 21, 2022
1ada578
fix: Add EOF to retry_tests.json
saranshdhingra Nov 21, 2022
5665dd7
Fix lint issues
saranshdhingra Nov 21, 2022
9a3c4d8
Added storage endpoint in RetryConformanceTest
saranshdhingra Nov 21, 2022
9f6de85
Added preconditions and conditionally idempotent methods in RetryConf…
saranshdhingra Nov 22, 2022
95e0fd2
Abstracted out createResources and disposeResources calls from method…
saranshdhingra Nov 23, 2022
a111e0c
Added conditionally idempotent cases in RetryConformanceTest.php
saranshdhingra Nov 23, 2022
5a196d2
Added method invocations for non idempotent methods
saranshdhingra Nov 24, 2022
9c21d11
Merge branch 'main' into gcs-conformance-testing
saranshdhingra Nov 24, 2022
07620aa
Test multiple scenarios, remove ACL cleanup, fix lockRetentionPolicy
saranshdhingra Nov 24, 2022
82e2dcb
Added tests for resumable uploads
saranshdhingra Nov 24, 2022
d588511
Login not needed in the conformance tests now
saranshdhingra Dec 9, 2022
fcb2569
Merge branch 'main' into gcs-conformance-testing
saranshdhingra Dec 21, 2022
5b01de8
feat(Storage): Add retry decider function for GCS (#5709)
ajupazhamayil Feb 28, 2023
0b0321a
feat(Storage): Add conformance testing with github workflows. (#5740)
saranshdhingra Mar 6, 2023
a4e633f
Merge branch 'main' into gcs-conformance-testing
saranshdhingra Mar 6, 2023
6ce5177
Merge branch 'main' into gcs-conformance-testing
yash30201 Mar 9, 2023
a4db5ea
Added tests for downloads, range headers and transcoded objects
saranshdhingra Mar 13, 2023
ae301ef
feat(Storage): Adding unit tests for GCS Retry (#5953)
yash30201 Mar 15, 2023
1325337
Merge branch 'main' into gcs-conformance-testing
saranshdhingra Mar 15, 2023
897ce82
Addressed PR comments
saranshdhingra Mar 29, 2023
8e7263e
Removed an extra constant from RequestWrapper and used an existing on…
saranshdhingra Apr 6, 2023
48d1b13
Lint fixes
saranshdhingra Apr 6, 2023
fabeaf6
Addressed comments for var/method visibility
saranshdhingra Apr 6, 2023
c60db5b
Merge branch 'main' into gcs-conformance-testing
bshaffer Apr 10, 2023
ff9d796
Merge branch 'main' into gcs-conformance-testing
yash30201 Apr 10, 2023
1e3c9eb
Fixed tests after dropping PHP 7.3
saranshdhingra Apr 10, 2023
7cc2403
Merge branch 'main' into gcs-conformance-testing
yash30201 Apr 11, 2023
ccd36eb
chore(gcs-conformance): simplify workflow file (#6054)
bshaffer Apr 11, 2023
8c56cfb
feat(Storage): Refactor header modification and PR review comments it…
yash30201 Apr 11, 2023
99deaf0
Removed retry attempt from custom retry function
saranshdhingra Apr 12, 2023
b0f03bf
Using Request::withHeader instead of modifyRequest in Rest.php
saranshdhingra Apr 12, 2023
8e6c49b
Merge branch 'main' into gcs-conformance-testing
saranshdhingra Apr 12, 2023
bed32f5
chore(gcs-conformance): tests the retry headers from the Rest surface…
bshaffer Apr 12, 2023
98211bf
chore(gcs-conformance): refactor stream so retryListener is internal …
bshaffer Apr 18, 2023
e750ca3
chore: changed RetryTrait::getRetryHeaders visibility to private
saranshdhingra Apr 18, 2023
9635c8d
chore: Simply preconditon tests in RestTest
saranshdhingra Apr 18, 2023
0c5085d
chore: Simplified the retry strategy tests
saranshdhingra Apr 18, 2023
429e4c5
Merge branch 'gcs-conformance-testing' of https://github.com/googleap…
saranshdhingra Apr 18, 2023
d27cac1
Lint fixes
saranshdhingra Apr 18, 2023
c800ec7
Removed non used vars from RetryTrait::retryDeciderFunction
saranshdhingra Apr 18, 2023
2e0a9bf
Merge branch 'main' into gcs-conformance-testing
saranshdhingra Apr 18, 2023
41c5bf7
chore(gcs-conformance): remove public methods from RetryTrait (#6075)
bshaffer Apr 18, 2023
929ca24
chore(gcs-conformance): better unit testing (#6090)
bshaffer Apr 19, 2023
c511885
chore: removed gax dependency from Storage
saranshdhingra Apr 19, 2023
2edd221
Merge branch 'main' into gcs-conformance-testing
saranshdhingra Apr 19, 2023
49d1346
Merge branch 'main' into gcs-conformance-testing
saranshdhingra Apr 19, 2023
c9b776b
Merge branch 'main' into gcs-conformance-testing
saranshdhingra Apr 19, 2023
61e4100
Update Storage/tests/Unit/StorageObjectTest.php
bshaffer Apr 20, 2023
66e7cdf
Update Storage/tests/Unit/Connection/RestTest.php
bshaffer Apr 20, 2023
b60572f
Merge branch 'main' into gcs-conformance-testing
yash30201 Apr 20, 2023
a0fcc94
Merge branch 'main' into gcs-conformance-testing
yash30201 Apr 20, 2023
686223c
fix: Storage unit tests
saranshdhingra Apr 20, 2023
62e1ef0
Addressed PR comments
saranshdhingra Apr 20, 2023
484e75e
Merge branch 'main' into gcs-conformance-testing
bshaffer Apr 24, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .github/workflows/storage-emulator-retry-conformance-tests.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
on:
push:
branches:
- main
paths:
- 'Storage/**'
- '.github/workflows/storage-emulator-retry-conformance-tests.yaml'
pull_request:
paths:
- 'Storage/**'
- '.github/workflows/storage-emulator-retry-conformance-tests.yaml'
name: Run Storage Retry Conformance Tests With Emulator
jobs:
test:
runs-on: ubuntu-20.04

services:
emulator:
image: gcr.io/cloud-devrel-public-resources/storage-testbench:v0.35.0
ports:
- 9000:9000

steps:
- name: Checkout code
uses: actions/checkout@v3

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '7.4'

- name: Install dependencies
run: |
composer update --prefer-dist --no-interaction --no-suggest

- name: Run storage retry conformance tests
run: |
vendor/bin/phpunit -c Storage/phpunit-conformance.xml.dist
env:
STORAGE_EMULATOR_HOST: http://localhost:9000
33 changes: 29 additions & 4 deletions Core/src/ExponentialBackoff.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,32 @@ class ExponentialBackoff
*/
private $calcDelayFunction;

/**
* @var callable|null
*/
private $retryListener;

/**
* @param int $retries [optional] Number of retries for a failed request.
* @param callable $retryFunction [optional] returns bool for whether or not to retry
* @param callable $retryFunction [optional] returns bool for whether or not
* to retry
* @param callable $retryListener [optional] Runs after the
* $retryFunction. Unlike the $retryFunction,this function isn't
* responsible to decide if a retry should happen or not, but it gives the
* users flexibility to consume exception messages and add custom logic.
* Function definition should match:
* function (\Exception $e, int $attempt, array $arguments): array
* Ex: One might want to change headers on every retry, this function can
* be used to achieve such a functionality.
*/
public function __construct($retries = null, callable $retryFunction = null)
{
public function __construct(
$retries = null,
callable $retryFunction = null,
callable $retryListener = null
) {
$this->retries = $retries !== null ? (int) $retries : 3;
$this->retryFunction = $retryFunction;
$this->retryListener = $retryListener;
// @todo revisit this approach
// @codeCoverageIgnoreStart
$this->delayFunction = static function ($delay) {
Expand All @@ -74,7 +92,6 @@ public function execute(callable $function, array $arguments = [])
$calcDelayFunction = $this->calcDelayFunction ?: [$this, 'calculateDelay'];
$retryAttempt = 0;
$exception = null;

while (true) {
try {
return call_user_func_array($function, $arguments);
Expand All @@ -91,6 +108,14 @@ public function execute(callable $function, array $arguments = [])

$delayFunction($calcDelayFunction($retryAttempt));
$retryAttempt++;
if ($this->retryListener) {
// Developer can modify the $arguments using the retryListener
// callback.
call_user_func_array(
$this->retryListener,
[$exception, $retryAttempt, &$arguments]
);
}
}
}

Expand Down
34 changes: 29 additions & 5 deletions Core/src/RequestWrapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;
use Google\ApiCore\AgentHeader;

/**
* The RequestWrapper is responsible for delivering and signing requests.
Expand Down Expand Up @@ -173,6 +174,11 @@ public function __construct(array $config = [])
* @type callable $restRetryFunction Sets the conditions for whether or
* not a request should attempt to retry. Function signature should
* match: `function (\Exception $ex) : bool`.
* @type callable $restRetryListener Runs after the restRetryFunction.
* This might be used to simply consume the exception and
* $arguments b/w retries. This returns the new $arguments thus
* allowing modification on demand for $arguments. For ex:
* changing the headers in b/w retries.
* @type callable $restDelayFunction Executes a delay, defaults to
* utilizing `usleep`. Function signature should match:
* `function (int $delay) : void`.
Expand All @@ -189,7 +195,8 @@ public function send(RequestInterface $request, array $options = [])
$retryOptions = $this->getRetryOptions($options);
$backoff = new ExponentialBackoff(
$retryOptions['retries'],
$retryOptions['retryFunction']
$retryOptions['retryFunction'],
$retryOptions['retryListener'],
);

if ($retryOptions['delayFunction']) {
Expand All @@ -202,7 +209,7 @@ public function send(RequestInterface $request, array $options = [])

try {
return $backoff->execute($this->httpHandler, [
$this->applyHeaders($request),
$this->applyHeaders($request, $options),
$this->getRequestOptions($options)
]);
} catch (\Exception $ex) {
Expand Down Expand Up @@ -252,7 +259,7 @@ public function sendAsync(RequestInterface $request, array $options = [])
}

return $asyncHttpHandler(
$this->applyHeaders($request),
$this->applyHeaders($request, $options),
$this->getRequestOptions($options)
)->then(null, function (\Exception $ex) use ($fn, $retryAttempt, $retryOptions) {
$shouldRetry = $retryOptions['retryFunction']($ex, $retryAttempt);
Expand All @@ -276,15 +283,29 @@ public function sendAsync(RequestInterface $request, array $options = [])
* Applies headers to the request.
*
* @param RequestInterface $request A PSR-7 request.
* @param array $options
* @return RequestInterface
*/
private function applyHeaders(RequestInterface $request)
private function applyHeaders(RequestInterface $request, array $options = [])
{
$headers = [
'User-Agent' => 'gcloud-php/' . $this->componentVersion,
'x-goog-api-client' => 'gl-php/' . PHP_VERSION . ' gccl/' . $this->componentVersion,
AgentHeader::AGENT_HEADER_KEY => sprintf(
'gl-php/%s gccl/%s',
PHP_VERSION,
$this->componentVersion
),
];

if (isset($options['retryHeaders'])) {
$headers[AgentHeader::AGENT_HEADER_KEY] = sprintf(
'%s %s',
$headers[AgentHeader::AGENT_HEADER_KEY],
implode(' ', $options['retryHeaders'])
);
unset($options['retryHeaders']);
}

if ($this->shouldSignRequest) {
$quotaProject = $this->quotaProject;
$token = null;
Expand Down Expand Up @@ -427,6 +448,9 @@ private function getRetryOptions(array $options)
'retryFunction' => isset($options['restRetryFunction'])
? $options['restRetryFunction']
: $this->retryFunction,
'retryListener' => isset($options['restRetryListener'])
? $options['restRetryListener']
: null,
'delayFunction' => isset($options['restDelayFunction'])
? $options['restDelayFunction']
: $this->delayFunction,
Expand Down
4 changes: 3 additions & 1 deletion Core/src/RestTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,10 @@ public function send($resource, $method, array $options = [], $whitelisted = fal
$requestOptions = $this->pluckArray([
'restOptions',
'retries',
'retryHeaders',
'requestTimeout',
'restRetryFunction'
'restRetryFunction',
'restRetryListener',
], $options);

try {
Expand Down
4 changes: 3 additions & 1 deletion Core/src/Upload/AbstractUploader.php
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,9 @@ public function __construct(
$this->requestOptions = array_intersect_key($options, [
'restOptions' => null,
'retries' => null,
'requestTimeout' => null
'requestTimeout' => null,
'restRetryFunction' => null,
'retryListener' => null
saranshdhingra marked this conversation as resolved.
Show resolved Hide resolved
]);

$this->contentType = $options['contentType'] ?? 'application/octet-stream';
Expand Down
33 changes: 33 additions & 0 deletions Core/tests/Unit/ExponentialBackoffTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -160,4 +160,37 @@ public function delayProvider()
[10, 60000000, 60000000]
];
}

/**
* Tests whether `retryListener()` callback is
* properly invoked when exception occurs in the request being made.
*/
public function testRetryListener()
{
$args = ['foo' => 'bar'];
$retryListener = function (
$ex,
$retryAttempt,
$arguments
) {
self::assertEquals(0, $retryAttempt);
self::assertEquals('bar', $arguments[0]['foo']);
};

// Setting $retries to 0 so that retry doesn't happens after first
// failure.
$backoff = new ExponentialBackoff(0, null, $retryListener);
try {
$backoff->execute(
function () {
throw new \Exception('Intentionally failing request');
},
[$args]
);
} catch (\Exception $err) {
// Do nothing.
// Catching the intentional failing call being made above:
// "Intentionally failing request"
}
}
}
35 changes: 35 additions & 0 deletions Core/tests/Unit/RequestWrapperTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -631,6 +631,41 @@ public function testEmptyTokenThrowsException()

$requestWrapper->send(new Request('GET', 'http://www.example.com'));
}

/**
* This test asserts that the retry related options and callbacks are
* properly mapped and set in the RequestWrapper's `$requestOptions`
* property.
*/
public function testRetryOptionsPassingInGetRetryOptions()
{
// Using Reflection instead of Prophecy because we want to test a
// private method's logic by verifying the output for a given input.
$requestWrapper = new RequestWrapper();
$reflection = new \ReflectionClass($requestWrapper);
$reflectionMethod = $reflection->getMethod('getRetryOptions');
$reflectionMethod->setAccessible(true);

$placeholderCallback = function () {
};
$options = [
'restRetryFunction' => $placeholderCallback,
'restRetryListener' => $placeholderCallback,
];

$result = $reflectionMethod->invoke($requestWrapper, $options);

// In the `getRetryOptions` method, the keys of $options are mapped after
// removing the prefix 'rest'. For example, 'restRetryFunction' becomes
// 'retryFunction'. Therefore, we take the substring after 4th character
// , convert first upper case character to lower and the use this as the
// new key for the respective value
foreach ($options as $key => $value) {
$key = lcfirst(substr($key, 4));
$this->assertArrayHasKey($key, $result);
$this->assertEquals($value, $result[$key]);
}
}
saranshdhingra marked this conversation as resolved.
Show resolved Hide resolved
}

//@codingStandardsIgnoreStart
Expand Down
31 changes: 31 additions & 0 deletions Core/tests/Unit/Upload/ResumableUploaderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,37 @@ public function testResumeFinishedUpload()
);
}

/**
* This tests whether retry related options are properly set in the
* abstract uploader class. Since we already had these tests for
* ResumableUploader class which extends the AbstractUploader class,
* thus testing it here would be sufficient.
*/
public function testRetryOptionsPassing()
{
$options = [
'restRetryFunction' => 'arg',
'retryListener' => 'arg'
];
$uploader = new ResumableUploader(
$this->requestWrapper->reveal(),
$this->stream,
'http://www.example.com',
$options
);

// Using Reflection instead of Prophecy because we want to test a
// private method's logic by verifying the output for a given input.
$reflection = new \ReflectionObject($uploader);
$requestOptionsProperty = $reflection->getProperty('requestOptions');
$requestOptionsProperty->setAccessible(true);
$requestOptions = $requestOptionsProperty->getValue($uploader);
foreach ($options as $key => $value) {
$this->assertArrayHasKey($key, $requestOptions);
$this->assertEquals($value, $requestOptions[$key]);
}
}
saranshdhingra marked this conversation as resolved.
Show resolved Hide resolved

public function testThrowsExceptionWhenAttemptsAsyncUpload()
{
$this->expectException(GoogleException::class);
Expand Down
4 changes: 3 additions & 1 deletion Storage/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
"require": {
"php": ">=7.4",
"google/cloud-core": "^1.43",
"google/crc32": "^0.1.0"
"google/crc32": "^0.1.0",
"ramsey/uuid": "^3.0|^4.0",
"google/gax": "^1.9"
saranshdhingra marked this conversation as resolved.
Show resolved Hide resolved
},
"require-dev": {
"phpunit/phpunit": "^9.0",
Expand Down
16 changes: 16 additions & 0 deletions Storage/phpunit-conformance.xml.dist
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
saranshdhingra marked this conversation as resolved.
Show resolved Hide resolved
<phpunit colors="true">
<testsuites>
<testsuite name="Conformance Test Suite">
<directory>tests/Conformance</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory suffix=".php">src</directory>
<exclude>
<directory suffix=".php">src/V[!a-zA-Z]*</directory>
</exclude>
</whitelist>
</filter>
</phpunit>
Loading