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

Download support #33

Merged
merged 13 commits into from
Jun 6, 2023
Merged

Download support #33

merged 13 commits into from
Jun 6, 2023

Conversation

matejmolnar
Copy link
Collaborator

@matejmolnar matejmolnar commented Mar 7, 2023

A rough implementation of a working DownloadAPIManager with an example scene.

  • Separate manager to deal with download tasks DownloadAPIManager
  • It's now possible to use background urlSession configuration specific for downloading files
  • Retry handling moved to a protocol Retryable to avoid duplicate code
  • Download simulation UI to test download tasks with the option to run/suspend/cancel

@matejmolnar matejmolnar linked an issue Mar 7, 2023 that may be closed by this pull request
Base automatically changed from dev to master March 13, 2023 13:07
@gajddo00 gajddo00 changed the title Download support v2 Download support May 9, 2023
@gajddo00 gajddo00 changed the base branch from master to dev May 9, 2023 08:45
@gajddo00 gajddo00 marked this pull request as ready for review May 9, 2023 08:45
@gajddo00 gajddo00 added the enhancement New feature or request label May 9, 2023
@gajddo00 gajddo00 requested a review from cejanen May 9, 2023 08:46
Copy link
Collaborator

@cejanen cejanen left a comment

Choose a reason for hiding this comment

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

Hola, 👏
thank you, finally checked the PR, I would say the current approach is feasible and nice. I found minor changes we can improve

@gajddo00 gajddo00 requested a review from cejanen May 12, 2023 09:13
Copy link

@brenovaladao brenovaladao left a comment

Choose a reason for hiding this comment

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

🪙

Comment on lines 58 to 60
.onAppear {
viewModel.onAppear()
}

Choose a reason for hiding this comment

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

Suggested change
.onAppear {
viewModel.onAppear()
}
.task {
await viewModel.onAppear()
}

Copy link
Collaborator

Choose a reason for hiding this comment

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

👍 thinking also about better name for viewModel function, maybe startDownloadTasks maybe better? Or any other suggestions?

Copy link
Contributor

@gajddo00 gajddo00 May 25, 2023

Choose a reason for hiding this comment

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

It's not starting the download, so I'd suggest something like startObservingDownloadProgress.

Comment on lines 18 to 19
func onAppear() {
Task {

Choose a reason for hiding this comment

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

same here

Copy link
Contributor

@gajddo00 gajddo00 May 25, 2023

Choose a reason for hiding this comment

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

method not used and not necessary

Sources/Networking/Core/DownloadAPIManager.swift Outdated Show resolved Hide resolved
Copy link
Contributor

@tonyskansf tonyskansf left a comment

Choose a reason for hiding this comment

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

👋

The sample app crashes on both the simulator and real device when multiple downloads are ongoing (at 5 downloads). Reproducible on every try.

See crash logs.

Sources/Networking/Misc/URLSessionTask+asyncResponse.swift Outdated Show resolved Hide resolved
Comment on lines 179 to 182
public func urlSession(_: URLSession, downloadTask: URLSessionDownloadTask, didWriteData _: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
downloadStateDict[downloadTask]?.downloadedBytes = totalBytesWritten
downloadStateDict[downloadTask]?.totalBytes = totalBytesExpectedToWrite
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Could this be the place where (accessing the downloadStateDict) it's not thread-safe and perhaps causing the crash? Maybe using an actor would be safer.

Copy link
Collaborator

Choose a reason for hiding this comment

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

👍 similar we have for running calls and retry mechanism

Copy link
Contributor

Choose a reason for hiding this comment

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

agreed, all props which are being mutated should be thread safe

Sources/Networking/Core/DownloadAPIManager.swift Outdated Show resolved Hide resolved
Sources/Networking/Misc/URLSessionTask+asyncResponse.swift Outdated Show resolved Hide resolved
Sources/Networking/Misc/URLSessionTask+asyncResponse.swift Outdated Show resolved Hide resolved
Copy link
Collaborator

@cejanen cejanen left a comment

Choose a reason for hiding this comment

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

My pieces also added
Thanks guys for great comments

Comment on lines 58 to 60
.onAppear {
viewModel.onAppear()
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

👍 thinking also about better name for viewModel function, maybe startDownloadTasks maybe better? Or any other suggestions?

Comment on lines 179 to 182
public func urlSession(_: URLSession, downloadTask: URLSessionDownloadTask, didWriteData _: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
downloadStateDict[downloadTask]?.downloadedBytes = totalBytesWritten
downloadStateDict[downloadTask]?.totalBytes = totalBytesExpectedToWrite
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

👍 similar we have for running calls and retry mechanism

Sources/Networking/Misc/URLSessionTask+asyncResponse.swift Outdated Show resolved Hide resolved
@gajddo00 gajddo00 requested review from tonyskansf and cejanen May 25, 2023 08:14
Copy link
Contributor

@tonyskansf tonyskansf left a comment

Choose a reason for hiding this comment

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

The crash is now fixed with the actors 👍

From further testing, the memory graph showed that there is a memory leak — the DownloadAPIManager is not being released.

image

This is what I've changed to test it:

  • Initialize the DownloadAPIManager only once in the DownloadsViewModel and pass this instance down to the DownloadProgressViewModel instead of using shared.

Going from the examples list to Downloads and back (without tapping "Download") will cause the memory leak. This seems to be the source:

urlSession = URLSession(
    configuration: urlSessionConfiguration,
    delegate: self,
    delegateQueue: OperationQueue()
)

@gajddo00
Copy link
Contributor

gajddo00 commented May 25, 2023

By design, the DownloadAPIManager should be used as a singleton object. URLSession keeps strong reference and Apple also recommends using a shared urlSession, otherwise we'll need the user to free the urlSession manually by calling either

  • urlSession.finishTasksAndInvalidate()
  • urlSession.invalidateAndCancel()

I added this as an option in case somebody would like to use a non-singleton instance in the future so they could invalidate the session manually by calling

deinit {
      downloadAPIManager.invalidateSession(true)
}

Give it a try 👍

@gajddo00 gajddo00 requested a review from brenovaladao May 29, 2023 07:31
@brenovaladao
Copy link

brenovaladao commented May 30, 2023

When testing the app on the simulator, when I tap on the download button it always starts the download twice, is it the intended behavior? - I see there are 2 tasks in the VM, so I think it's 🤔 in that case, I'd rather put this in a more explicit way in the method name / button copy

@gajddo00
Copy link
Contributor

Yes, that was just exemplary to test out the support for simultaneous downloads. Let's keep it as the original - one download showcase.

Copy link
Collaborator

@cejanen cejanen left a comment

Choose a reason for hiding this comment

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

Another round of minor updates

@gajddo00 gajddo00 requested a review from cejanen May 31, 2023 06:40
brenovaladao
brenovaladao previously approved these changes May 31, 2023
Copy link

@brenovaladao brenovaladao left a comment

Choose a reason for hiding this comment

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

👍🏼

Comment on lines +136 to +152
do {
/// If retry fails (retryCount is 0 or Task.sleep thrown), catch the error and process it with `ErrorProcessing` plugins.
try await sleepIfRetry(
for: error,
endpointRequest: endpointRequest,
retryConfiguration: retryConfiguration
)

return try await downloadRequest(
endpointRequest,
resumableData: resumableData,
retryConfiguration: retryConfiguration
)
} catch {
/// error processing
throw await errorProcessors.process(error, for: endpointRequest)
}

Choose a reason for hiding this comment

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

maybe this could go in another method

cejanen
cejanen previously approved these changes Jun 4, 2023
Copy link
Collaborator

@cejanen cejanen left a comment

Choose a reason for hiding this comment

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

I happy with final result. No code is perfect but we enhanced it a lot in PR. @tonyskansf can you just confirm your concerns about strong reference in URLSession delegate are fixed or explained?
Thank you all!

👏

Copy link
Contributor

@tonyskansf tonyskansf left a comment

Choose a reason for hiding this comment

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

By design, the DownloadAPIManager should be used as a singleton object. URLSession keeps strong reference and Apple also recommends using a shared urlSession, otherwise we'll need the user to free the urlSession manually

What specific part of the implementation suggests using the DownloadAPIManager as a singleton prior to 4f25236? I thought it was just for the sake of the example app 😄

Out of curiosity and scope of this PR, is it possible to let the concrete implementation of the DownloadAPIManaging handle the URL session instead of leaving this task up to the dev who is error-prone? For example, the URL session could be invalidated upon the deinitialization of the manager instance. That way, the lifecycle of the URL session would be bound to it. 🤔

// DownloadAPIManager
deinit {
    invalidateSession()
}

Sources/Networking/Core/DownloadAPIManaging.swift Outdated Show resolved Hide resolved
Co-authored-by: Tony Ngo <tonyngo.jr@gmail.com>
@gajddo00 gajddo00 dismissed stale reviews from cejanen and brenovaladao via f00c16c June 5, 2023 14:15
@gajddo00
Copy link
Contributor

gajddo00 commented Jun 5, 2023

There was no documentation, only Matej's intent I guess, that's why I added that in as well. The thing is, that deinit method of the manager instance will never get called because of the strong reference, so the user has to do it externally. 😮‍💨 I guess the only way would be to create the session every time a download is requested and then destroy it after the download is finished.

Copy link
Contributor

@tonyskansf tonyskansf left a comment

Choose a reason for hiding this comment

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

I guess the only way would be to create the session every time a download is requested and then destroy it after the download is finished.

If that would make sense that could be an improvement. I think for now, whoever will use this lib and want this behavior can provide a different DownloadAPIManaging implementation or subclass the one provided.

@gajddo00
Copy link
Contributor

gajddo00 commented Jun 5, 2023

I think I'll try that but in a separate enhancement PR to test if it's a better solution 👍

@gajddo00 gajddo00 merged commit a0821d0 into dev Jun 6, 2023
@gajddo00 gajddo00 deleted the feat/download-support-v2 branch June 6, 2023 07:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Simplistic download
5 participants