Skip to content

Commit d7950dd

Browse files
authored
Separate error messages into extensions (#7322)
* Restores ML Pods after M77. * Fix Package.swift * Re-add catalyst to GHA workflow. * Update error handling. * Add logging for background download failure. * Remove empty file. * Slight refactor of error descriptions + using ISO8601DateFormatter where available. * Slight refactor error descriptions + adding availability for ISO8601DateFormatter.
1 parent ac04fb6 commit d7950dd

File tree

9 files changed

+210
-61
lines changed

9 files changed

+210
-61
lines changed

FirebaseMLModelDownloader/Sources/DeviceLogger.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ enum LoggerMessageCode {
2222
case downloadedModelMovedToURL
2323
case analyticsEventEncodeError
2424
case telemetryInitError
25+
case backgroundDownloadError
2526
}
2627

2728
/// On-device logger.

FirebaseMLModelDownloader/Sources/LocalModelInfo.swift

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,6 @@ class LocalModelInfo {
2121
/// Model name.
2222
let name: String
2323

24-
/// Download URL for the model file, as returned by server.
25-
let downloadURL: URL
26-
2724
/// Hash of the model, as returned by server.
2825
let modelHash: String
2926

@@ -33,9 +30,8 @@ class LocalModelInfo {
3330
/// Local path of the model.
3431
let path: String
3532

36-
init(name: String, downloadURL: URL, modelHash: String, size: Int, path: String) {
33+
init(name: String, modelHash: String, size: Int, path: String) {
3734
self.name = name
38-
self.downloadURL = downloadURL
3935
self.modelHash = modelHash
4036
self.size = size
4137
self.path = path
@@ -45,7 +41,6 @@ class LocalModelInfo {
4541
convenience init(from remoteModelInfo: RemoteModelInfo, path: String) {
4642
self.init(
4743
name: remoteModelInfo.name,
48-
downloadURL: remoteModelInfo.downloadURL,
4944
modelHash: remoteModelInfo.modelHash,
5045
size: remoteModelInfo.size,
5146
path: path
@@ -55,15 +50,12 @@ class LocalModelInfo {
5550
/// Convenience init to create local model info from stored info in user defaults.
5651
convenience init?(fromDefaults defaults: UserDefaults, name: String, appName: String) {
5752
let defaultsPrefix = LocalModelInfo.getUserDefaultsKeyPrefix(appName: appName, modelName: name)
58-
guard let downloadURL = defaults
59-
.value(forKey: "\(defaultsPrefix).model-download-url") as? String,
60-
let url = URL(string: downloadURL),
61-
let modelHash = defaults.value(forKey: "\(defaultsPrefix).model-hash") as? String,
53+
guard let modelHash = defaults.value(forKey: "\(defaultsPrefix).model-hash") as? String,
6254
let size = defaults.value(forKey: "\(defaultsPrefix).model-size") as? Int,
6355
let path = defaults.value(forKey: "\(defaultsPrefix).model-path") as? String else {
6456
return nil
6557
}
66-
self.init(name: name, downloadURL: url, modelHash: modelHash, size: size, path: path)
58+
self.init(name: name, modelHash: modelHash, size: size, path: path)
6759
}
6860
}
6961

@@ -78,7 +70,6 @@ extension LocalModelInfo: DownloaderUserDefaultsWriteable {
7870
/// Write local model info to user defaults.
7971
func writeToDefaults(_ defaults: UserDefaults, appName: String) {
8072
let defaultsPrefix = LocalModelInfo.getUserDefaultsKeyPrefix(appName: appName, modelName: name)
81-
defaults.setValue(downloadURL.absoluteString, forKey: "\(defaultsPrefix).model-download-url")
8273
defaults.setValue(modelHash, forKey: "\(defaultsPrefix).model-hash")
8374
defaults.setValue(size, forKey: "\(defaultsPrefix).model-size")
8475
defaults.setValue(path, forKey: "\(defaultsPrefix).model-path")

FirebaseMLModelDownloader/Sources/ModelDownloadTask.swift

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,14 +89,37 @@ extension ModelDownloadTask: URLSessionDownloadDelegate {
8989
return "fbml_model__\(appName)__\(remoteModelInfo.name)"
9090
}
9191

92+
/// Handle client-side errors.
9293
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
93-
// TODO: Handle model download url expiry and other download errors
94+
// TODO: Log this.
95+
guard task == downloadTask else { return }
96+
guard let error = error else { return }
97+
/// Unable to resolve hostname or connect to host.
98+
DispatchQueue.main.async {
99+
self.downloadHandlers
100+
.completion(.failure(.internalError(description: error.localizedDescription)))
101+
}
94102
}
95103

96104
func urlSession(_ session: URLSession,
97105
downloadTask: URLSessionDownloadTask,
98106
didFinishDownloadingTo location: URL) {
99-
assert(downloadTask == self.downloadTask)
107+
// TODO: Log this.
108+
guard downloadTask == self.downloadTask else { return }
109+
guard let response = downloadTask.response as? HTTPURLResponse else {
110+
DispatchQueue.main.async {
111+
self.downloadHandlers
112+
.completion(.failure(.internalError(description: ModelDownloadTask
113+
.ErrorDescription.invalidServerResponse)))
114+
}
115+
return
116+
}
117+
118+
guard (200 ..< 299).contains(response.statusCode) else {
119+
// TODO: Handle download url expiry + retries.
120+
return
121+
}
122+
100123
let modelFileURL = ModelFileManager.getDownloadedModelFilePath(
101124
appName: appName,
102125
modelName: remoteModelInfo.name
@@ -109,7 +132,7 @@ extension ModelDownloadTask: URLSessionDownloadDelegate {
109132
DeviceLogger.logEvent(
110133
level: .info,
111134
category: .modelDownload,
112-
message: "Unable to save downloaded remote model file.",
135+
message: ModelDownloadTask.ErrorDescription.saveModel,
113136
messageCode: .modelDownloaded
114137
)
115138
DispatchQueue.main.async {
@@ -122,7 +145,7 @@ extension ModelDownloadTask: URLSessionDownloadDelegate {
122145
DeviceLogger.logEvent(
123146
level: .info,
124147
category: .modelDownload,
125-
message: "Unable to save downloaded remote model file.",
148+
message: ModelDownloadTask.ErrorDescription.saveModel,
126149
messageCode: .modelDownloaded
127150
)
128151
DispatchQueue.main.async {
@@ -154,7 +177,8 @@ extension ModelDownloadTask: URLSessionDownloadDelegate {
154177
didWriteData bytesWritten: Int64,
155178
totalBytesWritten: Int64,
156179
totalBytesExpectedToWrite: Int64) {
157-
assert(downloadTask == self.downloadTask)
180+
// TODO: Log this.
181+
guard downloadTask == self.downloadTask else { return }
158182
/// Check if progress handler is unspecified.
159183
guard let progressHandler = downloadHandlers.progressHandler else { return }
160184
let calculatedProgress = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite)
@@ -163,3 +187,14 @@ extension ModelDownloadTask: URLSessionDownloadDelegate {
163187
}
164188
}
165189
}
190+
191+
/// Possible error messages for model downloading.
192+
extension ModelDownloadTask {
193+
/// Error descriptions.
194+
private enum ErrorDescription {
195+
static let invalidServerResponse =
196+
"Could not get server response for model downloading."
197+
static let saveModel: StaticString =
198+
"Unable to save downloaded remote model file."
199+
}
200+
}

FirebaseMLModelDownloader/Sources/ModelDownloader.swift

Lines changed: 43 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -78,18 +78,20 @@ public class ModelDownloader {
7878
telemetryLogger = TelemetryLogger(app: app)
7979
userDefaults = defaults
8080

81+
let notificationName = "FIRAppDeleteNotification"
8182
NotificationCenter.default.addObserver(
8283
self,
8384
selector: #selector(deleteModelDownloader),
84-
name: Notification.Name("FIRAppDeleteNotification"),
85+
name: Notification.Name(notificationName),
8586
object: nil
8687
)
8788
}
8889

8990
/// Handles app deletion notification.
9091
@objc private func deleteModelDownloader(notification: Notification) {
92+
let userInfoKey = "FIRAppNameKey"
9193
if let userInfo = notification.userInfo,
92-
let appName = userInfo["FIRAppNameKey"] as? String {
94+
let appName = userInfo[userInfoKey] as? String {
9395
ModelDownloader.modelDownloaderDictionary.removeValue(forKey: appName)
9496
// TODO: Clean up user defaults
9597
// TODO: Clean up local instances of app
@@ -99,7 +101,7 @@ public class ModelDownloader {
99101
/// Model downloader with default app.
100102
public static func modelDownloader() -> ModelDownloader {
101103
guard let defaultApp = FirebaseApp.app() else {
102-
fatalError("Default Firebase app not configured.")
104+
fatalError(ModelDownloader.ErrorDescription.defaultAppNotConfigured)
103105
}
104106
return modelDownloader(app: defaultApp)
105107
}
@@ -144,9 +146,14 @@ public class ModelDownloader {
144146
progressHandler: nil,
145147
completion: { result in
146148
switch result {
147-
// TODO: Log outcome of background download
148149
case .success: break
149-
case .failure: break
150+
case .failure:
151+
DeviceLogger.logEvent(
152+
level: .info,
153+
category: .modelDownload,
154+
message: ModelDownloader.ErrorDescription.backgroundModelDownload,
155+
messageCode: .backgroundDownloadError
156+
)
150157
}
151158
}
152159
)
@@ -176,18 +183,22 @@ public class ModelDownloader {
176183
var customModels = Set<CustomModel>()
177184
for path in modelPaths {
178185
guard let modelName = ModelFileManager.getModelNameFromFilePath(path) else {
179-
completion(.failure(.internalError(description: "Invalid model file name.")))
186+
completion(.failure(.internalError(description: ModelDownloader
187+
.ErrorDescription
188+
.parseModelName(path.absoluteString))))
180189
return
181190
}
182191
guard let modelInfo = getLocalModelInfo(modelName: modelName) else {
183192
completion(
184-
.failure(.internalError(description: "Failed to get model info for model file."))
193+
.failure(.internalError(description: ModelDownloader.ErrorDescription
194+
.retrieveLocalModelInfo))
185195
)
186196
return
187197
}
188198
guard modelInfo.path == path.absoluteString else {
189199
completion(
190-
.failure(.internalError(description: "Outdated model paths in local storage."))
200+
.failure(.internalError(description: ModelDownloader.ErrorDescription
201+
.outdatedModelPath))
191202
)
192203
return
193204
}
@@ -206,7 +217,6 @@ public class ModelDownloader {
206217
public func deleteDownloadedModel(name modelName: String,
207218
completion: @escaping (Result<Void, DownloadedModelError>)
208219
-> Void) {
209-
// TODO: Delete previously downloaded model
210220
guard let localModelInfo = getLocalModelInfo(modelName: modelName),
211221
let localPath = URL(string: localModelInfo.path)
212222
else {
@@ -276,16 +286,18 @@ extension ModelDownloader {
276286
progressHandler: progressHandler,
277287
completion: completion
278288
)
289+
// TODO: Is it possible for download URL to expire here?
279290
downloadTask.resumeModelDownload()
280291
/// Local model info is the latest model info.
281292
case .notModified:
282293
guard let localModel = self.getLocalModel(modelName: modelName) else {
283294
DispatchQueue.main.async {
284295
/// This can only happen if either local model info or the model file was suddenly wiped out in the middle of model info request and server response
285-
// TODO: Consider handling: if model file is deleted after local model info is retrieved but before model info network call
296+
// TODO: Consider handling: If model file is deleted after local model info is retrieved but before model info network call.
286297
completion(
287298
.failure(
288-
.internalError(description: "Model unavailable due to deleted local model info.")
299+
.internalError(description: ModelDownloader.ErrorDescription
300+
.deletedLocalModelInfo)
289301
)
290302
)
291303
}
@@ -320,3 +332,23 @@ extension ModelDownloader {
320332
}
321333
}
322334
}
335+
336+
/// Possible error messages while using model downloader.
337+
extension ModelDownloader {
338+
/// Error descriptions.
339+
private enum ErrorDescription {
340+
static let defaultAppNotConfigured =
341+
"Default Firebase app not configured."
342+
static let parseModelName = { (path: String) in
343+
"Unable to parse model file name at \(path)."
344+
}
345+
346+
static let retrieveLocalModelInfo =
347+
"Failed to get stored model info for model file."
348+
static let outdatedModelPath = "Outdated model paths in local storage."
349+
static let deletedLocalModelInfo =
350+
"Model unavailable due to deleted local model info."
351+
static let backgroundModelDownload: StaticString =
352+
"Failed to update model in background."
353+
}
354+
}

FirebaseMLModelDownloader/Sources/ModelFileManager.swift

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,15 +68,17 @@ enum ModelFileManager {
6868
} catch {
6969
throw DownloadError
7070
.internalError(
71-
description: "Could not replace existing model file - \(error.localizedDescription)"
71+
description: ModelFileManager.ErrorDescription
72+
.replaceFile(error.localizedDescription)
7273
)
7374
}
7475
}
7576
do {
7677
try FileManager.default.moveItem(at: sourceURL, to: destinationURL)
7778
} catch {
7879
throw DownloadError
79-
.internalError(description: "Unable to save model file - \(error.localizedDescription)")
80+
.internalError(description: ModelFileManager
81+
.ErrorDescription.saveFile(error.localizedDescription))
8082
}
8183
}
8284

@@ -87,7 +89,7 @@ enum ModelFileManager {
8789
} catch {
8890
throw DownloadedModelError
8991
.internalError(
90-
description: "Could not delete old model file - \(error.localizedDescription)"
92+
description: ModelFileManager.ErrorDescription.deleteFile(error.localizedDescription)
9193
)
9294
}
9395
}
@@ -105,8 +107,31 @@ enum ModelFileManager {
105107
} catch {
106108
throw DownloadedModelError
107109
.internalError(
108-
description: "Could not retrieve model files in directory - \(error.localizedDescription)"
110+
description: ModelFileManager.ErrorDescription
111+
.retrieveFile(error.localizedDescription)
109112
)
110113
}
111114
}
112115
}
116+
117+
/// Possible error messages during file management.
118+
extension ModelFileManager {
119+
/// Error descriptions.
120+
private enum ErrorDescription {
121+
static let retrieveFile = { (error: String) in
122+
"Could not retrieve model files in directory: \(error)"
123+
}
124+
125+
static let deleteFile = { (error: String) in
126+
"Could not delete old model file: \(error)"
127+
}
128+
129+
static let saveFile = { (error: String) in
130+
"Unable to save model file: \(error)"
131+
}
132+
133+
static let replaceFile = { (error: String) in
134+
"Could not replace existing model file: \(error)"
135+
}
136+
}
137+
}

0 commit comments

Comments
 (0)