I am trying to migrate a WatchConnectivity App to Swift6 and I found an Issue with my replyHandler
callback for sendMessageData.
I am wrapping sendMessageData
in withCheckedThrowingContinuation,
so that I can await the response of the reply. I then update a Main Actor ObservableObject
that keeps track of the count of connections that have not replied yet, before returning the data using continuation.resume.
...
@preconcurrency import WatchConnectivity
actor ConnectivityManager: NSObject, WCSessionDelegate {
private var session: WCSession = .default
private let connectivityMetaInfoManager: ConnectivityMetaInfoManager
...
private func sendMessageData(_ data: Data) async throws -> Data? {
Logger.shared.debug("called on Thread \(Thread.current)")
await connectivityMetaInfoManager.increaseOpenSendConnectionsCount()
return try await withCheckedThrowingContinuation({
continuation in
self.session.sendMessageData(
data,
replyHandler: { data in
Task {
await self.connectivityMetaInfoManager
.decreaseOpenSendConnectionsCount()
}
continuation.resume(returning: data)
},
errorHandler: { (error) in
Task {
await self.connectivityMetaInfoManager
.decreaseOpenSendConnectionsCount()
}
continuation.resume(throwing: error)
}
)
})
}
Calling sendMessageData
somehow causing the app to crash and display the debug message: Incorrect actor executor assumption
.
The code runs on swift 5 with SWIFT_STRICT_CONCURRENCY = complete. However when I switch to swift 6 the code crashes.
I rebuilt a simple version of the App. Adding bit by bit until I was able to cause the crash. See Broken App
Awaiting sendMessageData
and wrapping it in a task and adding the @Sendable
attribute to continuation,
solve the crash.
See Fixed App
But I do not understand why yet.
Is this intended behaviour?
Should the compiler warn you about this?
Is it a WatchConnectivity
issue?
I initially posted on forums.swift.org, but was told to repost here.
I believe the crash (with the "Incorrect actor executor assumption" message) is intentional. When you use the Swift 6 mode to build your app, the compiler adds some runtime checks to detect problems that could otherwise cause a data race. If a check is not satisfied, it triggers a crash.
I am by no means an expert of Swift compiler, but here is my theory about what happens in your case:
a. ConnectivityManager
is actor-isolated, meaning that, in your code snippet, self.session.sendMessageData(...)
, the replyHandler
closure, and also the errorHandler
closure are supposed to run in the actor's executor.
b. WCSession.sendMessageData(...)
doesn't really run replyHandler
(or errorHandler
) in the calling queue. This is mentioned in the API reference: "If you specify a reply handler block, your handler block is executed asynchronously on a background thread."
For some reason, the compiler doesn't detect the problem at the compile time, but at runtime, replyHandler
(or errorHandler
) is running in a queue different from the actor's executor. That triggers a runtime check failure, and hence the crash.
To fix the issue, you can consider making the closures passed to WCSession.sendMessageData(...)
sendable, which tells the compiler that the closure can be safely run in any thread, and hence should eliminate the crash:
return try await withCheckedThrowingContinuation({
continuation in
self.session.sendMessageData(
data,
replyHandler: { @Sendable data in // Mark the replyHandler closure as sendable.
Task {
await self.connectivityMetaInfoManager
.decreaseOpenSendConnectionsCount()
}
continuation.resume(returning: data)
},
errorHandler: { @Sendable (error) in // Mark the errorHandler closure as sendable.
Task {
await self.connectivityMetaInfoManager
.decreaseOpenSendConnectionsCount()
}
continuation.resume(throwing: error)
}
)
})
You can give it a try and follow up here if this doesn't help.
Best,
——
Ziqiao Chen
Worldwide Developer Relations.