Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix race condition in ImageDownloader #4587

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -419,7 +419,7 @@ jobs:
command: xcodebuild -resolvePackageDependencies
- run:
name: MapboxNavigation-Package
command: xcodebuild -sdk iphonesimulator -destination 'platform=iOS Simulator,OS=<< parameters.iOS >>,name=<< parameters.device >>' -scheme MapboxNavigation-Package -configuration << parameters.configuration >> build <<# parameters.test >>test <</ parameters.test >> <<# parameters.codecoverage >>-enableCodeCoverage YES<</ parameters.codecoverage >> ENABLE_TESTABILITY=YES CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED="NO"
command: xcodebuild -enableThreadSanitizer YES -run-tests-until-failure -test-repetition-relaunch-enabled YES -only-testing:MapboxNavigationTests/ImageDownloaderTests -sdk iphonesimulator -destination 'platform=iOS Simulator,OS=<< parameters.iOS >>,name=<< parameters.device >>' -scheme MapboxNavigation-Package -configuration << parameters.configuration >> build <<# parameters.test >>test <</ parameters.test >> <<# parameters.codecoverage >>-enableCodeCoverage YES<</ parameters.codecoverage >> ENABLE_TESTABILITY=YES CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED="NO"
# FIXME: SPM test host is currently disabled, but we should run tests on the SPM test host job. When it is reenabled, delete this section that generates the code coverage report.
- when:
condition: << parameters.codecoverage >>
Expand Down
2 changes: 1 addition & 1 deletion Sources/MapboxNavigation/ImageDownload.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ final class ImageDownloadOperation: Operation, ImageDownload {
return state == .finished
}

override var isConcurrent: Bool {
override var isAsynchronous: Bool {
return true
}

Expand Down
13 changes: 5 additions & 8 deletions Sources/MapboxNavigation/ImageDownloader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ typealias CachedResponseCompletionHandler = (CachedURLResponse?, Error?) -> Void
typealias ImageDownloadCompletionHandler = (DownloadError?) -> Void

protocol ReentrantImageDownloader {
func download(with url: URL, completion: CachedResponseCompletionHandler?) -> Void
func download(with url: URL, completion: @escaping CachedResponseCompletionHandler) -> Void
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see why we would ever want completion to be nil, changed to non-nil.

func activeOperation(with url: URL) -> ImageDownload?
func setOperationType(_ operationType: ImageDownload.Type?)
}
Expand Down Expand Up @@ -42,22 +42,19 @@ class ImageDownloader: NSObject, ReentrantImageDownloader, URLSessionDataDelegat
urlSession.invalidateAndCancel()
}

func download(with url: URL, completion: CachedResponseCompletionHandler?) {
func download(with url: URL, completion: @escaping CachedResponseCompletionHandler) {
accessQueue.sync {
let request = URLRequest(url)
var operation: ImageDownload
if let activeOperation = self.unsafeActiveOperation(with: url) {
operation = activeOperation
activeOperation.addCompletion(completion)
} else {
operation = self.operationType.init(request: request, in: self.urlSession)
let operation = self.operationType.init(request: request, in: self.urlSession)
operation.addCompletion(completion)
self.operations[url] = operation
if let operation = operation as? Operation {
self.downloadQueue.addOperation(operation)
}
}
if let completion = completion {
operation.addCompletion(completion)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ final class ReentrantImageDownloaderSpy: ReentrantImageDownloader {
var returnedDownloadResults = [URL: Data]()
var returnedOperation: ImageDownload?

func download(with url: URL, completion: CachedResponseCompletionHandler?) -> Void {
func download(with url: URL, completion: @escaping CachedResponseCompletionHandler) -> Void {
passedDownloadUrl = url
let response = cachedResponse(with: returnedDownloadResults[url], url: url)
completion?(response, response == nil ? DirectionsError.noData : nil)
completion(response, response == nil ? DirectionsError.noData : nil)
}

func activeOperation(with url: URL) -> ImageDownload? {
Expand Down
4 changes: 2 additions & 2 deletions Tests/MapboxNavigationTests/ImageDownloaderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ class ImageDownloaderTests: TestCase {

func testDownloadWithImmidiateCancel() {
let incorrectUrl = URL(fileURLWithPath: "/incorrect_url")
downloader.download(with: incorrectUrl, completion: nil)
downloader.download(with: incorrectUrl, completion: { _,_ in })
let operation = (downloader.activeOperation(with: incorrectUrl) as! ImageDownloadOperation)
operation.cancel()

Expand All @@ -181,7 +181,7 @@ class ImageDownloaderTests: TestCase {

func testDownloadWithImmidiateCancelFromAnotherThread() {
let incorrectUrl = URL(fileURLWithPath: "/incorrect_url")
downloader.download(with: incorrectUrl, completion: nil)
downloader.download(with: incorrectUrl, completion: { _,_ in })
let operation = (downloader.activeOperation(with: incorrectUrl) as! ImageDownloadOperation)
DispatchQueue.global().sync {
operation.cancel()
Expand Down