Help understanding a Swift 5.10 compiler warning when racing two async tasks to arrange a timeout mechanism using structured concurrency

I cribbed this rough pattern from WWDC23's Beyond Structured Concurrency talk, but I'm concerned that I'm mis-using it or that it's not safe to do what I'm after.

My goal is to call an async method that returns a result, and intentionally "race" the time it takes that method to return a value against a specific timeout, throwing an error if the timeout is exceeded, or returning the value if it returns before the timeout is triggered.

The code from my project:

let msg = try await withThrowingTaskGroup(of: SyncV1Msg.self) { group in
    group.addTask {
        // retrieve the next message
        try await self.receiveSingleMessage()
    }

    group.addTask {
        // Race against the receive call with a continuous timer
        try await Task.sleep(for: explicitTimeout)
        throw SyncV1Msg.Errors.Timeout()
    }

    guard let msg = try await group.next() else {
        throw CancellationError()
    }
    // cancel all ongoing tasks (the websocket receive request, in this case)
    group.cancelAll()
    return msg
}

This (and some similar patterns that don't involve returning a value) are all throwing the same compiler warning:

warning: passing argument of non-sendable type 'inout ThrowingTaskGroup<SyncV1Msg, any Error>' outside of global actor 'AutomergeRepo'-isolated context may introduce data races:

guard let msg = try await group.next() else {
                          ^

I thought I'd read elsewhere on the forums that this was a "known issue", but I was looking for the relevant reference today, and was unable to search back to find the relevant conversation, if it exists.

Is this a legit pattern with an overly cautious warning, or is this not a pattern that works. In practice, it seems to be operating as I'd expect - but I'm afraid the specifics of task groups and calling group.next() are eluding me.

Is there a better way to express this pattern?

(@mattie - have you run into this one in your recipes?)

2 Likes

I have run into that, and here is the answer:

The code in which I got this warning:

@MainActor
struct Wrapper {
    func test() async {
        await withTaskGroup(of: Void.self) { group in
            for _ in 0..<100 {
                group.addTask {
                    print("Hello, world!")
                }
            }
            await group.waitForAll() // warning: passing argument of non-sendable type 'inout TaskGroup<Void>' outside of main actor-isolated context may introduce data races
        }
    }
}

And the mentioned workaround for now is to make test nonisolated.

SE-0420 was partly to address this issue in some of the APIs.

5 Likes

Thank you !!