Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 7 additions & 0 deletions .changes/nextrelease/transport-sharing.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[
{
"type": "feature",
"category": "Handler",
"description": "Adds support for Guzzle transport sharing (persistent connections) via the new transport_sharing client option."
}
]
8 changes: 7 additions & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,11 @@ jobs:
composer-options: '--prefer-lowest'
- php-versions: '8.5'
composer-options: ''
- php-versions: '8.4'
composer-options: ''
guzzle-version: '>=7.11 <8'
# set the name for each job
name: PHP ${{ matrix.php-versions }} ${{ matrix.composer-options }}
name: PHP ${{ matrix.php-versions }} ${{ matrix.composer-options }} ${{ matrix.guzzle-version }}
# set up environment variables used by unit tests
env:
AWS_ACCESS_KEY_ID: foo
Expand Down Expand Up @@ -80,6 +83,9 @@ jobs:
else
composer update ${{ matrix.composer-options }} --no-interaction --prefer-dist
fi
if [[ -n "${{ matrix.guzzle-version }}" ]]; then
composer require "guzzlehttp/guzzle:${{ matrix.guzzle-version }}" --no-interaction --with-all-dependencies
fi

# run tests
- name: Run test suite
Expand Down
3 changes: 3 additions & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,7 @@ parameters:
# These exception classes only exist in Guzzle 8, but instances cannot occur under Guzzle 7
- '#Class GuzzleHttp\\Exception\\(NetworkException|ResponseException|ResponseTransferException) not found\.#'

# The transport sharing class only exists in Guzzle 7.11+ and 8; usage is guarded by feature detection
- '#Class GuzzleHttp\\TransportSharing not found\.#'

reportUnmatchedIgnoredErrors: false
7 changes: 7 additions & 0 deletions src/AwsClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,13 @@ public static function getArguments()
* signature version to use with a service (e.g., v4). Note that
* per/operation signature version MAY override this requested signature
* version.
* - transport_sharing: (string) Set to a transport sharing mode ("none",
* "handler_prefer", "handler_require", "persistent_prefer", or
* "persistent_require") to enable connection sharing on the default
* HTTP handler. The "*_prefer" modes degrade gracefully when the
* installed version of Guzzle or the runtime cannot honor them, and
* the "*_require" modes throw. This option only applies when the SDK
* creates the default HTTP handler.
* - use_aws_shared_config_files: (bool, default=bool(true)) Set to false to
* disable checking for shared config file in '~/.aws/config' and
* '~/.aws/credentials'. This will override the AWS_CONFIG_FILE
Expand Down
24 changes: 23 additions & 1 deletion src/ClientResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
use Aws\EndpointV2\EndpointProviderV2;
use Aws\Exception\AwsException;
use Aws\Exception\InvalidRegionException;
use Aws\Handler\HttpTransportSharing;
use Aws\Retry\ConfigurationInterface as RetryConfigInterface;
use Aws\Retry\ConfigurationProvider as RetryConfigProvider;
use Aws\Retry\V3\OptIn as NewRetriesOptIn;
Expand Down Expand Up @@ -293,6 +294,12 @@ class ClientResolver
'default' => [],
'doc' => 'Set to an array of SDK request options to apply to each request (e.g., proxy, verify, etc.).',
],
'transport_sharing' => [
'type' => 'value',
'valid' => ['string'],
'doc' => 'Set to a transport sharing mode ("none", "handler_prefer", "handler_require", "persistent_prefer", or "persistent_require") to enable connection sharing on the default HTTP handler. The "*_prefer" modes degrade gracefully when the installed version of Guzzle or the runtime cannot honor them, and the "*_require" modes throw. This option only applies when the SDK creates the default HTTP handler, and the "*_require" modes throw when combined with a custom "handler" or "http_handler" option.',
'fn' => [__CLASS__, '_apply_transport_sharing'],
],
'http_handler' => [
'type' => 'value',
'valid' => ['callable'],
Expand Down Expand Up @@ -988,7 +995,7 @@ public static function _apply_handler($value, array &$args, HandlerList $list)
public static function _default_handler(array &$args)
{
return new WrappedHttpHandler(
default_http_handler(),
default_http_handler($args['transport_sharing'] ?? null),
$args['parser'],
$args['error_parser'],
$args['exception_class'],
Expand All @@ -1007,6 +1014,21 @@ public static function _apply_http_handler($value, array &$args, HandlerList $li
);
}

public static function _apply_transport_sharing($value, array &$args)
{
HttpTransportSharing::validate($value);

if ((isset($args['http_handler']) || isset($args['handler']))
&& HttpTransportSharing::isRequired($value)
) {
throw new IAE('The "transport_sharing" option can only'
. ' require transport sharing when the SDK creates the'
. ' default HTTP handler. Remove the "handler" or'
. ' "http_handler" option, or configure transport sharing'
. ' on the custom handler instead.');
}
}

public static function _apply_app_id($value, array &$args)
{
// AppId should not be longer than 50 chars
Expand Down
21 changes: 17 additions & 4 deletions src/Handler/Guzzle/GuzzleHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
namespace Aws\Handler\Guzzle;

use Aws\Handler\HttpHandlerError;
use Aws\Handler\HttpTransportSharing;
use GuzzleHttp\Utils;
use GuzzleHttp\Promise;
use GuzzleHttp\Client;
Expand All @@ -18,11 +19,23 @@ class GuzzleHandler
private $client;

/**
* @param ClientInterface $client
* @param ClientInterface|null $client
* @param string|null $transportSharing
*/
public function __construct(?ClientInterface $client = null)
{
$this->client = $client ?: new Client();
public function __construct(
?ClientInterface $client = null,
?string $transportSharing = null
) {
if ($client !== null && HttpTransportSharing::isRequired($transportSharing)) {
throw new \InvalidArgumentException('The provided transport'
. ' sharing mode cannot require sharing when a client is'
. ' provided. Configure the "transport_sharing" option on'
. ' the provided client instead.');
}

$this->client = $client ?: new Client(
HttpTransportSharing::toClientConfig($transportSharing)
Comment thread
GrahamCampbell marked this conversation as resolved.
);
}

/**
Expand Down
118 changes: 118 additions & 0 deletions src/Handler/HttpTransportSharing.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
<?php
namespace Aws\Handler;

use GuzzleHttp\TransportSharing;

/**
* @internal
*/
final class HttpTransportSharing
{
private const NONE = 'none';
private const HANDLER_PREFER = 'handler_prefer';
private const HANDLER_REQUIRE = 'handler_require';
private const PERSISTENT_PREFER = 'persistent_prefer';
private const PERSISTENT_REQUIRE = 'persistent_require';

private const MODES = [
self::NONE,
self::HANDLER_PREFER,
self::HANDLER_REQUIRE,
self::PERSISTENT_PREFER,
self::PERSISTENT_REQUIRE,
];

public static function isRequired(?string $mode): bool
{
return $mode === self::HANDLER_REQUIRE
|| $mode === self::PERSISTENT_REQUIRE;
}

/**
* Validates a requested transport sharing mode without resolving it
* against the capabilities of the installed version of Guzzle.
*/
public static function validate(?string $mode): void
{
if ($mode !== null && !in_array($mode, self::MODES, true)) {
throw new \InvalidArgumentException('The provided transport'
. ' sharing mode "' . $mode . '" is invalid. Valid modes are:'
. ' "none", "handler_prefer", "handler_require",'
. ' "persistent_prefer", "persistent_require".');
}
}

/**
* Resolves a requested transport sharing mode to the mode that should be
* passed to the installed version of Guzzle, or null when no mode should
* be passed. The "*_prefer" modes degrade gracefully when the installed
* version of Guzzle cannot honor them, and the "*_require" modes throw.
*/
public static function resolve(?string $mode): ?string
{
self::validate($mode);

if ($mode === null || $mode === self::NONE) {
return null;
}

// Guzzle 8: all modes are understood, and Guzzle enforces the
// runtime requirements of the "*_require" modes itself.
if (self::supportsPersistentSharing()) {
return $mode;
}

// Guzzle 7.11+: handler-lifetime sharing only.
if (self::supportsHandlerSharing()) {
if ($mode === self::PERSISTENT_PREFER) {
return self::HANDLER_PREFER;
}

if ($mode === self::PERSISTENT_REQUIRE) {
throw new \RuntimeException('The "persistent_require"'
. ' transport sharing mode requires guzzlehttp/guzzle'
. ' ^8.0.');
}

return $mode;
}

// Guzzle < 7.11: no transport sharing support.
if ($mode === self::PERSISTENT_REQUIRE) {
throw new \RuntimeException('The "persistent_require" transport'
. ' sharing mode requires guzzlehttp/guzzle ^8.0.');
}

if ($mode === self::HANDLER_REQUIRE) {
throw new \RuntimeException('The "handler_require" transport'
. ' sharing mode requires guzzlehttp/guzzle ^7.11 || ^8.0.');
}

return null;
}

/**
* Resolves a requested transport sharing mode to a Guzzle client
* constructor configuration array.
*/
public static function toClientConfig(?string $mode): array
{
$mode = self::resolve($mode);

return $mode === null ? [] : ['transport_sharing' => $mode];
}

private static function supportsPersistentSharing(): bool
{
static $supported;

return $supported ??= defined(TransportSharing::class . '::PERSISTENT_PREFER');
}

private static function supportsHandlerSharing(): bool
{
static $supported;

return $supported ??= class_exists(TransportSharing::class);
}
}
5 changes: 4 additions & 1 deletion src/Sdk.php
Original file line number Diff line number Diff line change
Expand Up @@ -884,7 +884,10 @@ public function __construct(array $args = [])
$this->args = $args;

if (!isset($args['handler']) && !isset($args['http_handler'])) {
$this->args['http_handler'] = default_http_handler();
$this->args['http_handler'] = default_http_handler(
$args['transport_sharing'] ?? null
);
unset($this->args['transport_sharing']);
}
}

Expand Down
10 changes: 8 additions & 2 deletions src/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -270,11 +270,17 @@ function describe_type($input)
/**
* Creates a default HTTP handler based on the available clients.
*
* @param string|null $transportSharing Optional transport sharing mode
* ("none", "handler_prefer", "handler_require", "persistent_prefer",
* or "persistent_require") to apply to the underlying HTTP client.
* The "*_prefer" modes degrade gracefully when the installed version
* of Guzzle cannot honor them, and the "*_require" modes throw.
*
* @return callable
*/
function default_http_handler()
function default_http_handler(?string $transportSharing = null)
{
return new \Aws\Handler\Guzzle\GuzzleHandler();
return new \Aws\Handler\Guzzle\GuzzleHandler(null, $transportSharing);
}

/**
Expand Down
79 changes: 79 additions & 0 deletions tests/ClientResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -749,6 +749,85 @@ public function testCanAddHttpClientDefaultOptions()
$this->assertSame('bar', $conf['http']['foo']);
}

public function testAppliesTransportSharingToDefaultHandler()
{
$r = new ClientResolver(ClientResolver::getDefaultArguments());
$conf = $r->resolve([
'service' => 'sqs',
'region' => 'x',
'version' => 'latest',
'transport_sharing' => 'handler_prefer',
], new HandlerList());

$this->assertSame('handler_prefer', $conf['transport_sharing']);
}

public function testPreservesRequestedTransportSharingMode()
{
$r = new ClientResolver(ClientResolver::getDefaultArguments());
$conf = $r->resolve([
'service' => 'sqs',
'region' => 'x',
'version' => 'latest',
'transport_sharing' => 'persistent_prefer',
], new HandlerList());

$this->assertSame('persistent_prefer', $conf['transport_sharing']);
}

public function testTransportSharingCannotRequireWithCustomHandler()
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('can only require transport sharing');
$r = new ClientResolver(ClientResolver::getDefaultArguments());
$r->resolve([
'service' => 'sqs',
'region' => 'x',
'version' => 'latest',
'http_handler' => function () {},
'transport_sharing' => 'handler_require',
], new HandlerList());
}

#[DoesNotPerformAssertions]
public function testTransportSharingPreferIsIgnoredWithCustomHandler()
{
$r = new ClientResolver(ClientResolver::getDefaultArguments());
$r->resolve([
'service' => 'sqs',
'region' => 'x',
'version' => 'latest',
'http_handler' => function () {},
'transport_sharing' => 'persistent_prefer',
], new HandlerList());
}

public function testTransportSharingRejectsInvalidMode()
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('The provided transport sharing mode "always" is invalid.');
$r = new ClientResolver(ClientResolver::getDefaultArguments());
$r->resolve([
'service' => 'sqs',
'region' => 'x',
'version' => 'latest',
'transport_sharing' => 'always',
], new HandlerList());
}

public function testTransportSharingRejectsNonStringValue()
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Invalid configuration value provided for "transport_sharing". Expected string, but got int(1)');
$r = new ClientResolver(ClientResolver::getDefaultArguments());
$r->resolve([
'service' => 'sqs',
'region' => 'x',
'version' => 'latest',
'transport_sharing' => 1,
], new HandlerList());
}

public function testCanAddConfigOptions()
{
$c = new S3Client([
Expand Down
15 changes: 15 additions & 0 deletions tests/FunctionsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,21 @@ public function testGuzzleHttpHandler()
);
}

public function testGuzzleHttpHandlerWithTransportSharing()
{
if (!class_exists('GuzzleHttp\Handler\StreamHandler')) {
$this->markTestSkipped();
}
$this->assertInstanceOf(
Aws\Handler\Guzzle\GuzzleHandler::class,
Aws\default_http_handler('none')
);
$this->assertInstanceOf(
Aws\Handler\Guzzle\GuzzleHandler::class,
Aws\default_http_handler('persistent_prefer')
);
}

public function testSerializesHttpRequests()
{
$mock = new MockHandler([new Result([])]);
Expand Down
Loading