77
88import Foundation
99
10- public typealias AsyncSchedulerHandler = ( ) async -> Void
10+ public typealias AsyncSchedulerHandler = @ Sendable ( ) async -> Void
1111
1212public protocol AsyncScheduler : Actor {
1313 var now : TimeInterval { get }
@@ -23,6 +23,7 @@ public actor MainAsyncScheduler: AsyncScheduler {
2323 lazy var idCounter : UInt = 0
2424 lazy var completedElementIds = Set < UInt > ( )
2525 lazy var cancelledElementIds = Set < UInt > ( )
26+ private var isCompletingElement = false
2627
2728 public var now : TimeInterval {
2829 Date ( ) . timeIntervalSince1970
@@ -34,8 +35,6 @@ public actor MainAsyncScheduler: AsyncScheduler {
3435 /// - parameter handler: async closure to be executed when 'after' time elapses
3536 ///
3637 /// - Returns: reference to a Task which supports cancellation
37- ///
38- /// - Complexity: O(log n) where n is the number of elements currently scheduled
3938 @discardableResult
4039 public func schedule(
4140 after: TimeInterval ,
@@ -52,36 +51,69 @@ public actor MainAsyncScheduler: AsyncScheduler {
5251
5352 increaseCounterId ( )
5453
55- return Task {
56- try ? await Task . sleep ( nanoseconds: UInt64 ( after * 1_000_000_000 ) )
57- await complete ( currentId: currentId, cancelled: Task . isCancelled)
58- }
54+ return createScheduledExecutionTask ( currentId: currentId, after: after)
5955 }
6056
6157 /// Based on the timeIntervalSince1970 from Date, the smallest intervals will need
6258 /// to complete before other elements' handlers can be executed. Due to the nature
6359 /// of Tasks, there could be some situations where some tasks scheduled to finish
6460 /// before others finish first. This could potentially have unwanted behaviors on
65- /// objects scheduling events. To address this matter, a minimum priority queue
66- /// is critical to always keep the first element that should be completed in the
67- /// top of the queue. Once its task completes, a Set will keep track of all
61+ /// objects scheduling events.
62+ ///
63+ /// - parameter currentId: integer variable denoting handler/task id
64+ ///
65+ /// - Returns: reference to a Task which supports cancellation
66+ private func createScheduledExecutionTask(
67+ currentId: UInt ,
68+ after: TimeInterval
69+ ) -> Task < Void , Never > {
70+ return Task {
71+ try ? await Task . sleep ( for: . seconds( after) )
72+
73+ completedElementIds. insert ( currentId)
74+ if Task . isCancelled {
75+ cancelledElementIds. insert ( currentId)
76+ }
77+
78+ // Make sure that only one complete method is running at all times.
79+ // The reason why this is important is because there is an inner await in a while loop which
80+ // releases the execution of this actor and it cause race conditions if another scheduled
81+ // task completes within the time this method is executing causing a weird state where two
82+ // while loops might have erroneous values and destroy the serial execution intended from
83+ // this method.
84+ guard !isCompletingElement else { return }
85+
86+ // Block any other Tasks from calling complete from this point.
87+ isCompletingElement = true
88+
89+ await complete ( currentId: currentId)
90+
91+ // Allow any future callers of this method to call complete.
92+ isCompletingElement = false
93+ }
94+ }
95+
96+ /// This method runs the completion handler for a given scheduled item matching the `currentId`.
97+ ///
98+ /// A minimum priority queue is critical to always keep the first element that should be
99+ /// completed in the top of the queue. Once its task completes, a Set will keep track of all
68100 /// completed ID tasks that are yet to be executed. If the current top element of
69101 /// the queue has already completed, its closure will execute. This will repeat
70102 /// until all completed top elements of the queue are executed.
71103 /// The obvious drawback of this handling, is that a small delay could be
72104 /// introduced to some scheduled async-closures. Ideally, this would be in the
73105 /// order of micro/nanoseconds depending of the system load.
74106 ///
75- /// - parameter currentId: integer variable denoting handler/task id
76- /// - parameter cancelled: boolean flag required to determine whether or not to execute the handler
107+ /// This method will execute an inner loop resolving all available completed elements.
108+ ///
109+ /// Note that his actor switches execution and during this `paused` time another scheduled task
110+ /// can complete and call `complete`. It is important to have only one `complete` running at
111+ /// all times (specifically, due the inner while loop).
112+ ///
113+ /// This method should only be called from within `createScheduledExecutionTask`.
77114 ///
78115 /// - Complexity: O(log n) where n is the number of elements currently scheduled
79- private func complete( currentId: UInt , cancelled: Bool ) async {
80- completedElementIds. insert ( currentId)
81- if cancelled {
82- cancelledElementIds. insert ( currentId)
83- }
84-
116+ private func complete( currentId: UInt ) async {
85117 while let minElement = queue. peek, completedElementIds. contains ( minElement. id) {
86118 queue. removeFirst ( )
87119 completedElementIds. remove ( minElement. id)
0 commit comments