fix(macos): recheck scheduled task cancellation

This commit is contained in:
Vincent Koc
2026-07-30 10:54:20 +02:00
parent 6c8363f7b9
commit 6876ae752a
2 changed files with 53 additions and 13 deletions

View File

@@ -18,12 +18,13 @@ enum SimpleTaskSupport {
task: inout Task<Void, Never>?,
interval: TimeInterval,
sleep: @escaping @Sendable (UInt64) async throws -> Void = { try await Task.sleep(nanoseconds: $0) },
operation: @escaping @Sendable () async -> Void)
{
operation: @escaping @Sendable () async -> Void
) {
guard task == nil else { return }
task = Task.detached {
await operation()
while await self.waitForNextOperation(interval: interval, sleep: sleep) {
guard !Task.isCancelled else { break }
await operation()
}
}
@@ -33,18 +34,22 @@ enum SimpleTaskSupport {
task: inout Task<Void, Never>?,
delay: TimeInterval,
sleep: @escaping @Sendable (UInt64) async throws -> Void = { try await Task.sleep(nanoseconds: $0) },
operation: @escaping @MainActor @Sendable () async -> Void)
{
beforeOperationCheck: @escaping @Sendable () -> Void = {},
operation: @escaping @MainActor @Sendable () async -> Void
) {
task?.cancel()
task = Task {
guard await self.waitForNextOperation(interval: delay, sleep: sleep) else { return }
beforeOperationCheck()
guard !Task.isCancelled else { return }
await operation()
}
}
nonisolated static func waitForNextOperation(
interval: TimeInterval,
sleep: @escaping @Sendable (UInt64) async throws -> Void = { try await Task.sleep(nanoseconds: $0) }) async
sleep: @escaping @Sendable (UInt64) async throws -> Void = { try await Task.sleep(nanoseconds: $0) }
) async
-> Bool
{
guard !Task.isCancelled else { return false }

View File

@@ -1,12 +1,12 @@
import Testing
@testable import OpenClaw
import Testing
private actor SimpleTaskSignal {
private var signaled = false
private var waiters: [CheckedContinuation<Void, Never>] = []
func signal() {
self.signaled = true
signaled = true
let waiters = self.waiters
self.waiters.removeAll()
for waiter in waiters {
@@ -15,7 +15,7 @@ private actor SimpleTaskSignal {
}
func wait() async {
if self.signaled {
if signaled {
return
}
await withCheckedContinuation { continuation in
@@ -28,11 +28,11 @@ private actor SimpleTaskOperationProbe {
private var recordedCalls: [String] = []
func recordCall(_ value: String) {
self.recordedCalls.append(value)
recordedCalls.append(value)
}
func calls() -> [String] {
self.recordedCalls
recordedCalls
}
}
@@ -53,7 +53,8 @@ struct SimpleTaskSupportTests {
},
operation: {
await operation.recordCall("loop")
})
}
)
await sleepStarted.wait()
guard let runningTask = task else {
@@ -84,7 +85,8 @@ struct SimpleTaskSupportTests {
},
operation: {
await operation.recordCall("first")
})
}
)
await firstSleepStarted.wait()
guard let firstTask = task else {
@@ -98,7 +100,8 @@ struct SimpleTaskSupportTests {
sleep: { _ in },
operation: {
await operation.recordCall("second")
})
}
)
guard let secondTask = task else {
Issue.record("replacement task did not start")
return
@@ -111,4 +114,36 @@ struct SimpleTaskSupportTests {
#expect(await operation.calls() == ["second"])
#expect(task == nil)
}
@Test
@MainActor
func `cancelling after sleep completes does not run the scheduled operation`() async {
let operation = SimpleTaskOperationProbe()
var task: Task<Void, Never>?
SimpleTaskSupport.schedule(
task: &task,
delay: 0,
sleep: { _ in },
beforeOperationCheck: {
withUnsafeCurrentTask { currentTask in
currentTask?.cancel()
}
},
operation: {
await operation.recordCall("stale")
}
)
guard let runningTask = task else {
Issue.record("scheduled task did not start")
return
}
await runningTask.value
SimpleTaskSupport.stop(task: &task)
#expect(await operation.calls().isEmpty)
#expect(task == nil)
}
}