Skip to content

Add new HTTPConnectionPool #418

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

Closed
wants to merge 3 commits into from

Conversation

fabianfett
Copy link
Member

Follow up to #416

@fabianfett fabianfett force-pushed the ff-connection-pool branch 2 times, most recently from 48dd7a3 to 3c96411 Compare September 8, 2021 22:44
@fabianfett fabianfett added this to the HTTP/2 support milestone Sep 9, 2021
@fabianfett fabianfett requested a review from glbrntt September 9, 2021 12:29
@fabianfett fabianfett force-pushed the ff-connection-pool branch 2 times, most recently from 9beffc7 to 4011291 Compare September 9, 2021 13:48
@fabianfett fabianfett requested a review from Lukasa September 9, 2021 13:50
@fabianfett fabianfett force-pushed the ff-connection-pool branch 2 times, most recently from 3aba015 to 5d707f7 Compare September 9, 2021 14:10
case .executeRequest(let request, let connection, cancelTimeout: let cancelTimeout):
connection.executeRequest(request.req)
if cancelTimeout {
self.cancelRequestTimeout(request.id)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should these two operations be in the other order?

Copy link
Member Author

Choose a reason for hiding this comment

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

Added a code comment.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think I disagree with your code comment. If we're about to execute a request then we should aim to cancel the timeout first, as this will make some small number of requests succeed where the other order would fail.

Copy link
Member Author

@fabianfett fabianfett Sep 10, 2021

Choose a reason for hiding this comment

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

Let's look at the definition of the request timeouts.

When a request timeout fires, it will tell the state machine about it having fired. The state machine will then look for the request in its own request queue. If it doesn't find the request in there, it knows that the request was already removed (probably in another thread) before. The action will for this reason be none.

For this reason the order in calling the request actions does not matter. The only place where we make decisions about failing requests is in the state machine.

From the HTTPConnectionPool:

    private func scheduleRequestTimeout(_ request: Request, on eventLoop: EventLoop) {
        let requestID = request.id
        let scheduled = eventLoop.scheduleTask(deadline: request.connectionDeadline) {
            // The timer has fired. Now we need to do a couple of things:
            //
            // 1. Remove ourselves from the timer dictionary to not leak any data. If our
            //    waiter entry still exists, we need to tell the state machine, that we want
            //    to fail the request.
            let timeoutFired = self.timerLock.withLock {
                self._requestTimer.removeValue(forKey: requestID) != nil
            }

            // 2. If the entry did not exists anymore, we can assume that the request was
            //    scheduled on another connection. The timer still fired anyhow because of a
            //    race. In such a situation we don't need to do anything.
            guard timeoutFired else { return }

            // 3. Tell the state machine about the timeout
            let action = self.stateLock.withLock {
                self._state.timeoutRequest(requestID)
            }

            self.run(action: action)
        }

        self.timerLock.withLockVoid {
            assert(self._requestTimer[requestID] == nil)
            self._requestTimer[requestID] = scheduled
        }

        request.req.requestWasQueued(self)
    }

From the StateMachine:

        mutating func timeoutRequest(_ requestID: Request.ID) -> Action {
            // 1. check requests in queue
            if let request = self.requests.remove(requestID) {
                return .init(
                    request: .failRequest(request, HTTPClientError.getConnectionFromPoolTimeout, cancelTimeout: false),
                    connection: .none
                )
            }

            // 2. This point is reached, because the request may have already been scheduled. A
            //    connection might have become available shortly before the request timeout timer
            //    fired.
            return .none
        }

case .executeRequestsAndCancelTimeouts(let requests, let connection):
for request in requests {
connection.executeRequest(request.req)
self.cancelRequestTimeout(request.id)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Same ordering question here.

Copy link
Collaborator

Choose a reason for hiding this comment

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

For efficiency reasons should we be able to do a "bulk" request timeout cancellation? We have to acquire locks in order to do this (both the timerLock and the event loop locks) so it may be useful to try to coalesce work where we can to try to reduce the pressure on those locks.

connectionID: connectionID,
http1ConnectionDelegate: self,
http2ConnectionDelegate: self,
deadline: .now() + (self.clientConfiguration.timeout.connect ?? .seconds(30)),
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can this .seconds(30) be lifted out to a static so that we can see it clearly and give it a name?

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.

This is coming together nicely! It's quite mechanical so it's reasonably easy to review as well.

func connectionPoolDidShutdown(_ pool: HTTPConnectionPool, unclean: Bool)
}

class HTTPConnectionPool {
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
class HTTPConnectionPool {
final class HTTPConnectionPool {

@@ -121,6 +128,364 @@ enum HTTPConnectionPool {
}
}
}

let stateLock = Lock()
Copy link
Collaborator

Choose a reason for hiding this comment

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

private?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Not actioned.

}
}

let timerLock = Lock()
Copy link
Collaborator

Choose a reason for hiding this comment

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

private?


case .closeConnection(let connection, isShutdown: let isShutdown):
// we are not interested in the close future...
_ = connection.close()
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should we add an equivalent close API accepting an optional promise and use that instead?

Copy link
Member Author

Choose a reason for hiding this comment

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

Great idea. Promise is enough I guess :)

self.run(action: action)
}

func run(action: StateMachine.Action) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

private?

func runRequestAction(_ action: StateMachine.RequestAction) {
switch action {
case .executeRequest(let request, let connection, cancelTimeout: let cancelTimeout):
connection.executeRequest(request.req)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is the ordering of execute and cancelling the timeout done for any particular reason?

// MARK: Run actions

private func createConnection(_ connectionID: Connection.ID, on eventLoop: EventLoop) {
self.connectionFactory.makeConnection(
Copy link
Collaborator

Choose a reason for hiding this comment

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

This API wasn't too clear to me. In my mind "makeThing" should return a "Thing" to the caller. Consider a different name / dropping a comment in here.

// The timer has fired. Now we need to do a couple of things:
//
// 1. Remove ourselves from the timer dictionary to not leak any data. If our
// waiter entry still exist, we need to tell the state machine, that we want
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
// waiter entry still exist, we need to tell the state machine, that we want
// waiter entry still exists, we need to tell the state machine, that we want

// waiter entry still exist, we need to tell the state machine, that we want
// to fail the request.

let timeout = self.timerLock.withLock {
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: timeoutFired / timedOut

@fabianfett fabianfett force-pushed the ff-connection-pool branch 2 times, most recently from 5cefa51 to 40b6851 Compare September 10, 2021 07:03
Co-authored-by: George Barnett <[email protected]>
@@ -121,6 +128,364 @@ enum HTTPConnectionPool {
}
}
}

let stateLock = Lock()
Copy link
Collaborator

Choose a reason for hiding this comment

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

Not actioned.

}
}

static let fallbackConnectTimeout: TimeAmount = .seconds(30)
Copy link
Collaborator

Choose a reason for hiding this comment

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

private here too.

case .executeRequest(let request, let connection, cancelTimeout: let cancelTimeout):
connection.executeRequest(request.req)
if cancelTimeout {
self.cancelRequestTimeout(request.id)
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think I disagree with your code comment. If we're about to execute a request then we should aim to cancel the timeout first, as this will make some small number of requests succeed where the other order would fail.


private func createConnection(_ connectionID: Connection.ID, on eventLoop: EventLoop) {
// Even though this function is called make it actually creates/establishes a connection.
// TBD: Should we rename it? To what?
Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe just changing "make" to "create" is enough to differentiate make it clear that it's not a factory. Alternatively something like provideNewConnection(to: self, ...)?

@fabianfett fabianfett added the 🔨 semver/patch No public API change. label Sep 10, 2021
@fabianfett
Copy link
Member Author

Superseeded by #420

@fabianfett fabianfett closed this Sep 10, 2021
@fabianfett fabianfett deleted the ff-connection-pool branch September 14, 2021 09:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🔨 semver/patch No public API change.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants