Skip to content

[ConnectionPool] HTTP1StateMachine #416

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
merged 5 commits into from
Sep 9, 2021

Conversation

fabianfett
Copy link
Member

@fabianfett fabianfett commented Sep 6, 2021

Follow up to #415, #417.

For the new HTTPConnectionPool a new StateMachine is handy.

@fabianfett fabianfett requested review from Lukasa and glbrntt September 6, 2021 16:17
@fabianfett fabianfett force-pushed the ff-pool-state-http1 branch 3 times, most recently from 23fadc0 to 395573d Compare September 7, 2021 10:00
@@ -375,6 +375,11 @@ extension HTTPConnectionPool {
self.connections[index].lease()
}

mutating func parkConnection(at index: Int) -> (Connection.ID, EventLoop) {
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.

Is there any reason this shouldn't be a precondition?

}
}

mutating func executeRequestOnPreferredEventLoop(_ request: Request, eventLoop: EventLoop) -> Action {
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 private?

)
}

mutating func executeRequestOnRequiredEventLoop(_ request: Request, eventLoop: EventLoop) -> Action {
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 private?

let requestAction: StateMachine.RequestAction = .scheduleRequestTimeout(
request.connectionDeadline,
for: requestID,
on: request.preferredEventLoop
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do we need to look this up here? We have an event loop in scope already.

let requestAction: StateMachine.RequestAction = .scheduleRequestTimeout(
request.connectionDeadline,
for: requestID,
on: request.preferredEventLoop
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do we need to look this up here? We have an event loop in scope already.

return .none
}

assert(self.state == .running, "If we are shutting down, we must not have any idle connections")
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?


// If we have remaining request queued, we should fail all of them with a cancelled
// error.
let waitingRequests = self.queue.removeAll().map { ($0, $0.id) }
Copy link
Collaborator

Choose a reason for hiding this comment

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

The intermediate tuple here seems a bit excessive: do we really need it?

Copy link
Member Author

@fabianfett fabianfett Sep 8, 2021

Choose a reason for hiding this comment

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

I create the tuple here to keep consistency between the two request actions:

case failRequest(Request, Error, cancelTimeout: Request.ID?)
case failRequests([(Request, cancelTimeout: Request.ID?)], Error)

If we fail multiple requests at the same time, we will always need to cancel their timeouts. So I preferred the beauty in having same looking Actions.

Copy link
Member Author

Choose a reason for hiding this comment

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

Chatted offline: Fixed now

return .none

case .shutDown:
preconditionFailure("It the pool is already shutdown, all connections must have been torn down.")
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
preconditionFailure("It the pool is already shutdown, all connections must have been torn down.")
preconditionFailure("If the pool is already shutdown, all connections must have been torn down.")

enum HTTPTypeStateMachine {
case http1(HTTP1StateMachine)

case _modifying
Copy link
Collaborator

Choose a reason for hiding this comment

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

We almost certainly shouldn't need this case. I know I haven't provided this feedback much prior to here, so don't feel compelled to remove it now, but we should probably go through these state machines and rip out the _modifying states wherever we have then. Recent Swift versions do not CoW here.

@fabianfett fabianfett added this to the HTTP/2 support milestone Sep 8, 2021
@fabianfett fabianfett force-pushed the ff-pool-state-http1 branch 2 times, most recently from 993fd7c to 1d99def Compare September 8, 2021 09:47
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.

(Partial review, will finish tomorrow.)

@@ -375,6 +375,11 @@ extension HTTPConnectionPool {
self.connections[index].lease()
}

mutating func parkConnection(at index: Int) -> (Connection.ID, EventLoop) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

This isn't actually a mutating func

// - 29 failed attempts: ~60s (max out)

let start = Double(TimeAmount.milliseconds(100).nanoseconds)
let backoffNanoseconds = Int64(start * pow(1.25, Double(self.failedConsecutiveConnectionAttempts)))
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
let backoffNanoseconds = Int64(start * pow(1.25, Double(self.failedConsecutiveConnectionAttempts)))
let backoffNanoseconds = Int64(start * pow(1.25, Double(attempt)))

return .none
}

private mutating func calculateBackoff(for attempt: Int) -> TimeAmount {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Don't think this is actually mutating?

}

// if we are not at max connections, we may want to create a new connection
if self.connections.startingGeneralPurposeConnections >= self.queue.count {
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 counting the wrong thing? This seems to risk starting a general purpose connection because we have lots of loop specific work, but without consulting what that loop specific work is.

at index: Int,
context: HTTP1Connections.FailedConnectionContext
) -> Action {
if context.connectionsStartingForUseCase < self.queue.count(for: nil) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Perhaps file an issue for future enhancement (no sense gluing it into this PR), but I don't love this spelling. Probably self.queue.generalPurposeCount would be clearer.

private var connections: HTTP1Connections
private var failedConsecutiveConnectionAttempts: Int = 0

private var queue: RequestQueue
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: requests or requestQueue provides a little more context

}

mutating func failedToCreateNewConnection(_ error: Error, connectionID: Connection.ID) -> Action {
defer {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Minor, but I find it a bit counter-intuitive to defer the increment here. It means that the first time we call this function we'll pass 0 to calculateBackoff which says that for 1 failed attempt backoff will 100ms. While that's true it's out of sync with the value of attempt we pass to it.

I think it'd be clearer to not defer the increment and to reframe calculateBackoff to be in terms failed attempts (rather than just "attempts") and just subtract 1 from the attempts when computing the unjittered backoff.

case http1(HTTP1StateMachine)
}

var state: HTTPTypeStateMachine
Copy link
Collaborator

Choose a reason for hiding this comment

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

Assuming HTTPTypeStateMachine will only have http1 and http2 cases (at least for now!) then httpVersion/httpType or similar might be a clearer name here since we're really just delegating to the state machine each case holds.

Comment on lines 179 to 180
guard !self.isShuttingDown else {
preconditionFailure("Shutdown must only be called once")
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: precondition(!self.isShuttingDown, "...")

Comment on lines 294 to 295
// in 10% of the cases, we require an explicit EventLoop.
// let elRequired = (0..<10).randomElement().flatMap { $0 == 0 ? true : false }!
Copy link
Collaborator

Choose a reason for hiding this comment

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

What's the deal here? (also in testBestConnectionIsPicked)

@fabianfett fabianfett requested review from Lukasa and glbrntt September 9, 2021 11:39
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.

LGTM!

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.

:shipit:

@fabianfett fabianfett merged commit 4d726ba into swift-server:main Sep 9, 2021
@fabianfett fabianfett added the 🆕 semver/minor Adds new public API. label Sep 9, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🆕 semver/minor Adds new public API.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants