`assumeIsolated(_:)` fails with DispatchSerialQueue as custom Executor in Actor

51 views Asked by At

I want to implement an actor which uses "legacy" components which are classes using a dispatch queue for calling callbacks.

These "legacy" components are actually classes from Apples NWNetwork framework. These classes require a dispatch queue and callback functions to be specified. The callback functions will be called by the framework on the specified dispatch queue.

What I want to avoid is, to implement the callbacks like:

    @Sendable
    nonisolated
    private func callback(to newState: NWListener.State) {
        Task {
            await self.send(.listenerStateChange(newState))
        }
    }

i.e. dispatching into a Task which runs on the isolated context.

Instead, I want to run the whole actor on a custom Executor - a SerialDispatchQueue - which is also the queue for the "legacy" components.

Then, my actor provides the callback function as a member function like:

    @Sendable
    nonisolated
    private func callback(to newState: NWListener.State) {
        dispatchPrecondition(condition: .onQueue(self.queue))

        self.assumeIsolated { thisActor in
            thisActor.send(.listenerStateChange(newState))
        }
    }

The callback function must be @Sendable and nonisolated. Due to the design, it's guaranteed that the callback function is called on the given dispatch queue self.queue.

Now, when executing the actor, I get the following runtime error when executing self.assumeIsolated(:_):

Fatal error: Incorrect actor executor assumption; Expected same executor as MyLib.TestActor.

I wrote a minimal test to demonstrate the issue:

public final actor TestActor {
    let queue = DispatchSerialQueue(label: "test_actor_queue")
 
    nonisolated
    public var unownedExecutor: UnownedSerialExecutor {
        queue.asUnownedSerialExecutor()
    }
    
    func isolatedFunc() {
        self.preconditionIsolated()
    }
    
    @Sendable
    nonisolated
    func nonIsolatedFunc() {
        self.queue.async {
            self.callback()
        }
    }
    
    @Sendable
    nonisolated
    func callback() {
        dispatchPrecondition(condition: .onQueue(queue))

        self.assumeIsolated { this in    // <== Runtime error
            this.isolatedFunc()
        }
    }

}

Test

final class HTTPServerTests: XCTestCase {
    
    func testExample() async throws {
        let actor = TestActor()
        
        await actor.isolatedFunc()
        actor.nonIsolatedFunc()
        
    }
}    

The statement await actor.isolatedFunc() runs OK as expected.

actor.nonIsolatedFunc() simply simulates the "legacy" framework to call the callback on the specified dispatch queue. It fails during runtime with the given error.

Stack trace

The stack trace at the time of the runtime error also clearly shows, that we are running on the specified dispatch queue - as expected:

Thread 4 Queue : test_actor_queue (serial)
#0  0x000000019326df68 in _swift_runtime_on_report ()
#1  0x000000019330614c in _swift_stdlib_reportFatalErrorInFile ()
#2  0x0000000192fcb2d8 in _assertionFailure(_:_:file:line:flags:) ()
#3  0x00000002099cb298 in Actor.assumeIsolated<τ_0_0>(_:file:line:) ()
#4  0x00000001053a2918 in TestActor.callback@Sendable () at /Users/agrosam/Developer/HTTPServer-NWNetwork-Actor/HTTPServer/Sources/HTTPServer/HTTPServer.swift:77
#5  0x00000001053a27a0 in closure #1 in TestActor.nonIsolatedFunc@Sendable () at /Users/agrosam/Developer/HTTPServer-NWNetwork-Actor/HTTPServer/Sources/HTTPServer/HTTPServer.swift:68
#6  0x00000001053a2978 in thunk for @escaping @callee_guaranteed @Sendable () -> () ()

My understanding is, that DispatchLib already implemented everything to have a DispatchSerialQueue as a well behaving citizen of a SerialExecutor in the actor's realm.

So, I would expect, the runtime assertion would succeed.

See also:

https://github.com/apple/swift-evolution/blob/main/proposals/0424-custom-isolation-checking-for-serialexecutor.md

0

There are 0 answers