فهرست منبع

Update Swift Task Continuation Misuse fix for Swift 6 compatibility

- Replace NSLock with actor-based approach for thread safety
- Ensure Swift 6 strict concurrency compliance
- Add handling for CancellationError in test cases
- Verified with 100x repetition testing to confirm race condition is resolved
- No continuation misuse warnings with Swift 6 strict concurrency enabled

Manual testing confirmed:
- Single test runs: No issues detected
- 100x repetition: Previously reproduced issue, now fixed
- Swift 6 compilation: No concurrency warnings
onevcat 8 ماه پیش
والد
کامیت
80b808e789

+ 5 - 0
Sources/General/KingfisherError.swift

@@ -74,6 +74,8 @@ public enum KingfisherError: Error {
         ///
         /// Error Code: 1004
         case livePhotoTaskCancelled(source: LivePhotoSource)
+        
+        case asyncTaskContextCancelled
     }
     
     /// Represents the error reason during networking response phase.
@@ -500,6 +502,8 @@ extension KingfisherError.RequestErrorReason {
             return "The session task was cancelled. Task: \(task), cancel token: \(token)."
         case .livePhotoTaskCancelled(let source):
             return "The live photo download task was cancelled. Source: \(source)"
+        case .asyncTaskContextCancelled:
+            return "The async task context was cancelled. This usually happens when the task is cancelled before it starts."
         }
     }
     
@@ -509,6 +513,7 @@ extension KingfisherError.RequestErrorReason {
         case .invalidURL: return 1002
         case .taskCancelled: return 1003
         case .livePhotoTaskCancelled: return 1004
+        case .asyncTaskContextCancelled: return 1005
         }
     }
 }

+ 18 - 8
Sources/General/KingfisherManager.swift

@@ -838,16 +838,26 @@ extension KingfisherManager {
         let task = CancellationDownloadTask()
         return try await withTaskCancellationHandler {
             try await withCheckedThrowingContinuation { continuation in
-                // Use a lock to ensure continuation is only resumed once
-                let lock = NSLock()
-                var isResumed = false
+                // Use an actor to ensure continuation is only resumed once in a Swift 6 compatible way
+                actor ContinuationState {
+                    var isResumed = false
+                    
+                    func tryResume() -> Bool {
+                        if !isResumed {
+                            isResumed = true
+                            return true
+                        }
+                        return false
+                    }
+                }
+                
+                let state = ContinuationState()
                 
                 @Sendable func safeResume(with result: Result<RetrieveImageResult, KingfisherError>) {
-                    lock.lock()
-                    defer { lock.unlock() }
-                    if !isResumed {
-                        isResumed = true
-                        continuation.resume(with: result)
+                    Task {
+                        if await state.tryResume() {
+                            continuation.resume(with: result)
+                        }
                     }
                 }
                 

+ 2 - 0
Tests/KingfisherTests/KingfisherManagerTests.swift

@@ -255,6 +255,8 @@ class KingfisherManagerTests: XCTestCase {
                     // This should be a cancellation error
                     if let kfError = error as? KingfisherError, kfError.isTaskCancelled {
                         // Expected cancellation
+                    } else if error is CancellationError {
+                        // Expected cancellation
                     } else {
                         print("Task \(i) failed with unexpected error: \(error)")
                     }