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

[WIP] Connection Pool Refactor #182

Merged
merged 32 commits into from
Jul 31, 2019
Merged

[WIP] Connection Pool Refactor #182

merged 32 commits into from
Jul 31, 2019

Conversation

trowski
Copy link
Member

@trowski trowski commented Jun 17, 2019

Currently a WIP. I still need to refine the structure a bit more and add some needed functionality to how the pool handles returning connections (e.g.: keep-alive vs. close connections).

{
$this->socketPool = $socketPool ?? new UnlimitedSocketPool;
$this->connector = $connector ?? new DnsConnector;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should fallback to connector() instead.

public function createConnection(Socket $socket): Connection
{
$connection = new Http1Connection($socket);
$this->connections[$socket->getRemoteAddress()->toString()] = $connection;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to say this only allows one concurrent connection per host, but actually this works, because it has the port in it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So it allows one concurrent connection per host per port, which isn't really enough for HTTP/1.x, but works fine for HTTP/2. This is one of the areas where I'm not exactly sure how I want to approach the design.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ugh, well, right. I think I'd add Connection::isBusy and ConnectionPool chooses the next connection then or creates a new one if none is available. This also allows limiting the number of requests per HTTP/2 connection then, so the connection manager creates a separate HTTP/2 connection at some point.

The major issue here is that we can't use the remote address as key, because different sites might be hosted on the same IP, but due to SNI there might be different TLS certificates served.


$address = $uri->getHost();
$port = $uri->getPort();
$authority = $request->getUri()->getHost() . ':' . ($request->getUri()->getPort() ?: $defaultPort);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably error here if we didn't get an absolute URI.

if ($port !== null) {
$address .= ':' . $port;
}
if (isset($this->connections[$authority])) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also include the scheme in the key here, there are very rare cases where requests with http and https are supported on the same port.

return yield from $this->doRead($request, $cancellation, $readingCancellation, $completionDeferred);
$response = yield from $this->doRead($request, $cancellation, $readingCancellation, $completionDeferred);

$this->busy = false;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is too early. You need to subscribe to the completion promise in Response instead, because at this point we have only read the header, not the body.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was intentional, since another request can be made while a prior response body is being streamed… but that could cause another request to be severely delayed, so I'll use the completion promise instead.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I know no browser supports pipelining, so we probably shouldn't either.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do in http-server (because we should to comply with the RFC I think…), but in the client, I agree.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, for http-server we need to support it.

src/Response.php Outdated
@@ -33,7 +36,7 @@ public function __construct(
array $headers,
InputStream $body,
Request $request,
ConnectionInfo $connectionInfo,
Connection $connection,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Response shouldn't have a reference to the connection itself.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose not… I was hoping to just drop ConnectionInfo, but maybe I'll have to keep it after all. Does it make sense to pass a Connection to NetworkInterceptor too?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NetworkInterceptor can receive Connection, that's fine. We'll have to keep ConnectionInfo for Response or remove support for it in Response and if you want to access it, you have to implement a NetworkInterceptor.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I like the second option, since 99.9% of the time you don't care about that information.

src/Connection/DefaultConnectionPool.php Outdated Show resolved Hide resolved
unset($this->connections[$key]);
}
return;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We only push the connection to the array two lines below, so there shouldn't be any entry here to delete.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The promise is pushed to the array when it's created, so that needs to be deleted.

src/Connection/Http1Connection.php Outdated Show resolved Hide resolved
return 0;
}

if (\strcasecmp($responseConnHeader, 'keep-alive')) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be checking against close, too, as keep-alive is the default for HTTP/1.1.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the value is anything other than 'keep-alive' the connection will be closed.

src/Connection/Http1Connection.php Outdated Show resolved Hide resolved
@@ -75,6 +75,9 @@ final class Http2Connection implements Connection
public const INADEQUATE_SECURITY = 0xc;
public const HTTP_1_1_REQUIRED = 0xd;

public const DEFAULT_MAX_HEADER_SIZE = 1 << 20;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1MB for headers is a bit much, no?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably. We'll have to look at if and how we want these settings to be configured and pick some defaults.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should not matter too much as a client. TBH, if you do random requests to not-somewhat-known sites, then you need sanity checks at every layer.

*
* @return Promise<Response>
*/
public function request(Request $request, ?CancellationToken $token = null): Promise;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CancellationToken isn't optional in ConnectionPool, so probably can be required here, too?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, the Client object should take care of creating one.

}
}

++$index;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't safe. Suppose we have 3 concurrent connections which all allow just one request. Due to the yielding above, the second and third both have $index = 0 and hang in the yield until the connection is established. Both push to $index = 1 then.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, right, since the new connections won't be added to the list used by foreach. Hmm… might have to use a queue or something where that would be the case.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd just do a keyless push and query the last key.

Also actually use it in Http2Connection. :-)
src/Response.php Outdated Show resolved Hide resolved
src/Connection/Http2Connection.php Outdated Show resolved Hide resolved
src/SocketClient.php Outdated Show resolved Hide resolved
@trowski trowski merged commit 39b5a3b into master Jul 31, 2019
@trowski trowski temporarily deployed to github-pages July 31, 2019 20:06 Inactive
@kelunik kelunik deleted the connection-refactor branch July 31, 2019 20:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants