Skip to content

HTTP1Connections state machine for ConnectionPool #413

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

Merged

Conversation

fabianfett
Copy link
Member

If we have HTTP/1 connections, a data structure to store them is handy.

Changes

  • Adds a HTTPConnectionPool.HTTP1Connections struct to store HTTP1 connections

Result

Less code in the connection pool state machine. Shared code between HTTP/1 and HTTP/2 connection pool state machine.

@fabianfett fabianfett force-pushed the ff-pool-state-http1-connections branch from 3dcb4e0 to 85d48fa Compare September 2, 2021 13:51
@fabianfett fabianfett requested review from glbrntt and Lukasa September 2, 2021 13:52
@fabianfett fabianfett added the semver/none No version bump required. label Sep 2, 2021
@fabianfett fabianfett force-pushed the ff-pool-state-http1-connections branch from 85d48fa to 66c9b5f Compare September 2, 2021 14:07
Copy link
Collaborator

@glbrntt glbrntt left a comment

Choose a reason for hiding this comment

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

I haven't looked at the tests -- mostly looks good, left a handful of questions and minor suggestions.

/// Represents the state of a single HTTP/1.1 connection
private struct HTTP1ConnectionState {
enum State {
/// the connection is creating a connection. Valid transitions are to: .backingOff, .available and .failed
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is .failed meant to be .closed?

enum State {
/// the connection is creating a connection. Valid transitions are to: .backingOff, .available and .failed
case starting
/// the connection is waiting to retry the establishing a connection. Transition to .closed. Must be replaced for a retry.
Copy link
Collaborator

Choose a reason for hiding this comment

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

What do you mean by "Must be replaced for a retry" ?

}

private var state: State
private(set) var connectionID: Connection.ID
Copy link
Collaborator

Choose a reason for hiding this comment

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

When does this change, when we retry after backing off?

}
}

mutating func cleanup(_ context: inout CleanupContext) -> Bool {
Copy link
Collaborator

Choose a reason for hiding this comment

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

What does the Bool represent?

}
}

mutating func cleanup(_ context: inout CleanupContext) -> Bool {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Also it doesn't look like this is actually mutating?

guard let index = self.connections.firstIndex(where: { $0.connectionID == connectionID }) else {
// because of a race this connection (connection close runs against trigger of timeout)
// was already removed from the state machine.
return .none
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: nil (let's be consistent within a single func 🙂)

/// the connection is waiting to retry the establishing a connection. Transition to .closed. Must be replaced for a retry.
case backingOff
/// the connection is available for a new request. Valid transitions to: .leased and .closed
case available(Connection, since: NIODeadline)
Copy link
Collaborator

Choose a reason for hiding this comment

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

"idle" and "available" are both used through this PR to refer to the same thing, can we stick to one?


/// Remove or replace a connection.
///
/// Call this function after the connection start backoff is done, or a live connection is closed.
Copy link
Collaborator

Choose a reason for hiding this comment

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

So do we expect the state to be one of available, leased or backingOff?

if index < self.overflowIndex {
use = .generalPurpose
// we need to subtract 1, since we dont want this connection to count
starting = self.startingGeneralPurposeConnections - 1
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is this right? If we're a live connection that's been closed then we won't be in the starting state.

@fabianfett fabianfett force-pushed the ff-pool-state-http1-connections branch 3 times, most recently from 137c647 to 9463b80 Compare September 2, 2021 22:16
@fabianfett fabianfett added this to the HTTP/2 support milestone Sep 3, 2021
@fabianfett fabianfett force-pushed the ff-pool-state-http1-connections branch from 9463b80 to b9ab9af Compare September 3, 2021 08:24
@fabianfett fabianfett requested a review from glbrntt September 3, 2021 08:24
@fabianfett fabianfett force-pushed the ff-pool-state-http1-connections branch from b9ab9af to 9d8037d Compare September 3, 2021 09:15
/// Represents the state of a single HTTP/1.1 connection
private struct HTTP1ConnectionState {
enum State {
/// the connection is creating a connection. Valid transitions are to: .idle, .available and .closed
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nit: .available doesn't exist.

enum State {
/// the connection is creating a connection. Valid transitions are to: .idle, .available and .closed
case starting
/// the connection is waiting to retry the establishing a connection. Transition to .closed. From .closed
Copy link
Collaborator

Choose a reason for hiding this comment

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

What does "Transition to .closed." mean here?

/// or fail until we finalize the shutdown.
///
/// - Parameter context: A cleanup context to add the connection to based on its state.
/// - Returns: A bool weather the connection can be removed from the connections
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nit:

Suggested change
/// - Returns: A bool weather the connection can be removed from the connections
/// - Returns: A bool tracking whether the connection can be removed from the connections

/// - Parameter context: A cleanup context to add the connection to based on its state.
/// - Returns: A bool weather the connection can be removed from the connections
/// struct immediately.
func cleanup(_ context: inout CleanupContext) -> Bool {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't love "mystery booleans" in contexts like this: I'd normally prefer us to add an enum that makes it clear what the return values actually mean.


mutating func closeConnection(at index: Int) -> Connection {
var connectionState = self.connections.remove(at: index)
self.overflowIndex = self.connections.index(before: self.overflowIndex)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is this unconditional decrement correct? We don't do this for removeConnection, we conditionalise it there.

}

mutating func replaceConnection(at index: Int) -> (Connection.ID, EventLoop) {
assert(self.connections[index].isClosed)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Any reason this isn't a precondition?

/// supplied index after this.
mutating func failConnection(_ connectionID: Connection.ID) -> (Int, FailedConnectionContext) {
guard let index = self.connections.firstIndex(where: { $0.connectionID == connectionID }) else {
preconditionFailure("We tried to create a new connection that we know nothing about?")
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
preconditionFailure("We tried to create a new connection that we know nothing about?")
preconditionFailure("We tried to fail a connection that we know nothing about?")

self.connections = self.connections.enumerated().compactMap { index, connectionState in
if connectionState.cleanup(&cleanupContext) {
if index < initialOverflowIndex {
self.overflowIndex -= 1
Copy link
Collaborator

Choose a reason for hiding this comment

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

You're inconsistent about using index(before:) in this spot. We should be consistent.

// MARK: - Private functions -

private func generateIdleConnectionContextForConnection(at index: Int) -> IdleConnectionContext {
assert(self.connections[index].isIdle)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Any reason this isn't a precondition?

@fabianfett fabianfett requested review from Lukasa and glbrntt September 3, 2021 13:08
@fabianfett fabianfett force-pushed the ff-pool-state-http1-connections branch from fea4b49 to e80b55d Compare September 3, 2021 13:55
Copy link
Collaborator

@glbrntt glbrntt left a comment

Choose a reason for hiding this comment

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

Looks great!

…ctionPool+HTTP1Connections.swift

Co-authored-by: George Barnett <[email protected]>
Copy link
Collaborator

@Lukasa Lukasa left a comment

Choose a reason for hiding this comment

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

One note, otherwise LGTM.

return self.connections[self.overflowIndex..<self.connections.endIndex].reduce(into: 0) { count, connection in
guard connection.eventLoop === eventLoop else { return }
if connection.isConnecting || connection.isBackingOff {
count += 1
Copy link
Collaborator

Choose a reason for hiding this comment

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

This can also be unchecked for the same reasons as above.

@fabianfett fabianfett merged commit 7d3c578 into swift-server:main Sep 6, 2021
@fabianfett fabianfett deleted the ff-pool-state-http1-connections branch September 6, 2021 10:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
semver/none No version bump required.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants