Skip to content

Commit 10c7fab

Browse files
authored
Merge pull request #2444 from darkbrewx/fix/task-add-overwrite
Fix non-atomic task creation for concurrent same-URL requests
2 parents dd3c422 + 297fa4f commit 10c7fab

File tree

2 files changed

+48
-0
lines changed

2 files changed

+48
-0
lines changed

Sources/Networking/ImageDownloader.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,8 @@ open class ImageDownloader: @unchecked Sendable {
248248
// The session bound to the downloader.
249249
private var session: URLSession
250250

251+
private let lock = NSLock()
252+
251253
// MARK: Initializers
252254

253255
/// Creates a downloader with a given name.
@@ -375,6 +377,9 @@ open class ImageDownloader: @unchecked Sendable {
375377
callback: SessionDataTask.TaskCallback
376378
) -> DownloadTask
377379
{
380+
lock.lock()
381+
defer { lock.unlock() }
382+
378383
// Ready to start download. Add it to session task manager (`sessionHandler`)
379384
let downloadTask: DownloadTask
380385
if let existingTask = sessionDelegate.task(for: context.url) {

Tests/KingfisherTests/ImageDownloaderTests.swift

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -684,6 +684,49 @@ class ImageDownloaderTests: XCTestCase {
684684
XCTAssertEqual(result.originalData, testImageData)
685685
XCTAssertEqual(result.url, url)
686686
}
687+
688+
func testConcurrentDownloadSameURL() {
689+
let exp = expectation(description: #function)
690+
691+
// Given
692+
let url = testURLs[0]
693+
stub(url, data: testImageData)
694+
695+
let callbackLock = NSLock()
696+
var callbackCount = 0
697+
let expectedCount = 10
698+
exp.expectedFulfillmentCount = expectedCount
699+
700+
// When
701+
DispatchQueue.concurrentPerform(iterations: expectedCount) { index in
702+
downloader.downloadImage(with: url) { result in
703+
switch result {
704+
case .success(let imageResult):
705+
XCTAssertNotNil(imageResult.image)
706+
XCTAssertEqual(imageResult.url, url)
707+
XCTAssertEqual(imageResult.originalData, testImageData)
708+
709+
callbackLock.lock()
710+
callbackCount += 1
711+
callbackLock.unlock()
712+
713+
exp.fulfill()
714+
715+
case .failure(let error):
716+
XCTFail("Download should succeed: \(error)")
717+
exp.fulfill()
718+
}
719+
}
720+
}
721+
722+
// Then
723+
waitForExpectations(timeout: 3) { _ in
724+
callbackLock.lock()
725+
let finalCount = callbackCount
726+
callbackLock.unlock()
727+
XCTAssertEqual(finalCount, expectedCount, "All \(expectedCount) concurrent requests should receive callbacks")
728+
}
729+
}
687730
}
688731

689732
class URLNilDataModifier: ImageDownloaderDelegate {

0 commit comments

Comments
 (0)