From 997a5ff35a290b0da773b711f434792caf5613c6 Mon Sep 17 00:00:00 2001 From: Kabir Oberai Date: Sat, 28 Jun 2025 15:18:38 -0400 Subject: [PATCH 1/2] Remember target in NodeActor.shared --- Sources/NodeAPI/NodeActor.swift | 42 +++++++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/Sources/NodeAPI/NodeActor.swift b/Sources/NodeAPI/NodeActor.swift index 68e04b2..d8f4ef7 100644 --- a/Sources/NodeAPI/NodeActor.swift +++ b/Sources/NodeAPI/NodeActor.swift @@ -11,9 +11,10 @@ extension NodeContext { @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) private final class NodeExecutor: SerialExecutor { + private let defaultTarget: NodeAsyncQueue.Handle? private let schedulerQueue = DispatchQueue(label: "NodeExecutorScheduler") - fileprivate init() { + fileprivate init(defaultTarget: NodeAsyncQueue.Handle?) { // Swift often thinks that we're on the wrong executor, so we end up // with a lot of false alarms. This is what `checkIsolation` ostensibly // mitigates, but on Darwin that method doesn't seem to be called in many @@ -22,6 +23,7 @@ private final class NodeExecutor: SerialExecutor { #if canImport(Darwin) setenv("SWIFT_UNEXPECTED_EXECUTOR_LOG_LEVEL", "0", 1) #endif + self.defaultTarget = defaultTarget } func enqueue(_ job: UnownedJob) { @@ -37,6 +39,8 @@ private final class NodeExecutor: SerialExecutor { let q: NodeAsyncQueue if let target = NodeActor.target { q = target.queue + } else if let defaultTarget { + q = defaultTarget.queue } else if let globalQueue = NodeAsyncQueue.globalDefaultQueue { q = globalQueue } else { @@ -60,7 +64,14 @@ private final class NodeExecutor: SerialExecutor { } func checkIsolated() { - // TODO: crash if we're not on a Node thread + if !isIsolatingCurrentContext() { + nodeFatalError("NodeExecutor.checkIsolated: not in an isolated context") + } + } + + func isIsolatingCurrentContext() -> Bool { + // TODO: return false if we're not on a Node thread + return true } } @@ -72,12 +83,33 @@ private final class NodeExecutor: SerialExecutor { // Task.detatched closure will crash. @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) @globalActor public actor NodeActor { - private init() {} - public static let shared = NodeActor() + private init(target: NodeAsyncQueue.Handle?) { + executor = NodeExecutor(defaultTarget: target) + } + + private static nonisolated(unsafe) var cache: [ObjectIdentifier: Weak] = [:] + private static let lock = Lock() + private static let defaultShared = NodeActor(target: nil) + + public static var shared: NodeActor { + // hack: NodeActor.shared seems to be evaluated every time Swift dispatches + // to the actor. Use this opportunity to remember the target queue. + guard let target else { return defaultShared } + let key = ObjectIdentifier(target) + return lock.withLock { + if let item = cache[key]?.value { + return item + } else { + let newActor = NodeActor(target: target) + cache[key] = Weak(newActor) + return newActor + } + } + } @TaskLocal static var target: NodeAsyncQueue.Handle? - private nonisolated let executor = NodeExecutor() + private nonisolated let executor: NodeExecutor public nonisolated var unownedExecutor: UnownedSerialExecutor { executor.asUnownedSerialExecutor() } From 2c1db896c643f2a9e6bebd9a45d2703ae5f69b71 Mon Sep 17 00:00:00 2001 From: Kabir Oberai Date: Sat, 28 Jun 2025 15:22:12 -0400 Subject: [PATCH 2/2] uuid --- Sources/NodeAPI/NodeActor.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/NodeAPI/NodeActor.swift b/Sources/NodeAPI/NodeActor.swift index d8f4ef7..33a52f1 100644 --- a/Sources/NodeAPI/NodeActor.swift +++ b/Sources/NodeAPI/NodeActor.swift @@ -87,7 +87,7 @@ private final class NodeExecutor: SerialExecutor { executor = NodeExecutor(defaultTarget: target) } - private static nonisolated(unsafe) var cache: [ObjectIdentifier: Weak] = [:] + private static nonisolated(unsafe) var cache: [UUID: Weak] = [:] private static let lock = Lock() private static let defaultShared = NodeActor(target: nil) @@ -95,7 +95,7 @@ private final class NodeExecutor: SerialExecutor { // hack: NodeActor.shared seems to be evaluated every time Swift dispatches // to the actor. Use this opportunity to remember the target queue. guard let target else { return defaultShared } - let key = ObjectIdentifier(target) + let key = target.queue.instanceID return lock.withLock { if let item = cache[key]?.value { return item