Skip to content
Open
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: 2 additions & 0 deletions FirebaseStorage/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# 13.1.0
- [fixed] Prevented duplicate upload and download requests when resuming a task before its initial
request setup has completed. (#16771)
- [fixed] Fixed a race where an observer added while an upload or download task was completing
could be called twice for the success or failure event. This could crash `putDataAsync`,
`putFileAsync`, and `writeAsync` when called with an `onProgress` handler. (#16813)
Expand Down
8 changes: 8 additions & 0 deletions FirebaseStorage/Sources/Internal/StorageFetcherService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@ actor StorageFetcherService {

private var _fetcherService: GTMSessionFetcherService?

// Allows unit tests to hold service setup across a task pause/resume transition.
private var serviceWaiterForTesting: (@Sendable () async -> Void)?

func service(_ storage: Storage) async -> GTMSessionFetcherService {
await serviceWaiterForTesting?()
if let _fetcherService {
return _fetcherService
}
Expand Down Expand Up @@ -77,6 +81,10 @@ actor StorageFetcherService {
}
}

func updateServiceWaiterForTesting(_ waiter: (@Sendable () async -> Void)?) {
serviceWaiterForTesting = waiter
}

private var testBlock: GTMSessionFetcherTestBlock?

private var retryWhenOffline: GTMSessionFetcherRetryBlock = {
Expand Down
42 changes: 36 additions & 6 deletions FirebaseStorage/Sources/StorageDownloadTask.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,14 @@ open class StorageDownloadTask: StorageObservableTask, StorageTaskManagement, @u
* Prepares a task and begins execution.
*/
@objc open func enqueue() {
let generation = stateLock.withLock { () -> UInt64? in
guard state == .unknown, fetcher == nil else { return nil }
enqueueGeneration &+= 1
return enqueueGeneration
}
guard let generation else { return }
Task {
await enqueueImplementation()
await enqueueImplementation(generation: generation)
}
}

Expand Down Expand Up @@ -97,12 +103,14 @@ open class StorageDownloadTask: StorageObservableTask, StorageTaskManagement, @u
@objc open func resume() {
var downloadDataToResume: Data?
var snapshotToFire: StorageTaskSnapshot?
var generation: UInt64 = 0
let shouldReturn1 = stateLock.withLock { () -> Bool in
guard state == .unknown || state == .queueing || state == .pausing || state == .paused ||
state == .progress else {
guard state == .unknown || state == .queueing || state == .pausing || state == .paused else {
return true
}
state = .resuming
enqueueGeneration &+= 1
generation = enqueueGeneration
downloadDataToResume = downloadData
snapshotToFire = snapshotUnderLock()
return false
Expand All @@ -121,12 +129,17 @@ open class StorageDownloadTask: StorageObservableTask, StorageTaskManagement, @u

if shouldEnqueue {
Task {
await self.enqueueImplementation(resumeWith: downloadDataToResume)
await self.enqueueImplementation(resumeWith: downloadDataToResume, generation: generation)
}
}
}

private var fetcher: GTMSessionFetcher?
private var enqueueGeneration: UInt64 = 0
var hasFetcherForTesting: Bool {
stateLock.withLock { fetcher != nil }
}

var downloadData: Data?
// Hold completion in object to force it to be retained until completion block is called.
var completionData: ((Data?, Error?) -> Void)?
Expand All @@ -144,16 +157,21 @@ open class StorageDownloadTask: StorageObservableTask, StorageTaskManagement, @u
self.fetcher?.stopFetching()
}

private func enqueueImplementation(resumeWith resumeData: Data? = nil) async {
private func enqueueImplementation(resumeWith resumeData: Data? = nil,
generation: UInt64) async {
let shouldProceed = stateLock.withLock { () -> Bool in
guard generation == enqueueGeneration else { return false }
guard state == .unknown || state == .queueing || state == .resuming || state == .running ||
state == .progress else {
return false
}
state = .queueing
return true
}
if !shouldProceed { return }
if !shouldProceed {
notifySetupDiscardedForTesting()
return
}

var request = baseRequest
request.httpMethod = "GET"
Expand All @@ -168,6 +186,10 @@ open class StorageDownloadTask: StorageObservableTask, StorageTaskManagement, @u
fetcher.comment = "Resuming DownloadTask"
} else {
let fetcherService = await StorageFetcherService.shared.service(reference.storage)
guard stateLock.withLock({ generation == enqueueGeneration }) else {
notifySetupDiscardedForTesting()
return
}
Comment thread
paulb777 marked this conversation as resolved.

fetcher = fetcherService.fetcher(with: request)
fetcher.comment = "Starting DownloadTask"
Expand All @@ -183,6 +205,7 @@ open class StorageDownloadTask: StorageObservableTask, StorageTaskManagement, @u
guard let self = self else { return }
var snapshotToFire: StorageTaskSnapshot?
self.stateLock.withLock {
guard generation == self.enqueueGeneration else { return }
guard self.state == .unknown || self.state == .queueing || self.state == .resuming ||
self.state == .running || self.state == .progress else {
return
Expand All @@ -197,6 +220,7 @@ open class StorageDownloadTask: StorageObservableTask, StorageTaskManagement, @u
}

self.stateLock.withLock {
guard generation == self.enqueueGeneration else { return }
guard self.state == .unknown || self.state == .queueing || self.state == .resuming ||
self.state == .running || self.state == .progress else {
return
Expand All @@ -211,6 +235,7 @@ open class StorageDownloadTask: StorageObservableTask, StorageTaskManagement, @u
guard let self = self, let fetcher = fetcher else { return }
var snapshotToFire: StorageTaskSnapshot?
self.stateLock.withLock {
guard generation == self.enqueueGeneration else { return }
guard self.state == .unknown || self.state == .queueing || self.state == .resuming ||
self.state == .running || self.state == .progress else {
return
Expand All @@ -227,6 +252,7 @@ open class StorageDownloadTask: StorageObservableTask, StorageTaskManagement, @u
}

self.stateLock.withLock {
guard generation == self.enqueueGeneration else { return }
guard self.state == .unknown || self.state == .queueing || self.state == .resuming ||
self.state == .running || self.state == .progress else {
return
Expand All @@ -237,6 +263,7 @@ open class StorageDownloadTask: StorageObservableTask, StorageTaskManagement, @u
}
var isPausing = false
let shouldContinue = stateLock.withLock { () -> Bool in
guard generation == enqueueGeneration else { return false }
if state == .cancelled || state == .pausing || state == .paused {
isPausing = state == .pausing
return false
Expand All @@ -246,6 +273,7 @@ open class StorageDownloadTask: StorageObservableTask, StorageTaskManagement, @u
return true
}
if !shouldContinue {
notifySetupDiscardedForTesting()
if isPausing {
fetcher.resumeDataBlock = { [weak self] (data: Data) in
guard let self = self else { return }
Expand Down Expand Up @@ -274,6 +302,7 @@ open class StorageDownloadTask: StorageObservableTask, StorageTaskManagement, @u
var successSnapshot: StorageTaskSnapshot?

stateLock.withLock {
guard generation == enqueueGeneration else { return }
if state == .cancelled { return }

state = .progress
Expand All @@ -296,6 +325,7 @@ open class StorageDownloadTask: StorageObservableTask, StorageTaskManagement, @u
var failureSnapshot: StorageTaskSnapshot?

stateLock.withLock {
guard generation == enqueueGeneration else { return }
if state == .cancelled || state == .paused || state == .pausing { return }

state = .progress
Expand Down
12 changes: 12 additions & 0 deletions FirebaseStorage/Sources/StorageTask.swift
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,18 @@ import Foundation

let baseRequest: URLRequest

// A narrow synchronization point for tests of task setup cancellation.
private var setupDiscardedHandler: (@Sendable () -> Void)?
var setupDiscardedHandlerForTesting: (@Sendable () -> Void)? {
get { stateLock.withLock { setupDiscardedHandler } }
set { stateLock.withLock { setupDiscardedHandler = newValue } }
}

func notifySetupDiscardedForTesting() {
let handler = stateLock.withLock { setupDiscardedHandler }
handler?()
}

init(reference: StorageReference,
queue: DispatchQueue) {
self.reference = reference
Expand Down
28 changes: 28 additions & 0 deletions FirebaseStorage/Sources/StorageUploadTask.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,22 @@ import Foundation
* Prepares a task and begins execution.
*/
@objc open func enqueue() {
let generation = stateLock.withLock { () -> UInt64? in
guard uploadFetcher == nil,
state == .unknown || state == .resuming || state == .running else {
return nil
}
enqueueGeneration &+= 1
return enqueueGeneration
}
guard let generation else { return }

// Capturing self so that the upload is done whether or not there is a callback.
dispatchQueue.async { [self] in
let contentValidationError = self.contentUploadError()
var failureSnapshot: StorageTaskSnapshot?
let shouldProceed = stateLock.withLock { () -> Bool in
guard generation == enqueueGeneration else { return false }
guard state == .unknown || state == .queueing || state == .resuming || state == .running ||
state == .progress else {
return false
Expand All @@ -65,6 +76,8 @@ import Foundation
if !shouldProceed {
if let failureSnapshot {
self.finishTaskWithStatus(status: .failure, snapshot: failureSnapshot)
} else {
notifySetupDiscardedForTesting()
}
return
}
Expand All @@ -74,6 +87,10 @@ import Foundation

Task {
let fetcherService = await StorageFetcherService.shared.service(reference.storage)
guard stateLock.withLock({ generation == enqueueGeneration }) else {
notifySetupDiscardedForTesting()
return
}
Comment thread
paulb777 marked this conversation as resolved.
var request = self.baseRequest
request.httpMethod = "POST"
request.timeoutInterval = self.reference.storage.maxUploadRetryTime
Expand Down Expand Up @@ -127,6 +144,7 @@ import Foundation
guard let self = self else { return }
var snapshotToFire: StorageTaskSnapshot?
self.stateLock.withLock {
guard generation == self.enqueueGeneration else { return }
guard self.state == .unknown || self.state == .queueing || self.state == .resuming ||
self.state == .running || self.state == .progress else {
return
Expand All @@ -142,6 +160,7 @@ import Foundation
}

self.stateLock.withLock {
guard generation == self.enqueueGeneration else { return }
guard self.state == .unknown || self.state == .queueing || self.state == .resuming ||
self.state == .running || self.state == .progress else {
return
Expand All @@ -152,6 +171,7 @@ import Foundation
// Process fetches
var isPaused = false
let shouldContinue = self.stateLock.withLock { () -> Bool in
guard generation == self.enqueueGeneration else { return false }
if self.state == .cancelled || self.state == .pausing || self.state == .paused {
isPaused = self.state == .paused || self.state == .pausing
return false
Expand All @@ -161,6 +181,7 @@ import Foundation
return true
}
if !shouldContinue {
notifySetupDiscardedForTesting()
if isPaused {
uploadFetcher.pauseFetching()
} else {
Expand All @@ -175,6 +196,7 @@ import Foundation
var failureSnapshot: StorageTaskSnapshot?

self.stateLock.withLock {
guard generation == self.enqueueGeneration else { return }
if self.state == .cancelled { return }

self.state = .progress
Expand Down Expand Up @@ -207,6 +229,7 @@ import Foundation
var failureSnapshot: StorageTaskSnapshot?

self.stateLock.withLock {
guard generation == self.enqueueGeneration else { return }
if self.state == .cancelled || self.state == .paused || self
.state == .pausing { return }

Expand Down Expand Up @@ -320,6 +343,11 @@ import Foundation
}

private var uploadFetcher: GTMSessionUploadFetcher?
private var enqueueGeneration: UInt64 = 0
var hasFetcherForTesting: Bool {
stateLock.withLock { uploadFetcher != nil }
}

private var uploadMetadata: StorageMetadata
private var uploadData: Data?
// Hold completion in object to force it to be retained until completion block is called.
Expand Down
Loading