Skip to content

Commit bb64025

Browse files
authored
Merge pull request #35 from sliemeobn/add-sendable
Complete sendable-checking and refactoring for simpler state-handling
2 parents 946e3a8 + 6f6c337 commit bb64025

File tree

11 files changed

+713
-773
lines changed

11 files changed

+713
-773
lines changed

Package.resolved

Lines changed: 8 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
// swift-tools-version: 5.7
2-
// The swift-tools-version declares the minimum version of Swift required to build this package.
3-
1+
// swift-tools-version: 5.8
42
import PackageDescription
53

64
let package = Package(
@@ -12,14 +10,18 @@ let package = Package(
1210
dependencies: [
1311
.package(url: "https://github.com/apple/swift-nio.git", from: "2.48.0"),
1412
.package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.23.0"),
15-
.package(url: "https://github.com/apple/swift-collections.git", .upToNextMajor(from: "1.0.0"))
13+
.package(url: "https://github.com/apple/swift-collections.git", .upToNextMajor(from: "1.0.0")),
1614
],
1715
targets: [
1816
.target(
1917
name: "AMQPProtocol",
2018
dependencies: [
2119
.product(name: "NIOCore", package: "swift-nio"),
22-
]),
20+
],
21+
swiftSettings: [
22+
.enableUpcomingFeature("StrictConcurrency"),
23+
]
24+
),
2325
.target(
2426
name: "AMQPClient",
2527
dependencies: [
@@ -29,9 +31,17 @@ let package = Package(
2931
.product(name: "NIOConcurrencyHelpers", package: "swift-nio"),
3032
.product(name: "NIOSSL", package: "swift-nio-ssl"),
3133
.product(name: "Collections", package: "swift-collections"),
32-
]),
34+
],
35+
swiftSettings: [
36+
.enableUpcomingFeature("StrictConcurrency"),
37+
]
38+
),
3339
.testTarget(
3440
name: "AMQPClientTests",
35-
dependencies: ["AMQPClient"]),
41+
dependencies: ["AMQPClient"],
42+
swiftSettings: [
43+
.enableUpcomingFeature("StrictConcurrency"),
44+
]
45+
),
3646
]
3747
)

Sources/AMQPClient/AMQPChannel.swift

Lines changed: 145 additions & 144 deletions
Large diffs are not rendered by default.

Sources/AMQPClient/AMQPChannels.swift

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,34 +11,33 @@
1111
//
1212
//===----------------------------------------------------------------------===//
1313

14-
1514
struct AMQPChannels {
1615
private enum ChannelSlot {
1716
case reserved
1817
case channel(AMQPChannel)
1918
}
20-
19+
2120
private var channels: [UInt16: ChannelSlot] = [:]
22-
21+
2322
let channelMax: UInt16
24-
23+
2524
init(channelMax: UInt16) {
2625
self.channelMax = channelMax
2726
}
2827

2928
func get(id: UInt16) -> AMQPChannel? {
30-
guard case let .channel(channel) = self.channels[id] else {
29+
guard case let .channel(channel) = channels[id] else {
3130
return nil
3231
}
3332
return channel
3433
}
35-
34+
3635
mutating func reserveNext() -> UInt16? {
37-
guard self.channels.count < channelMax else {
36+
guard channels.count < channelMax else {
3837
return nil
3938
}
40-
41-
for i in 1...channelMax where channels[i] == nil {
39+
40+
for i in 1 ... channelMax where channels[i] == nil {
4241
channels[i] = .reserved
4342
return i
4443
}
@@ -47,10 +46,10 @@ struct AMQPChannels {
4746
}
4847

4948
mutating func add(channel: AMQPChannel) {
50-
self.channels[channel.ID] = .channel(channel)
49+
channels[channel.ID] = .channel(channel)
5150
}
5251

5352
mutating func remove(id: UInt16) {
54-
self.channels[id] = nil
53+
channels[id] = nil
5554
}
5655
}

Sources/AMQPClient/AMQPConnection.swift

Lines changed: 64 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -11,40 +11,36 @@
1111
//
1212
//===----------------------------------------------------------------------===//
1313

14+
import NIOConcurrencyHelpers
1415
import NIOCore
1516
import NIOPosix
1617
import NIOSSL
17-
import NIOConcurrencyHelpers
1818

19-
public final class AMQPConnection {
19+
public final class AMQPConnection: Sendable {
2020
internal enum ConnectionState {
2121
case open
2222
case shuttingDown
2323
case closed
2424
}
25-
25+
2626
public var isConnected: Bool {
2727
// `Channel.isActive` is set to false before the `closeFuture` resolves in cases where the channel might be
2828
// closed, or closing, before our state has been updated
29-
return self.channel.isActive && self.state.withLockedValue { $0 == .open }
29+
return channel.isActive && state.withLockedValue { $0 == .open }
3030
}
3131

32-
public var closeFuture: NIOCore.EventLoopFuture<Void> {
33-
return self.channel.closeFuture
34-
}
35-
36-
public var eventLoop: EventLoop { return self.channel.eventLoop }
32+
public var closeFuture: NIOCore.EventLoopFuture<Void> { connectionHandler.channel.closeFuture }
33+
public var eventLoop: EventLoop { return connectionHandler.channel.eventLoop }
3734

38-
private let channel: NIOCore.Channel
39-
private let multiplexer: AMQPConnectionMultiplexHandler
35+
private let connectionHandler: AMQPConnectionHandler
36+
private var channel: NIOCore.Channel { connectionHandler.channel }
4037

4138
private let state = NIOLockedValueBox(ConnectionState.open)
4239
private let channels: NIOLockedValueBox<AMQPChannels>
4340

44-
init(channel: NIOCore.Channel, multiplexer: AMQPConnectionMultiplexHandler, channelMax: UInt16) {
45-
self.channel = channel
46-
self.multiplexer = multiplexer
47-
self.channels = .init(AMQPChannels(channelMax: channelMax))
41+
init(connectionHandler: AMQPConnectionHandler, channelMax: UInt16) {
42+
self.connectionHandler = connectionHandler
43+
channels = .init(AMQPChannels(channelMax: channelMax))
4844
}
4945

5046
/// Connect to broker.
@@ -53,22 +49,15 @@ public final class AMQPConnection {
5349
/// - config: Configuration data.
5450
/// - Returns: EventLoopFuture with AMQP Connection.
5551
public static func connect(use eventLoop: EventLoop, from config: AMQPConnectionConfiguration) -> EventLoopFuture<AMQPConnection> {
56-
let promise = eventLoop.makePromise(of: AMQPResponse.self)
57-
let multiplexer = AMQPConnectionMultiplexHandler(eventLoop: eventLoop, config: config.server, onReady: promise)
58-
59-
return eventLoop.flatSubmit { () -> EventLoopFuture<AMQPConnection> in
60-
let result = self.boostrapChannel(use: eventLoop, from: config, with: multiplexer).flatMap { channel in
61-
promise.futureResult.flatMapThrowing { response in
62-
guard case .connection(let connection) = response, case .connected(let connected) = connection else {
63-
throw AMQPConnectionError.invalidResponse(response)
64-
}
65-
66-
return AMQPConnection(channel: channel, multiplexer: multiplexer, channelMax: connected.channelMax)
52+
eventLoop.flatSubmit {
53+
self.boostrapChannel(use: eventLoop, from: config).flatMap { connectionHandler in
54+
connectionHandler.startConnection().map {
55+
AMQPConnection(
56+
connectionHandler: connectionHandler,
57+
channelMax: $0.channelMax
58+
)
6759
}
6860
}
69-
70-
result.whenFailure { err in multiplexer.failAllResponses(because: err) }
71-
return result
7261
}
7362
}
7463

@@ -77,24 +66,22 @@ public final class AMQPConnection {
7766
/// Channel ID is automatically assigned (next free one).
7867
/// - Returns: EventLoopFuture with AMQP Channel.
7968
public func openChannel() -> EventLoopFuture<AMQPChannel> {
80-
guard self.isConnected else { return self.eventLoop.makeFailedFuture(AMQPConnectionError.connectionClosed()) }
69+
guard isConnected else { return eventLoop.makeFailedFuture(AMQPConnectionError.connectionClosed()) }
8170

8271
let channelID = channels.withLockedValue { $0.reserveNext() }
83-
72+
8473
guard let channelID = channelID else {
85-
return self.eventLoop.makeFailedFuture(AMQPConnectionError.tooManyOpenedChannels)
74+
return eventLoop.makeFailedFuture(AMQPConnectionError.tooManyOpenedChannels)
8675
}
8776

88-
return self.eventLoop.flatSubmit {
89-
let future = self.multiplexer.openChannel(id: channelID)
77+
let future = connectionHandler.openChannel(id: channelID)
9078

91-
future.whenFailure { _ in self.channels.withLockedValue { $0.remove(id: channelID) } }
92-
93-
return future.map { channel in
94-
let amqpChannel = AMQPChannel(channelID: channelID, eventLoop: self.eventLoop, channel: channel)
95-
self.channels.withLockedValue { $0.add(channel: amqpChannel) }
96-
return amqpChannel
97-
}
79+
future.whenFailure { _ in self.channels.withLockedValue { $0.remove(id: channelID) } }
80+
81+
return future.map { channel in
82+
let amqpChannel = AMQPChannel(channelID: channelID, eventLoop: self.eventLoop, channel: channel)
83+
self.channels.withLockedValue { $0.add(channel: amqpChannel) }
84+
return amqpChannel
9885
}
9986
}
10087

@@ -109,69 +96,64 @@ public final class AMQPConnection {
10996
state = .shuttingDown
11097
return true
11198
}
112-
99+
113100
return false
114101
}
115-
116-
guard shouldClose else { return self.channel.closeFuture }
117-
118-
return self.eventLoop.flatSubmit {
119-
let result = self.multiplexer.close(reason: reason, code: code)
120-
.map { () in
121-
return nil as Error?
102+
103+
guard shouldClose else { return closeFuture }
104+
105+
let result = connectionHandler.close(reason: reason, code: code)
106+
.map { () in
107+
nil as Error?
108+
}
109+
.recover { $0 }
110+
.flatMap { result in
111+
self.channel.close().map {
112+
self.state.withLockedValue { $0 = .closed }
113+
return (result, nil) as (Error?, Error?)
122114
}
123-
.recover { $0 }
124-
.flatMap { result in
125-
self.channel.close().map {
115+
.recover { error in
116+
if case ChannelError.alreadyClosed = error {
126117
self.state.withLockedValue { $0 = .closed }
127-
return (result, nil) as (Error?, Error?)
128-
}
129-
.recover { error in
130-
if case ChannelError.alreadyClosed = error {
131-
self.state.withLockedValue { $0 = .closed }
132-
return (result, nil)
133-
}
134-
135-
return (result, error)
118+
return (result, nil)
136119
}
120+
121+
return (result, error)
137122
}
138-
return result.flatMapThrowing {
139-
let (broker, conn) = $0
140-
if (broker ?? conn) != nil { throw AMQPConnectionError.connectionClose(broker: broker, connection: conn) }
141-
return ()
142123
}
124+
return result.flatMapThrowing {
125+
let (broker, conn) = $0
126+
if (broker ?? conn) != nil { throw AMQPConnectionError.connectionClose(broker: broker, connection: conn) }
127+
return ()
143128
}
144129
}
145130

146131
private static func boostrapChannel(
147132
use eventLoop: EventLoop,
148-
from config: AMQPConnectionConfiguration,
149-
with handler: AMQPConnectionMultiplexHandler
150-
) -> EventLoopFuture<NIOCore.Channel> {
151-
let channelPromise = eventLoop.makePromise(of: NIOCore.Channel.self)
152-
133+
from config: AMQPConnectionConfiguration
134+
) -> EventLoopFuture<AMQPConnectionHandler> {
153135
do {
154136
let bootstrap = try boostrapClient(use: eventLoop, from: config)
137+
let multiplexer = NIOLoopBound(
138+
AMQPConnectionMultiplexHandler(eventLoop: eventLoop, config: config.server),
139+
eventLoop: eventLoop)
155140

156-
bootstrap
141+
return bootstrap
157142
.channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)
158143
.channelOption(ChannelOptions.socket(IPPROTO_TCP, TCP_NODELAY), value: 1)
159144
.connectTimeout(config.server.timeout)
160145
.channelInitializer { channel in
161146
channel.pipeline.addHandlers([
162147
MessageToByteHandler(AMQPFrameEncoder()),
163148
ByteToMessageHandler(AMQPFrameDecoder()),
164-
handler
149+
multiplexer.value,
165150
])
166151
}
167152
.connect(host: config.server.host, port: config.server.port)
168-
.map { channelPromise.succeed($0) }
169-
.cascadeFailure(to: channelPromise)
153+
.map { AMQPConnectionHandler(channel: $0, multiplexer: multiplexer.value) }
170154
} catch {
171-
channelPromise.fail(error)
155+
return eventLoop.makeFailedFuture(error)
172156
}
173-
174-
return channelPromise.futureResult
175157
}
176158

177159
private static func boostrapClient(
@@ -182,17 +164,17 @@ public final class AMQPConnection {
182164
preconditionFailure("Cannot create bootstrap for the supplied EventLoop")
183165
}
184166

185-
switch config.connection {
186-
case .plain:
167+
switch config.connection {
168+
case .plain:
187169
return NIOClientTCPBootstrap(clientBootstrap, tls: NIOInsecureNoTLS())
188-
case .tls(let tls, let sniServerName):
170+
case let .tls(tls, sniServerName):
189171
let sslContext = try NIOSSLContext(configuration: tls ?? TLSConfiguration.clientDefault)
190172
let tlsProvider = try NIOSSLClientTLSProvider<ClientBootstrap>(context: sslContext, serverHostname: sniServerName ?? config.server.host)
191173
let bootstrap = NIOClientTCPBootstrap(clientBootstrap, tls: tlsProvider)
192174
return bootstrap.enableTLS()
193-
}
175+
}
194176
}
195-
177+
196178
deinit {
197179
if isConnected {
198180
assertionFailure("close() was not called before deinit!")

0 commit comments

Comments
 (0)