Skip to content

Commit 1efd68e

Browse files
committed
HTTP1StateMachine
1 parent abdabf1 commit 1efd68e

9 files changed

+1508
-18
lines changed

Diff for: Sources/AsyncHTTPClient/ConnectionPool/State Machine/HTTPConnectionPool+HTTP1Connections.swift

+5-18
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,11 @@ extension HTTPConnectionPool {
375375
self.connections[index].lease()
376376
}
377377

378+
mutating func parkConnection(at index: Int) -> (Connection.ID, EventLoop) {
379+
assert(self.connections[index].isIdle)
380+
return (self.connections[index].connectionID, self.connections[index].eventLoop)
381+
}
382+
378383
/// A new HTTP/1.1 connection was released.
379384
///
380385
/// This will put the position into the idle state.
@@ -607,22 +612,4 @@ extension HTTPConnectionPool {
607612
var connecting: Int = 0
608613
var backingOff: Int = 0
609614
}
610-
611-
/// The pool cleanup todo list.
612-
struct CleanupContext: Equatable {
613-
/// the connections to close right away. These are idle.
614-
var close: [Connection]
615-
616-
/// the connections that currently run a request that needs to be cancelled to close the connections
617-
var cancel: [Connection]
618-
619-
/// the connections that are backing off from connection creation
620-
var connectBackoff: [Connection.ID]
621-
622-
init(close: [Connection] = [], cancel: [Connection] = [], connectBackoff: [Connection.ID] = []) {
623-
self.close = close
624-
self.cancel = cancel
625-
self.connectBackoff = connectBackoff
626-
}
627-
}
628615
}

Diff for: Sources/AsyncHTTPClient/ConnectionPool/State Machine/HTTPConnectionPool+HTTP1StateMachine.swift

+432
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the AsyncHTTPClient open source project
4+
//
5+
// Copyright (c) 2021 Apple Inc. and the AsyncHTTPClient project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of AsyncHTTPClient project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import NIO
16+
import NIOHTTP1
17+
18+
extension HTTPConnectionPool {
19+
struct StateMachine {
20+
struct Action {
21+
let request: RequestAction
22+
let connection: ConnectionAction
23+
24+
init(request: RequestAction, connection: ConnectionAction) {
25+
self.request = request
26+
self.connection = connection
27+
}
28+
29+
static let none: Action = Action(request: .none, connection: .none)
30+
}
31+
32+
enum ConnectionAction {
33+
enum IsShutdown: Equatable {
34+
case yes(unclean: Bool)
35+
case no
36+
}
37+
38+
case createConnection(Connection.ID, on: EventLoop)
39+
case scheduleBackoffTimer(Connection.ID, backoff: TimeAmount, on: EventLoop)
40+
41+
case scheduleTimeoutTimer(Connection.ID, on: EventLoop)
42+
case cancelTimeoutTimer(Connection.ID)
43+
44+
case closeConnection(Connection, isShutdown: IsShutdown)
45+
case cleanupConnections(CleanupContext, isShutdown: IsShutdown)
46+
47+
case none
48+
}
49+
50+
enum RequestAction {
51+
case executeRequest(Request, Connection, cancelTimeout: Request.ID?)
52+
case executeRequests([(Request, cancelTimeout: Request.ID?)], Connection)
53+
54+
case failRequest(Request, Error, cancelTimeout: Request.ID?)
55+
case failRequests([(Request, cancelTimeout: Request.ID?)], Error)
56+
57+
case scheduleRequestTimeout(NIODeadline?, for: Request.ID, on: EventLoop)
58+
case cancelRequestTimeout(Request.ID)
59+
60+
case none
61+
}
62+
63+
enum HTTPTypeStateMachine {
64+
case http1(HTTP1StateMachine)
65+
66+
case _modifying
67+
}
68+
69+
var state: HTTPTypeStateMachine
70+
var isShuttingDown: Bool = false
71+
72+
let eventLoopGroup: EventLoopGroup
73+
let maximumConcurrentHTTP1Connections: Int
74+
75+
init(eventLoopGroup: EventLoopGroup, idGenerator: Connection.ID.Generator, maximumConcurrentHTTP1Connections: Int) {
76+
self.maximumConcurrentHTTP1Connections = maximumConcurrentHTTP1Connections
77+
let http1State = HTTP1StateMachine(
78+
idGenerator: idGenerator,
79+
maximumConcurrentConnections: maximumConcurrentHTTP1Connections
80+
)
81+
self.state = .http1(http1State)
82+
self.eventLoopGroup = eventLoopGroup
83+
}
84+
85+
mutating func executeRequest(_ request: Request) -> Action {
86+
switch self.state {
87+
case .http1(var http1StateMachine):
88+
return self.state.modify { state -> Action in
89+
let action = http1StateMachine.executeRequest(request)
90+
state = .http1(http1StateMachine)
91+
return action
92+
}
93+
94+
case ._modifying:
95+
preconditionFailure("Invalid state: \(self.state)")
96+
}
97+
}
98+
99+
mutating func newHTTP1ConnectionCreated(_ connection: Connection) -> Action {
100+
switch self.state {
101+
case .http1(var httpStateMachine):
102+
return self.state.modify { state -> Action in
103+
let action = httpStateMachine.newHTTP1ConnectionEstablished(connection)
104+
state = .http1(httpStateMachine)
105+
return action
106+
}
107+
108+
case ._modifying:
109+
preconditionFailure("Invalid state: \(self.state)")
110+
}
111+
}
112+
113+
mutating func failedToCreateNewConnection(_ error: Error, connectionID: Connection.ID) -> Action {
114+
switch self.state {
115+
case .http1(var http1StateMachine):
116+
return self.state.modify { state -> Action in
117+
let action = http1StateMachine.failedToCreateNewConnection(
118+
error,
119+
connectionID: connectionID
120+
)
121+
state = .http1(http1StateMachine)
122+
return action
123+
}
124+
125+
case ._modifying:
126+
preconditionFailure("Invalid state: \(self.state)")
127+
}
128+
}
129+
130+
mutating func connectionCreationBackoffDone(_ connectionID: Connection.ID) -> Action {
131+
switch self.state {
132+
case .http1(var http1StateMachine):
133+
return self.state.modify { state -> Action in
134+
let action = http1StateMachine.connectionCreationBackoffDone(connectionID)
135+
state = .http1(http1StateMachine)
136+
return action
137+
}
138+
139+
case ._modifying:
140+
preconditionFailure("Invalid state: \(self.state)")
141+
}
142+
}
143+
144+
/// A request has timed out.
145+
///
146+
/// This is different to a request being cancelled. If a request times out, we need to fail the
147+
/// request, but don't need to cancel the timer (it already triggered). If a request is cancelled
148+
/// we don't need to fail it but we need to cancel its timeout timer.
149+
mutating func timeoutRequest(_ requestID: Request.ID) -> Action {
150+
switch self.state {
151+
case .http1(var http1StateMachine):
152+
return self.state.modify { state -> Action in
153+
let action = http1StateMachine.timeoutRequest(requestID)
154+
state = .http1(http1StateMachine)
155+
return action
156+
}
157+
158+
case ._modifying:
159+
preconditionFailure("Invalid state: \(self.state)")
160+
}
161+
}
162+
163+
/// A request was cancelled.
164+
///
165+
/// This is different to a request timing out. If a request is cancelled we don't need to fail it but we
166+
/// need to cancel its timeout timer. If a request times out, we need to fail the request, but don't
167+
/// need to cancel the timer (it already triggered).
168+
mutating func cancelRequest(_ requestID: Request.ID) -> Action {
169+
switch self.state {
170+
case .http1(var http1StateMachine):
171+
return self.state.modify { state -> Action in
172+
let action = http1StateMachine.cancelRequest(requestID)
173+
state = .http1(http1StateMachine)
174+
return action
175+
}
176+
177+
case ._modifying:
178+
preconditionFailure("Invalid state: \(self.state)")
179+
}
180+
}
181+
182+
mutating func connectionIdleTimeout(_ connectionID: Connection.ID) -> Action {
183+
switch self.state {
184+
case .http1(var http1StateMachine):
185+
return self.state.modify { state -> Action in
186+
let action = http1StateMachine.connectionIdleTimeout(connectionID)
187+
state = .http1(http1StateMachine)
188+
return action
189+
}
190+
191+
case ._modifying:
192+
preconditionFailure("Invalid state: \(self.state)")
193+
}
194+
}
195+
196+
/// A connection has been closed
197+
mutating func connectionClosed(_ connectionID: Connection.ID) -> Action {
198+
switch self.state {
199+
case .http1(var http1StateMachine):
200+
return self.state.modify { state -> Action in
201+
let action = http1StateMachine.connectionClosed(connectionID)
202+
state = .http1(http1StateMachine)
203+
return action
204+
}
205+
206+
case ._modifying:
207+
preconditionFailure("Invalid state: \(self.state)")
208+
}
209+
}
210+
211+
mutating func http1ConnectionReleased(_ connectionID: Connection.ID) -> Action {
212+
guard case .http1(var http1StateMachine) = self.state else {
213+
preconditionFailure("Invalid state: \(self.state)")
214+
}
215+
216+
return self.state.modify { state -> Action in
217+
let action = http1StateMachine.http1ConnectionReleased(connectionID)
218+
state = .http1(http1StateMachine)
219+
return action
220+
}
221+
}
222+
223+
mutating func shutdown() -> Action {
224+
guard !self.isShuttingDown else {
225+
preconditionFailure("Shutdown must only be called once")
226+
}
227+
228+
self.isShuttingDown = true
229+
230+
switch self.state {
231+
case .http1(var http1StateMachine):
232+
return self.state.modify { state -> Action in
233+
let action = http1StateMachine.shutdown()
234+
state = .http1(http1StateMachine)
235+
return action
236+
}
237+
238+
case ._modifying:
239+
preconditionFailure("Invalid state: \(self.state)")
240+
}
241+
}
242+
}
243+
}
244+
245+
extension HTTPConnectionPool {
246+
/// The pool cleanup todo list.
247+
struct CleanupContext: Equatable {
248+
/// the connections to close right away. These are idle.
249+
var close: [Connection]
250+
251+
/// the connections that currently run a request that needs to be cancelled to close the connections
252+
var cancel: [Connection]
253+
254+
/// the connections that are backing off from connection creation
255+
var connectBackoff: [Connection.ID]
256+
257+
init(close: [Connection] = [], cancel: [Connection] = [], connectBackoff: [Connection.ID] = []) {
258+
self.close = close
259+
self.cancel = cancel
260+
self.connectBackoff = connectBackoff
261+
}
262+
}
263+
}
264+
265+
extension HTTPConnectionPool.StateMachine.HTTPTypeStateMachine {
266+
mutating func modify<T>(_ closure: (inout Self) throws -> (T)) rethrows -> T {
267+
self = ._modifying
268+
defer {
269+
if case ._modifying = self {
270+
preconditionFailure("Invalid state. Use closure to modify state")
271+
}
272+
}
273+
return try closure(&self)
274+
}
275+
}
276+
277+
extension HTTPConnectionPool.StateMachine: CustomStringConvertible {
278+
var description: String {
279+
switch self.state {
280+
case .http1(let http1):
281+
return ".http1(\(http1))"
282+
283+
case ._modifying:
284+
preconditionFailure("Invalid state: \(self.state)")
285+
}
286+
}
287+
}

Diff for: Sources/AsyncHTTPClient/HTTPClient.swift

+8
Original file line numberDiff line numberDiff line change
@@ -925,6 +925,7 @@ public struct HTTPClientError: Error, Equatable, CustomStringConvertible {
925925
case tlsHandshakeTimeout
926926
case serverOfferedUnsupportedApplicationProtocol(String)
927927
case requestStreamCancelled
928+
case getConnectionFromPoolTimeout
928929
}
929930

930931
private var code: Code
@@ -997,4 +998,11 @@ public struct HTTPClientError: Error, Equatable, CustomStringConvertible {
997998
/// The remote server responded with a status code >= 300, before the full request was sent. The request stream
998999
/// was therefore cancelled
9991000
public static let requestStreamCancelled = HTTPClientError(code: .requestStreamCancelled)
1001+
1002+
/// Aquiring a HTTP connection from the connection pool timed out.
1003+
///
1004+
/// This can have multiple reasons:
1005+
/// - A connection could not be created within the timout period.
1006+
/// - Tasks are not processed fast enough on the existing connections, to process all waiters in time
1007+
public static let getConnectionFromPoolTimeout = HTTPClientError(code: .getConnectionFromPoolTimeout)
10001008
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the AsyncHTTPClient open source project
4+
//
5+
// Copyright (c) 2018-2019 Apple Inc. and the AsyncHTTPClient project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of AsyncHTTPClient project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
//
15+
// HTTPConnectionPool+HTTP1StateTests+XCTest.swift
16+
//
17+
import XCTest
18+
19+
///
20+
/// NOTE: This file was generated by generate_linux_tests.rb
21+
///
22+
/// Do NOT edit this file directly as it will be regenerated automatically when needed.
23+
///
24+
25+
extension HTTPConnectionPool_HTTP1StateMachineTests {
26+
static var allTests: [(String, (HTTPConnectionPool_HTTP1StateMachineTests) -> () throws -> Void)] {
27+
return [
28+
("testCreatingAndFailingConnections", testCreatingAndFailingConnections),
29+
("testConnectionFailureBackoff", testConnectionFailureBackoff),
30+
("testCancelRequestWorks", testCancelRequestWorks),
31+
("testExecuteOnShuttingDownPool", testExecuteOnShuttingDownPool),
32+
("testRequestsAreQueuedIfAllConnectionsAreInUseAndRequestsAreDequeuedInOrder", testRequestsAreQueuedIfAllConnectionsAreInUseAndRequestsAreDequeuedInOrder),
33+
("testBestConnectionIsPicked", testBestConnectionIsPicked),
34+
("testConnectionAbortIsIgnoredIfThereAreNoQueuedRequests", testConnectionAbortIsIgnoredIfThereAreNoQueuedRequests),
35+
("testConnectionCloseLeadsToTumbleWeedIfThereNoQueuedRequests", testConnectionCloseLeadsToTumbleWeedIfThereNoQueuedRequests),
36+
("testConnectionAbortLeadsToNewConnectionsIfThereAreQueuedRequests", testConnectionAbortLeadsToNewConnectionsIfThereAreQueuedRequests),
37+
("testParkedConnectionTimesOut", testParkedConnectionTimesOut),
38+
("testConnectionPoolFullOfParkedConnectionsIsShutdownImmediately", testConnectionPoolFullOfParkedConnectionsIsShutdownImmediately),
39+
("testParkedConnectionTimesOutButIsAlsoClosedByRemote", testParkedConnectionTimesOutButIsAlsoClosedByRemote),
40+
]
41+
}
42+
}

0 commit comments

Comments
 (0)