Skip to content

Commit 04c4bf9

Browse files
committed
Mark Sendable for NetworkContext's external async calls
1 parent 5fed088 commit 04c4bf9

24 files changed

Lines changed: 389 additions & 296 deletions

Sources/SwiftNetwork/Connection/Connection.swift

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2085,7 +2085,7 @@ extension NetworkChannel where ApplicationProtocol: MessageProtocol {
20852085

20862086
@_spi(Essentials)
20872087
@available(Network 0.1.0, *)
2088-
extension NetworkChannel where ApplicationProtocol: StreamProtocol {
2088+
extension NetworkChannel where ApplicationProtocol: StreamProtocol & Sendable {
20892089
public struct StreamMessage {
20902090
public static func message(content: [UInt8]? = nil, isComplete: Bool = false) -> StreamMessage {
20912091
StreamMessage(content: content, isComplete: isComplete)
@@ -2097,10 +2097,12 @@ extension NetworkChannel where ApplicationProtocol: StreamProtocol {
20972097

20982098
public func send(_ message: StreamMessage, completion: (@Sendable (Result<Void, NetworkError>) -> Void)? = nil) {
20992099
let endpointFlow = self.endpointFlow
2100+
let content = message.content
2101+
let isComplete = message.isComplete
21002102
endpointFlow.async {
21012103
let writeRequest = WriteRequest(
2102-
content: message.content,
2103-
isComplete: message.isComplete,
2104+
content: content,
2105+
isComplete: isComplete,
21042106
completion: completion
21052107
)
21062108
endpointFlow.addWriteRequestOnContext(writeRequest)
@@ -2114,10 +2116,12 @@ extension NetworkChannel where ApplicationProtocol: StreamProtocol {
21142116
completion: (@Sendable (Result<Void, NetworkError>) -> Void)? = nil
21152117
) {
21162118
let endpointFlow = self.endpointFlow
2119+
nonisolated(unsafe) let unsafeBuffer = buffer
2120+
nonisolated(unsafe) let unsafeOwner = owner
21172121
endpointFlow.async {
21182122
let writeRequest = WriteRequest(
2119-
buffer: buffer,
2120-
owner: owner,
2123+
buffer: unsafeBuffer,
2124+
owner: unsafeOwner,
21212125
isComplete: isComplete,
21222126
completion: completion
21232127
)
@@ -2144,7 +2148,7 @@ extension NetworkChannel where ApplicationProtocol: StreamProtocol {
21442148

21452149
@_spi(Essentials)
21462150
@available(Network 0.1.0, *)
2147-
extension NetworkChannel where ApplicationProtocol: DatagramProtocol {
2151+
extension NetworkChannel where ApplicationProtocol: DatagramProtocol & Sendable {
21482152
public struct DatagramMessage {
21492153
public static func message(content: [UInt8]? = nil) -> DatagramMessage {
21502154
DatagramMessage(content: content)
@@ -2155,8 +2159,9 @@ extension NetworkChannel where ApplicationProtocol: DatagramProtocol {
21552159

21562160
public func send(_ message: DatagramMessage, completion: (@Sendable (Result<Void, NetworkError>) -> Void)? = nil) {
21572161
let endpointFlow = self.endpointFlow
2162+
let content = message.content
21582163
endpointFlow.async {
2159-
let writeRequest = WriteRequest(content: message.content, isComplete: true, completion: completion)
2164+
let writeRequest = WriteRequest(content: content, isComplete: true, completion: completion)
21602165
endpointFlow.addWriteRequestOnContext(writeRequest)
21612166
}
21622167
}
@@ -2168,10 +2173,12 @@ extension NetworkChannel where ApplicationProtocol: DatagramProtocol {
21682173
completion: (@Sendable (Result<Void, NetworkError>) -> Void)? = nil
21692174
) {
21702175
let endpointFlow = self.endpointFlow
2176+
nonisolated(unsafe) let unsafeBuffer = buffer
2177+
nonisolated(unsafe) let unsafeOwner = owner
21712178
endpointFlow.async {
21722179
let writeRequest = WriteRequest(
2173-
buffer: buffer,
2174-
owner: owner,
2180+
buffer: unsafeBuffer,
2181+
owner: unsafeOwner,
21752182
isComplete: isComplete,
21762183
completion: completion
21772184
)

Sources/SwiftNetwork/Context/NetworkContext.swift

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,9 @@ public protocol NetworkContextProtocol: AnyObject, Hashable {
4646
init(identifier: String)
4747
var identifier: String { get }
4848
func activate()
49-
func async(_ block: @escaping () -> Void)
50-
func barrierAsync(_ block: @escaping () -> Void)
49+
func async(_ block: @Sendable @escaping () -> Void)
50+
func isolatedAsync(_ block: @escaping () -> Void)
51+
func barrierAsync(_ block: @Sendable @escaping () -> Void)
5152
}
5253

5354
@_spi(Essentials)
@@ -65,12 +66,17 @@ public final class NetworkContext: NetworkContextProtocol, @unchecked Sendable {
6566
}
6667

6768
public protocol Scheduler: AnyObject {
69+
/// Runs an immediate task, scheduled from the scheduler's context (so the completion is non-sendable).
70+
/// No assumptions are made about how the task is run.
71+
func runImmediateIsolated(_ task: @escaping () -> Void)
72+
6873
/// Runs an immediate task. No assumptions are made about how the task is run.
69-
func runImmediate(_ task: @escaping (() -> Void))
74+
func runImmediate(_ task: @Sendable @escaping () -> Void)
75+
7076
/// Schedules a task to run after a delay, using a reference.
7177
///
7278
/// The `milliseconds` parameter specifies the delay before the task runs.
73-
func schedule(_ task: @escaping (() -> Void), milliseconds: Int64, reference: TimerReference)
79+
func schedule(_ task: @escaping () -> Void, milliseconds: Int64, reference: TimerReference)
7480
/// Unschedules a task with a reference.
7581
func unschedule(reference: TimerReference)
7682
/// A Boolean value that indicates whether the current code is running in the scheduler.
@@ -301,7 +307,7 @@ extension NetworkContext {
301307
entries.removeFirst(where: { $0.reference == reference })
302308
}
303309

304-
func insert(targetTime: DispatchTime, reference: TimerReference, task: @escaping (() -> Void)) {
310+
func insert(targetTime: DispatchTime, reference: TimerReference, task: @escaping () -> Void) {
305311
// First, remove an existing entry for this reference
306312
remove(by: reference)
307313
// Check to see if the targetTime is before the first entry before it's added
@@ -348,14 +354,19 @@ extension NetworkContext {
348354
init(globals: Globals) {
349355
self.globals = globals
350356
}
357+
/// Runs an immediate task, scheduled from the scheduler's context (so the completion is non-sendable).
358+
/// No assumptions are made about how the task is run.
359+
func runImmediateIsolated(_ task: @escaping () -> Void) {
360+
globals.queue.async(execute: DispatchWorkItem(block: task))
361+
}
351362
/// Runs an immediate task. No assumptions are made about how the task is run.
352-
func runImmediate(_ task: @escaping (() -> Void)) {
363+
func runImmediate(_ task: @Sendable @escaping () -> Void) {
353364
globals.queue.async(execute: DispatchWorkItem(block: task))
354365
}
355366
/// Schedules a task to run after a delay, using a reference.
356367
///
357368
/// The `milliseconds` parameter specifies the delay before the task runs.
358-
func schedule(_ task: @escaping (() -> Void), milliseconds: Int64, reference: TimerReference) {
369+
func schedule(_ task: @escaping () -> Void, milliseconds: Int64, reference: TimerReference) {
359370
let targetTime = DispatchTime.now() + DispatchTimeInterval.milliseconds(Int(milliseconds))
360371
globals.timerList.insert(targetTime: targetTime, reference: reference, task: task)
361372
}
@@ -380,11 +391,18 @@ extension NetworkContext {
380391
globals.queue
381392
}
382393

383-
public func async(_ block: @escaping () -> Void) {
394+
public func async(_ block: @Sendable @escaping () -> Void) {
384395
scheduler.runImmediate(block)
385396
}
386397

387-
public func barrierAsync(_ block: @escaping () -> Void) {
398+
public func isolatedAsync(_ block: @escaping () -> Void) {
399+
#if DEBUG
400+
assert()
401+
#endif
402+
scheduler.runImmediateIsolated(block)
403+
}
404+
405+
public func barrierAsync(_ block: @Sendable @escaping () -> Void) {
388406
scheduler.runImmediate(block)
389407
}
390408

Sources/SwiftNetwork/EndpointFlow/EndpointFlow.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ internal import Synchronization
3232
#endif
3333

3434
@available(Network 0.1.0, *)
35-
final class EndpointFlow: CustomDebugStringConvertible {
35+
final class EndpointFlow: CustomDebugStringConvertible, @unchecked Sendable {
3636

3737
/// State used to emit logs on the data path.
3838
public var log = NetworkLoggerState()
@@ -269,7 +269,7 @@ final class EndpointFlow: CustomDebugStringConvertible {
269269
}
270270
}
271271

272-
func async(_ block: @escaping () -> Void) {
272+
func async(_ block: @Sendable @escaping () -> Void) {
273273
self.parameters.context.async(block)
274274
}
275275

Sources/SwiftNetwork/EndpointFlow/ReadRequest.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@
1313
//===----------------------------------------------------------------------===//
1414

1515
@available(Network 0.1.0, *)
16-
struct ReadRequest {
16+
struct ReadRequest: Sendable {
1717
let minimumBytes: Int
1818
let maximumBytes: Int
1919
let maximumFrames: Int
20-
let completion: ([UInt8]?, Bool, Bool, NetworkError?) -> Void
20+
let completion: @Sendable ([UInt8]?, Bool, Bool, NetworkError?) -> Void
2121

2222
func complete(content: [UInt8]?, isComplete: Bool, isFinal: Bool, error: NetworkError? = nil) {
2323
completion(content, isComplete, isFinal, error)

Sources/SwiftNetwork/Protocols/ProtocolEventManager.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -520,7 +520,7 @@ extension NetworkContext {
520520

521521
fileprivate func async(index: NetworkStateIndex, _ block: @escaping () -> Void) {
522522
self.softAssert()
523-
self.async {
523+
self.isolatedAsync {
524524
self.protocolEventStates[index].startAsyncCall()
525525
defer {
526526
self.protocolEventStates[index].finishAsyncCall()

Sources/SwiftNetwork/QUIC/QUICConnection.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5253,7 +5253,7 @@ extension QUICConnection {
52535253
}
52545254
asyncSendRunning = true
52555255
log.datapath("async: scheduling restart after packet burst")
5256-
self.context.async {
5256+
self.async {
52575257
self.resumeSendingAfterBurstLimit()
52585258
}
52595259
}

Sources/SwiftNetworkBenchmarks/BenchmarkUtility.swift

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ public struct QUICLoopbackState {
7272

7373
@_spi(ProtocolProvider)
7474
@available(Network 0.1.0, *)
75-
public struct QUICClientEndpointResult {
75+
public struct QUICClientEndpointResult: @unchecked Sendable {
7676
public var instance: QUICConnection
7777
public var parameters: Parameters
7878
public var upperHandler: StreamUpperHarness
@@ -82,7 +82,7 @@ public struct QUICClientEndpointResult {
8282

8383
@_spi(ProtocolProvider)
8484
@available(Network 0.1.0, *)
85-
public struct QUICServerEndpointResult {
85+
public struct QUICServerEndpointResult: @unchecked Sendable {
8686
public var instance: QUICConnection
8787
public var parameters: Parameters
8888
public var upperHandler: NewStreamFlowHarness
@@ -96,12 +96,12 @@ public enum BenchmarkError: Error {
9696

9797
@_spi(ProtocolProvider)
9898
@available(Network 0.1.0, *)
99-
public final class QUICBenchmarkUtility {
99+
public final class QUICBenchmarkUtility: Sendable {
100100

101101
// 127.0.0.1 (Just point both at loopback)
102102
public static let localIPv4Address: [UInt8] = [0x7f, 0x00, 0x00, 0x01]
103103
public static let remoteIPv4Address: [UInt8] = [0x7f, 0x00, 0x00, 0x01]
104-
var serverSigningKey = P256.Signing.PrivateKey()
104+
let serverSigningKey = P256.Signing.PrivateKey()
105105

106106
public func createQUICTestOptions(
107107
server: Bool = false,
@@ -265,20 +265,20 @@ public final class QUICBenchmarkUtility {
265265
#if !(os(Linux) || NETWORK_EMBEDDED)
266266
@available(macOS 11, iOS 14, tvOS 14, watchOS 7, *)
267267
#endif
268-
public struct LoggingHandle: CustomStringConvertible {
268+
public struct LoggingHandle: CustomStringConvertible, Sendable {
269269
#if os(Linux) || NETWORK_EMBEDDED
270270
let logger = Logger(label: "com.apple.network.benchmarks")
271271
#else
272272
#if canImport(os)
273273
let logger = Logger(subsystem: "com.apple.network.benchmarks", category: "perf")
274274
#endif
275275
#endif
276-
public enum LoggingType: Int {
276+
public enum LoggingType: Int, Sendable {
277277
case none = 0
278278
case print = 1
279279
case log = 2
280280
}
281-
public var loggingType: LoggingType = .none
281+
public let loggingType: LoggingType
282282

283283
public init(loggingType: LoggingType) {
284284
self.loggingType = loggingType
@@ -322,7 +322,7 @@ public struct LoggingHandle: CustomStringConvertible {
322322

323323
@_spi(ProtocolProvider)
324324
@available(Network 0.1.0, *)
325-
public final class DataBenchmarkUtility {
325+
public final class DataBenchmarkUtility: Sendable {
326326
@discardableResult
327327
public func loopOutputHandlerPackets(
328328
sender: DatagramLowerHarness,

Sources/Tools/IPUDPTransfer/main.swift

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ internal import os
2727
#endif
2828

2929
@available(Network 0.1.0, *)
30-
final class IPUDPTransfer {
30+
final class IPUDPTransfer: @unchecked Sendable {
3131

3232
// 169.254.156.146
3333
let localIPv4Address: [UInt8] = [0xa9, 0xfe, 0x9c, 0x92]
@@ -37,14 +37,14 @@ final class IPUDPTransfer {
3737
let dataBenchmarkUtility = DataBenchmarkUtility()
3838
let NSEC_PER_MSEC = UInt64(Duration.milliseconds(1) / Duration.nanoseconds(1))
3939

40+
var iterationIndex = 0
41+
4042
func run(iterations: Int, packets: Int, loggingHandle: LoggingHandle, group: DispatchGroup, sendSize: Int) -> Double
4143
{
42-
let ipv4Client = Endpoint(address: IPv4Address(localIPv4Address)!, port: 0)
43-
let ipv4Server = Endpoint(address: IPv4Address(remoteIPv4Address)!, port: 0)
4444
// Create a random payload to send back and forth
45-
var payload = [UInt8](repeating: 0, count: sendSize)
46-
payload = (0..<sendSize).map { _ in UInt8.random(in: 0...255) }
47-
var iterationIndex = 0
45+
var mutablePayload = [UInt8](repeating: 0, count: sendSize)
46+
mutablePayload = (0..<sendSize).map { _ in UInt8.random(in: 0...255) }
47+
let payload = mutablePayload
4848
print(
4949
"Running IP/UDP transfer, transferring \(iterations) iteration\(iterations > 1 ? "s" : "") of \(packets) packet\(packets > 1 ? "s" : "")"
5050
)
@@ -53,11 +53,15 @@ final class IPUDPTransfer {
5353
// NOTE: Today this benchmark relies on DispatchGroups due to Sendability issues on some of the project types.
5454
// In the future we should try to adopt Swift Concurrency.
5555
group.enter()
56-
var clientParameters = Parameters()
5756
let context = NetworkContext(identifier: "IPUDPTransfer")
58-
clientParameters.context = context
5957
context.activate()
6058
context.async {
59+
let ipv4Client = Endpoint(address: IPv4Address(self.localIPv4Address)!, port: 0)
60+
let ipv4Server = Endpoint(address: IPv4Address(self.remoteIPv4Address)!, port: 0)
61+
62+
var clientParameters = Parameters()
63+
clientParameters.context = context
64+
6165
for _ in 0..<iterations {
6266
// Client
6367
let path = PathProperties(parameters: clientParameters)
@@ -185,7 +189,7 @@ final class IPUDPTransfer {
185189
serverInput.stop()
186190
serverInput.teardown()
187191

188-
iterationIndex += 1
192+
self.iterationIndex += 1
189193
}
190194
group.leave()
191195
}

Sources/Tools/QUICHandshake/main.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ internal import os
2929
#if IMPORT_SWIFTTLS && canImport(SwiftTLS)
3030

3131
@available(Network 0.1.0, *)
32-
final class QUICHandshake {
32+
final class QUICHandshake: @unchecked Sendable {
3333

3434
let quicBenchmarkUtility = QUICBenchmarkUtility()
3535
let dataBenchmarkUtility = DataBenchmarkUtility()
@@ -137,7 +137,7 @@ final class QUICHandshake {
137137
}
138138
}
139139
// Wait until both instances are connected to signal handshake complete
140-
func pollForConnectedInstances() {
140+
@Sendable func pollForConnectedInstances() {
141141
// Wait for the client connection state to go into connected
142142
if client.instance.state == .connected {
143143
loggingHandle.log("Client connection handshake finished")

0 commit comments

Comments
 (0)