Skip to content

Commit c1ce0e2

Browse files
authored
refactor: fix actor isolation bug with protocol conformance on older swift versions (#11)
* refactor: fix actor isolation bug with protocol conformance on older swift versions * wip: improve semaphore logs
1 parent bdd688b commit c1ce0e2

File tree

6 files changed

+555
-430
lines changed

6 files changed

+555
-430
lines changed

AsyncObjects.xcodeproj/project.pbxproj

Lines changed: 429 additions & 425 deletions
Large diffs are not rendered by default.

Sources/AsyncObjects/AsyncCountdownEvent.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ import OrderedCollections
3737
///
3838
/// Use the ``limit`` parameter to indicate concurrent low priority usage, i.e. if limit set to zero,
3939
/// only one low priority usage allowed at one time.
40-
public actor AsyncCountdownEvent: AsyncObject, ContinuableCollection,
40+
public actor AsyncCountdownEvent: AsyncObject, ContinuableCollectionActor,
4141
LoggableActor
4242
{
4343
/// The suspended tasks continuation type.

Sources/AsyncObjects/AsyncEvent.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ import Foundation
2525
/// // signal event after completing some task
2626
/// event.signal()
2727
/// ```
28-
public actor AsyncEvent: AsyncObject, ContinuableCollection, LoggableActor {
28+
public actor AsyncEvent: AsyncObject, ContinuableCollectionActor, LoggableActor
29+
{
2930
/// The suspended tasks continuation type.
3031
@usableFromInline
3132
internal typealias Continuation = TrackedContinuation<

Sources/AsyncObjects/AsyncSemaphore.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ import OrderedCollections
2626
/// // release after executing critical async tasks
2727
/// defer { semaphore.signal() }
2828
/// ```
29-
public actor AsyncSemaphore: AsyncObject, ContinuableCollection, LoggableActor {
29+
public actor AsyncSemaphore: AsyncObject, ContinuableCollectionActor,
30+
LoggableActor
31+
{
3032
/// The suspended tasks continuation type.
3133
@usableFromInline
3234
internal typealias Continuation = TrackedContinuation<
@@ -113,8 +115,8 @@ public actor AsyncSemaphore: AsyncObject, ContinuableCollection, LoggableActor {
113115
withKey key: UUID,
114116
file: String, function: String, line: UInt
115117
) {
116-
log("Removing", id: key, file: file, function: function, line: line)
117118
incrementCount()
119+
log("Removing", id: key, file: file, function: function, line: line)
118120
continuations.removeValue(forKey: key)
119121
guard !continuation.resumed else {
120122
log(

Sources/AsyncObjects/Continuation/ContinuableCollection.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import Foundation
66

77
/// A type that manages a collection of continuations with an associated key.
88
///
9-
/// While removing continuation, the continuation should be cancelled
9+
/// While removing continuation, the continuation should be cancelled.
1010
@rethrows
1111
internal protocol ContinuableCollection {
1212
/// The continuation item type in collection.
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
#if swift(>=5.7)
2+
import Foundation
3+
4+
/// An actor type that manages a collection of continuations with an associated key.
5+
///
6+
/// On `Swift 5.7` and above [actor isolation bug with protocol conformance](https://forums.swift.org/t/actor-isolation-is-broken-by-protocol-conformance/57040)
7+
/// is fixed, and hence original protocol can be used without any issue.
8+
typealias ContinuableCollectionActor = ContinuableCollection
9+
#else
10+
@preconcurrency import Foundation
11+
12+
/// An actor type that manages a collection of continuations with an associated key.
13+
///
14+
/// This is to avoid [actor isolation bug with protocol conformance on older `Swift` versions](https://forums.swift.org/t/actor-isolation-is-broken-by-protocol-conformance/57040).
15+
///
16+
/// While removing continuation, the continuation should be cancelled.
17+
@rethrows
18+
internal protocol ContinuableCollectionActor: Actor {
19+
/// The continuation item type in collection.
20+
associatedtype Continuation: Continuable
21+
/// The key type that is associated with each continuation item.
22+
associatedtype Key: Hashable
23+
24+
/// Add continuation with the provided key to collection for tracking.
25+
///
26+
/// - Parameters:
27+
/// - continuation: The continuation value to add.
28+
/// - key: The key to associate continuation with.
29+
/// - file: The file add request originates from.
30+
/// - function: The function add request originates from.
31+
/// - line: The line add request originates from.
32+
/// - preinit: The pre-initialization handler to run
33+
/// in the beginning of this method.
34+
///
35+
/// - Important: The pre-initialization handler must run
36+
/// before any logic in this method.
37+
func addContinuation(
38+
_ continuation: Continuation, withKey key: Key,
39+
file: String, function: String, line: UInt,
40+
preinit: @Sendable () -> Void
41+
)
42+
/// Remove continuation with the associated key from collection out of tracking.
43+
///
44+
/// - Parameters:
45+
/// - continuation: The continuation value to remove and cancel.
46+
/// - key: The key for continuation to remove.
47+
/// - file: The file remove request originates from.
48+
/// - function: The function remove request originates from.
49+
/// - line: The line remove request originates from.
50+
func removeContinuation(
51+
_ continuation: Continuation, withKey key: Key,
52+
file: String, function: String, line: UInt
53+
)
54+
/// Suspends the current task, then calls the given closure with a continuation for the current task.
55+
///
56+
/// - Parameters:
57+
/// - key: The key associated to task, that requested suspension.
58+
/// - file: The file wait request originates from.
59+
/// - function: The function wait request originates from.
60+
/// - line: The line wait request originates from.
61+
///
62+
/// - Returns: The value continuation is resumed with.
63+
/// - Throws: If `resume(throwing:)` is called on the continuation, this function throws that error.
64+
func withPromisedContinuation(
65+
withKey key: Key,
66+
file: String, function: String, line: UInt
67+
) async rethrows -> Continuation.Success
68+
}
69+
70+
extension ContinuableCollectionActor
71+
where
72+
Self: AnyObject & Sendable, Continuation: TrackableContinuable & Sendable,
73+
Continuation.Value: Sendable & ThrowingContinuable, Key: Sendable,
74+
Key == Continuation.ID
75+
{
76+
/// Suspends the current task, then calls the given closure with a throwing continuation for the current task.
77+
/// Continuation can be cancelled with error if current task is cancelled, by invoking `removeContinuation`.
78+
///
79+
/// Spins up a new continuation and requests to track it with key by invoking `addContinuation`.
80+
/// This operation cooperatively checks for cancellation and reacting to it by invoking `removeContinuation`.
81+
/// Continuation can be resumed with error and some cleanup code can be run here.
82+
///
83+
/// - Parameters:
84+
/// - key: The key associated to task, that requested suspension.
85+
/// - file: The file wait request originates from (there's usually no need to pass it
86+
/// explicitly as it defaults to `#fileID`).
87+
/// - function: The function wait request originates from (there's usually no need to
88+
/// pass it explicitly as it defaults to `#function`).
89+
/// - line: The line wait request originates from (there's usually no need to pass it
90+
/// explicitly as it defaults to `#line`).
91+
///
92+
/// - Returns: The value continuation is resumed with.
93+
/// - Throws: If `resume(throwing:)` is called on the continuation, this function throws that error.
94+
@inlinable
95+
nonisolated func withPromisedContinuation(
96+
withKey key: Key,
97+
file: String, function: String, line: UInt
98+
) async rethrows -> Continuation.Success {
99+
return try await Continuation.withCancellation(id: key) {
100+
continuation in
101+
Task { [weak self] in
102+
await self?.removeContinuation(
103+
continuation, withKey: key,
104+
file: file, function: function, line: line
105+
)
106+
}
107+
} operation: { continuation, preinit in
108+
Task { [weak self] in
109+
await self?.addContinuation(
110+
continuation, withKey: key,
111+
file: file, function: function, line: line,
112+
preinit: preinit
113+
)
114+
}
115+
}
116+
}
117+
}
118+
#endif

0 commit comments

Comments
 (0)