Skip to content

[SR-14875] Resuming continuations from actor contexts can hang #57222

Closed
@josephlord

Description

@josephlord
Previous ID SR-14875
Radar rdar://problem/80238311
Original Reporter @josephlord
Type Bug
Status Resolved
Resolution Duplicate

Attachment: Download

Environment

Xcode 13 beta 2. Snapshot builds 5.5 toolchain 2021-07-03, 2021-07-01, 2021-06-29, M1 Mac mini, Monterey beta2, Xcode beta 3, Monterey beta 3

Additional Detail from JIRA
Votes 1
Component/s
Labels Bug
Assignee None
Priority Medium

md5: 0f377d3ebb1ee24957d4228c49aeb885

duplicates:

  • SR-14802 Concurrency: Unstructured Task Triggering Continuation Can Hang

relates to:

  • SR-14841 Concurrency: Resuming a stored continuation from an actor does not work

Issue Description:

Note also separate hang report here: SR-14802

I've been playing with and testing creating my own AsyncSequences. The first implementation below works well, the second is highly unreliable. The only difference is that the first one returns the continuation from the actor which then gets resumed and the second one it is resumed from the actor context.

I'm not aware of anything in the specification about not firing continuations from actor context. If it really shouldn't be done there should be compile time errors if there is an attempt to resume a continuation from an actor context.

@available(macOS 12.0, iOS 15.0, *)
public struct AsyncTimerActorSequence : AsyncSequence {
    
    public typealias AsyncIterator = Iterator
    public typealias Element = Void
    
    let interval: TimeInterval
    
    public init(interval: TimeInterval) {
        self.interval = interval
    }
    
    public func makeAsyncIterator() -> Iterator {
        Iterator(interval: interval)
    }
    
    public class Iterator : AsyncIteratorProtocol {
        
        private actor InnerActor {
            private var continuations: Deque<CheckedContinuation<(), Never>> = []
            
            fileprivate func getContinuation() -> CheckedContinuation<(), Never>? {
                return continuations.popFirst()
            }
            
            fileprivate func addContinuation(_ continuation: CheckedContinuation<(), Never>) {
                continuations.append(continuation)
            }
        }
        private let safeContinuations = InnerActor()
        private var timer: Timer?
        
        fileprivate init(interval: TimeInterval) {
            let safeConts = safeContinuations
            let t = Timer(fire: .now, interval: interval, repeats: true) { _ in
                Task {
                    let continuation = await safeConts.getContinuation()
                    continuation?.resume()
                }
            }
            self.timer = t
            RunLoop.main.add(t, forMode: .default)
        }
        
        public func next() async throws -> ()? {
            await withCheckedContinuation { continuation in
                Task {
                    await safeContinuations.addContinuation(continuation)
                }
            }
            return ()
        }
        
        deinit {
            timer?.invalidate()
        }
    }
}


@available(macOS 12.0, iOS 15.0, *)
public struct AsyncTimerActorSequenceUnreliable : AsyncSequence {
    
    public typealias AsyncIterator = Iterator
    public typealias Element = Void
    
    let interval: TimeInterval
    
    public init(interval: TimeInterval) {
        self.interval = interval
    }
    
    public func makeAsyncIterator() -> Iterator {
        Iterator(interval: interval)
    }
    
    public class Iterator : AsyncIteratorProtocol {
        
        private actor InnerActor {
            private var continuations: Deque<CheckedContinuation<(), Never>> = []
            
            fileprivate func fireContinuation() {
                continuations.popFirst()?.resume()
            }
            
            fileprivate func addContinuation(_ continuation: CheckedContinuation<(), Never>) {
                continuations.append(continuation)
            }
        }
        private let safeContinuations = InnerActor()
        private var timer: Timer?
        
        fileprivate init(interval: TimeInterval) {
            let safeConts = safeContinuations
            let t = Timer(fire: .now, interval: interval, repeats: true) { _ in
                Task {
                    await safeConts.fireContinuation()
                }
            }
            self.timer = t
            RunLoop.main.add(t, forMode: .default)
        }
        
        public func next() async throws -> ()? {
            await withCheckedContinuation { continuation in
                Task {
                    await safeContinuations.addContinuation(continuation)
                }
            }
            return ()
        }
        
        deinit {
            timer?.invalidate()
        }
    }
}

To reproduce open the package in Xcode and run the AsyncTimerSequenceUnreliableActorTests repeatedly (100 times). There should be some errors. Whereas the similar AsyncTimerSequenceActorTests should be the same tests but on the working version of the sequence.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugA deviation from expected or documented behavior. Also: expected but undesirable behavior.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions