WatchConnectivity Swift 6 - Incorrect actor executor assumption

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.

WatchConnectivity Swift 6 - Incorrect actor executor assumption
 
 
Q