Skip to content

Commit

Permalink
[Storage] Fix fetcherServiceMap potential race (#13446)
Browse files Browse the repository at this point in the history
  • Loading branch information
paulb777 authored Aug 2, 2024
1 parent 27d462b commit 9b5be20
Show file tree
Hide file tree
Showing 11 changed files with 164 additions and 308 deletions.
55 changes: 31 additions & 24 deletions FirebaseStorage/Sources/Internal/StorageFetcherService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,44 +23,43 @@ import Foundation
/// Manage Storage's fetcherService
@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
actor StorageFetcherService {
static let shared = StorageFetcherService()

private var _fetcherService: GTMSessionFetcherService?

func fetcherService(_ storage: Storage) -> GTMSessionFetcherService {
func service(_ storage: Storage) async -> GTMSessionFetcherService {
if let _fetcherService {
return _fetcherService
}
let app = storage.app
if StorageFetcherService.fetcherServiceMap[app.name] == nil {
StorageFetcherService.fetcherServiceMap[app.name] = [:]
}
var fetcherService = StorageFetcherService.fetcherServiceMap[app.name]?[storage.storageBucket]
if fetcherService == nil {
fetcherService = GTMSessionFetcherService()
fetcherService?.isRetryEnabled = true
fetcherService?.retryBlock = retryWhenOffline
fetcherService?.allowLocalhostRequest = true
fetcherService?.maxRetryInterval = storage.maxOperationRetryInterval
fetcherService?.testBlock = testBlock
if let fetcherService = getFromMap(appName: app.name, bucket: storage.storageBucket) {
return fetcherService
} else {
let fetcherService = GTMSessionFetcherService()
fetcherService.isRetryEnabled = true
fetcherService.retryBlock = retryWhenOffline
fetcherService.allowLocalhostRequest = true
fetcherService.maxRetryInterval = storage.maxOperationRetryInterval
fetcherService.testBlock = testBlock
let authorizer = StorageTokenAuthorizer(
googleAppID: app.options.googleAppID,
callbackQueue: storage.callbackQueue,
authProvider: storage.auth,
appCheck: storage.appCheck
)
fetcherService?.authorizer = authorizer
StorageFetcherService.fetcherServiceMap[app.name]?[storage.storageBucket] = fetcherService
}
if storage.usesEmulator {
fetcherService?.allowLocalhostRequest = true
fetcherService?.allowedInsecureSchemes = ["http"]
fetcherService.authorizer = authorizer
if storage.usesEmulator {
fetcherService.allowLocalhostRequest = true
fetcherService.allowedInsecureSchemes = ["http"]
}
setMap(appName: app.name, bucket: storage.storageBucket, fetcher: fetcherService)
return fetcherService
}
_fetcherService = fetcherService
return fetcherService!
}

/// Update the testBlock for unit testing. Save it as a property since this may be called before
/// fetcherService is initialized.
func updateTestBlock(_ block: @escaping GTMSessionFetcherTestBlock) {
func updateTestBlock(_ block: GTMSessionFetcherTestBlock?) {
testBlock = block
if let _fetcherService {
_fetcherService.testBlock = testBlock
Expand All @@ -69,9 +68,6 @@ actor StorageFetcherService {

private var testBlock: GTMSessionFetcherTestBlock?

/// Map of apps to a dictionary of buckets to GTMSessionFetcherService.
private static var fetcherServiceMap: [String: [String: GTMSessionFetcherService]] = [:]

private var retryWhenOffline: GTMSessionFetcherRetryBlock = {
(suggestedWillRetry: Bool,
error: Error?,
Expand All @@ -84,4 +80,15 @@ actor StorageFetcherService {
}
response(shouldRetry)
}

/// Map of apps to a dictionary of buckets to GTMSessionFetcherService.
private var fetcherServiceMap: [String: [String: GTMSessionFetcherService]] = [:]

private func getFromMap(appName: String, bucket: String) -> GTMSessionFetcherService? {
return fetcherServiceMap[appName]?[bucket]
}

private func setMap(appName: String, bucket: String, fetcher: GTMSessionFetcherService) {
fetcherServiceMap[appName, default: [:]][bucket] = fetcher
}
}
4 changes: 1 addition & 3 deletions FirebaseStorage/Sources/Internal/StorageInternalTask.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,7 @@ class StorageInternalTask: StorageTask {
dispatchQueue.async { [self] in
self.state = .queueing
Task {
let fetcherService = await reference.storage.fetcherService
.fetcherService(reference.storage)

let fetcherService = await StorageFetcherService.shared.service(reference.storage)
var request = request ?? self.baseRequest
request.httpMethod = httpMethod
request.timeoutInterval = self.reference.storage.maxOperationRetryTime
Expand Down
2 changes: 0 additions & 2 deletions FirebaseStorage/Sources/Storage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -266,8 +266,6 @@ import FirebaseCore
}
}

let fetcherService = StorageFetcherService()

let dispatchQueue: DispatchQueue

init(app: FirebaseApp, bucket: String) {
Expand Down
2 changes: 1 addition & 1 deletion FirebaseStorage/Sources/StorageDownloadTask.swift
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ open class StorageDownloadTask: StorageObservableTask, StorageTaskManagement {
fetcher = GTMSessionFetcher(downloadResumeData: resumeData)
fetcher.comment = "Resuming DownloadTask"
} else {
let fetcherService = await reference.storage.fetcherService.fetcherService(reference.storage)
let fetcherService = await StorageFetcherService.shared.service(reference.storage)

fetcher = fetcherService.fetcher(with: request)
fetcher.comment = "Starting DownloadTask"
Expand Down
3 changes: 1 addition & 2 deletions FirebaseStorage/Sources/StorageUploadTask.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,7 @@ import Foundation
let bodyData = try? JSONSerialization.data(withJSONObject: dataRepresentation)

Task {
let fetcherService = await reference.storage.fetcherService
.fetcherService(reference.storage)
let fetcherService = await StorageFetcherService.shared.service(reference.storage)
var request = self.baseRequest
request.httpMethod = "POST"
request.timeoutInterval = self.reference.storage.maxUploadRetryTime
Expand Down
2 changes: 0 additions & 2 deletions FirebaseStorage/Tests/Unit/StorageAuthorizerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ class StorageAuthorizerTests: StorageTestHelpers {
var appCheckTokenSuccess: FIRAppCheckTokenResultFake!
var appCheckTokenError: FIRAppCheckTokenResultFake!
var fetcher: GTMSessionFetcher!
var fetcherService: GTMSessionFetcherService!
var auth: FIRAuthInteropFake!
var appCheck: FIRAppCheckFake!

Expand Down Expand Up @@ -52,7 +51,6 @@ class StorageAuthorizerTests: StorageTestHelpers {

override func tearDown() {
fetcher = nil
fetcherService = nil
auth = nil
appCheck = nil
appCheckTokenSuccess = nil
Expand Down
93 changes: 26 additions & 67 deletions FirebaseStorage/Tests/Unit/StorageDeleteTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,29 +19,9 @@ import XCTest

@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
class StorageDeleteTests: StorageTestHelpers {
var fetcherService: GTMSessionFetcherService?
var dispatchQueue: DispatchQueue?

override func setUp() {
super.setUp()
fetcherService = GTMSessionFetcherService()
fetcherService?.authorizer = StorageTokenAuthorizer(
googleAppID: "dummyAppID",
authProvider: nil,
appCheck: nil
)
dispatchQueue = DispatchQueue(label: "Test dispatch queue")
}

override func tearDown() {
fetcherService = nil
super.tearDown()
}

func testFetcherConfiguration() {
let expectation = self.expectation(description: #function)
fetcherService!.testBlock = { (fetcher: GTMSessionFetcher!,
response: GTMSessionFetcherTestResponse) in
func testFetcherConfiguration() async {
let testBlock = { (fetcher: GTMSessionFetcher!,
response: GTMSessionFetcherTestResponse) in
XCTAssertEqual(fetcher.request?.url, self.objectURL())
XCTAssertEqual(fetcher.request?.httpMethod, "DELETE")
let httpResponse = HTTPURLResponse(
Expand All @@ -52,67 +32,46 @@ class StorageDeleteTests: StorageTestHelpers {
)
response(httpResponse, nil, nil)
}
await StorageFetcherService.shared.updateTestBlock(testBlock)
let path = objectPath()
let ref = StorageReference(storage: storage(), path: path)
StorageDeleteTask.deleteTask(
reference: ref,
queue: dispatchQueue!.self
) { _, error in
expectation.fulfill()
do {
let _ = try await ref.delete()
} catch {
// All testing is in test block.
}
waitForExpectation(test: self)
}

func testSuccessfulFetch() {
let expectation = self.expectation(description: #function)
fetcherService!.testBlock = { (fetcher: GTMSessionFetcher!,
response: GTMSessionFetcherTestResponse) in
XCTAssertEqual(fetcher.request?.url, self.objectURL())
XCTAssertEqual(fetcher.request?.httpMethod, "DELETE")
let httpResponse = HTTPURLResponse(
url: (fetcher.request?.url)!,
statusCode: 200,
httpVersion: "HTTP/1.1",
headerFields: nil
)
response(httpResponse, nil, nil)
}
func testSuccessfulFetch() async {
await StorageFetcherService.shared.updateTestBlock(successBlock())
let path = objectPath()
let ref = StorageReference(storage: storage(), path: path)
StorageDeleteTask.deleteTask(
reference: ref,
queue: dispatchQueue!.self
) { _, error in
expectation.fulfill()
do {
let _ = try await ref.delete()
} catch {
// All testing is in test block.
}
waitForExpectation(test: self)
}

func testSuccessfulFetchWithEmulator() {
let expectation = self.expectation(description: #function)
func testSuccessfulFetchWithEmulator() async {
let storage = self.storage()
storage.useEmulator(withHost: "localhost", port: 8080)
fetcherService?.allowLocalhostRequest = true

fetcherService!
.testBlock = successBlock(
withURL: URL(string: "http://localhost:8080/v0/b/bucket/o/object")!
)

let testBlock = successBlock(
withURL: URL(string: "http://localhost:8080/v0/b/bucket/o/object")!
)
await StorageFetcherService.shared.updateTestBlock(successBlock())
let path = objectPath()
let ref = StorageReference(storage: storage, path: path)
StorageDeleteTask.deleteTask(
reference: ref,
queue: dispatchQueue!.self
) { _, error in
expectation.fulfill()
do {
let _ = try await ref.delete()
} catch {
// All testing is in test block.
}
waitForExpectation(test: self)
}

func testUnsuccessfulFetchUnauthenticated() async {
let storage = storage()
await storage.fetcherService.updateTestBlock(unauthenticatedBlock())
await StorageFetcherService.shared.updateTestBlock(unauthenticatedBlock())
let path = objectPath()
let ref = StorageReference(storage: storage, path: path)
do {
Expand All @@ -124,7 +83,7 @@ class StorageDeleteTests: StorageTestHelpers {

func testUnsuccessfulFetchUnauthorized() async {
let storage = storage()
await storage.fetcherService.updateTestBlock(unauthorizedBlock())
await StorageFetcherService.shared.updateTestBlock(unauthorizedBlock())
let path = objectPath()
let ref = StorageReference(storage: storage, path: path)
do {
Expand All @@ -136,7 +95,7 @@ class StorageDeleteTests: StorageTestHelpers {

func testUnsuccessfulFetchObjectDoesntExist() async {
let storage = storage()
await storage.fetcherService.updateTestBlock(notFoundBlock())
await StorageFetcherService.shared.updateTestBlock(notFoundBlock())
let path = objectPath()
let ref = StorageReference(storage: storage, path: path)
do {
Expand Down
Loading

0 comments on commit 9b5be20

Please sign in to comment.