Change to make sure all queue's get cleared before suspending the database. Issue #1389

This commit is contained in:
Maurice Parker 2019-12-02 14:14:35 -06:00
parent 77cf9a0036
commit f159371967
11 changed files with 213 additions and 35 deletions

View File

@ -406,7 +406,6 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
} }
public func suspend() { public func suspend() {
delegate.cancelAll(for: self)
delegate.suspend() delegate.suspend()
database.suspend() database.suspend()
save() save()

View File

@ -22,7 +22,6 @@ protocol AccountDelegate {
var refreshProgress: DownloadProgress { get } var refreshProgress: DownloadProgress { get }
func cancelAll(for account: Account)
func refreshAll(for account: Account, completion: @escaping (Result<Void, Error>) -> Void) func refreshAll(for account: Account, completion: @escaping (Result<Void, Error>) -> Void)
func sendArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) func sendArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void))
func refreshArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) func refreshArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void))

View File

@ -32,6 +32,7 @@ final class FeedbinAPICaller: NSObject {
private let feedbinBaseURL = URL(string: "https://api.feedbin.com/v2/")! private let feedbinBaseURL = URL(string: "https://api.feedbin.com/v2/")!
private var transport: Transport! private var transport: Transport!
private var suspended = false
var credentials: Credentials? var credentials: Credentials?
weak var accountMetadata: AccountMetadata? weak var accountMetadata: AccountMetadata?
@ -41,8 +42,14 @@ final class FeedbinAPICaller: NSObject {
self.transport = transport self.transport = transport
} }
func cancelAll() { /// Cancels all pending requests rejects any that come in later
func suspend() {
transport.cancelAll() transport.cancelAll()
suspended = true
}
func resume() {
suspended = false
} }
func validateCredentials(completion: @escaping (Result<Credentials?, Error>) -> Void) { func validateCredentials(completion: @escaping (Result<Credentials?, Error>) -> Void) {
@ -51,6 +58,12 @@ final class FeedbinAPICaller: NSObject {
let request = URLRequest(url: callURL, credentials: credentials) let request = URLRequest(url: callURL, credentials: credentials)
transport.send(request: request) { result in transport.send(request: request) { result in
if self.suspended {
completion(.failure(TransportError.suspended))
return
}
switch result { switch result {
case .success: case .success:
completion(.success(self.credentials)) completion(.success(self.credentials))
@ -78,6 +91,11 @@ final class FeedbinAPICaller: NSObject {
transport.send(request: request, method: HTTPMethod.post, payload: opmlData) { result in transport.send(request: request, method: HTTPMethod.post, payload: opmlData) { result in
if self.suspended {
completion(.failure(TransportError.suspended))
return
}
switch result { switch result {
case .success(let (_, data)): case .success(let (_, data)):
@ -108,6 +126,11 @@ final class FeedbinAPICaller: NSObject {
transport.send(request: request, resultType: FeedbinImportResult.self) { result in transport.send(request: request, resultType: FeedbinImportResult.self) { result in
if self.suspended {
completion(.failure(TransportError.suspended))
return
}
switch result { switch result {
case .success(let (_, importResult)): case .success(let (_, importResult)):
completion(.success(importResult)) completion(.success(importResult))
@ -127,6 +150,11 @@ final class FeedbinAPICaller: NSObject {
transport.send(request: request, resultType: [FeedbinTag].self) { result in transport.send(request: request, resultType: [FeedbinTag].self) { result in
if self.suspended {
completion(.failure(TransportError.suspended))
return
}
switch result { switch result {
case .success(let (response, tags)): case .success(let (response, tags)):
self.storeConditionalGet(key: ConditionalGetKeys.tags, headers: response.allHeaderFields) self.storeConditionalGet(key: ConditionalGetKeys.tags, headers: response.allHeaderFields)
@ -143,7 +171,20 @@ final class FeedbinAPICaller: NSObject {
let callURL = feedbinBaseURL.appendingPathComponent("tags.json") let callURL = feedbinBaseURL.appendingPathComponent("tags.json")
let request = URLRequest(url: callURL, credentials: credentials) let request = URLRequest(url: callURL, credentials: credentials)
let payload = FeedbinRenameTag(oldName: oldName, newName: newName) let payload = FeedbinRenameTag(oldName: oldName, newName: newName)
transport.send(request: request, method: HTTPMethod.post, payload: payload, completion: completion)
transport.send(request: request, method: HTTPMethod.post, payload: payload) { result in
if self.suspended {
completion(.failure(TransportError.suspended))
return
}
switch result {
case .success:
completion(.success(()))
case .failure(let error):
completion(.failure(error))
}
}
} }
func retrieveSubscriptions(completion: @escaping (Result<[FeedbinSubscription]?, Error>) -> Void) { func retrieveSubscriptions(completion: @escaping (Result<[FeedbinSubscription]?, Error>) -> Void) {
@ -156,6 +197,11 @@ final class FeedbinAPICaller: NSObject {
transport.send(request: request, resultType: [FeedbinSubscription].self) { result in transport.send(request: request, resultType: [FeedbinSubscription].self) { result in
if self.suspended {
completion(.failure(TransportError.suspended))
return
}
switch result { switch result {
case .success(let (response, subscriptions)): case .success(let (response, subscriptions)):
self.storeConditionalGet(key: ConditionalGetKeys.subscriptions, headers: response.allHeaderFields) self.storeConditionalGet(key: ConditionalGetKeys.subscriptions, headers: response.allHeaderFields)
@ -186,6 +232,11 @@ final class FeedbinAPICaller: NSObject {
transport.send(request: request, method: HTTPMethod.post, payload: payload) { result in transport.send(request: request, method: HTTPMethod.post, payload: payload) { result in
if self.suspended {
completion(.failure(TransportError.suspended))
return
}
switch result { switch result {
case .success(let (response, data)): case .success(let (response, data)):
@ -246,13 +297,38 @@ final class FeedbinAPICaller: NSObject {
let callURL = feedbinBaseURL.appendingPathComponent("subscriptions/\(subscriptionID)/update.json") let callURL = feedbinBaseURL.appendingPathComponent("subscriptions/\(subscriptionID)/update.json")
let request = URLRequest(url: callURL, credentials: credentials) let request = URLRequest(url: callURL, credentials: credentials)
let payload = FeedbinUpdateSubscription(title: newName) let payload = FeedbinUpdateSubscription(title: newName)
transport.send(request: request, method: HTTPMethod.post, payload: payload, completion: completion)
transport.send(request: request, method: HTTPMethod.post, payload: payload) { result in
if self.suspended {
completion(.failure(TransportError.suspended))
return
}
switch result {
case .success:
completion(.success(()))
case .failure(let error):
completion(.failure(error))
}
}
} }
func deleteSubscription(subscriptionID: String, completion: @escaping (Result<Void, Error>) -> Void) { func deleteSubscription(subscriptionID: String, completion: @escaping (Result<Void, Error>) -> Void) {
let callURL = feedbinBaseURL.appendingPathComponent("subscriptions/\(subscriptionID).json") let callURL = feedbinBaseURL.appendingPathComponent("subscriptions/\(subscriptionID).json")
let request = URLRequest(url: callURL, credentials: credentials) let request = URLRequest(url: callURL, credentials: credentials)
transport.send(request: request, method: HTTPMethod.delete, completion: completion) transport.send(request: request, method: HTTPMethod.delete) { result in
if self.suspended {
completion(.failure(TransportError.suspended))
return
}
switch result {
case .success:
completion(.success(()))
case .failure(let error):
completion(.failure(error))
}
}
} }
func retrieveTaggings(completion: @escaping (Result<[FeedbinTagging]?, Error>) -> Void) { func retrieveTaggings(completion: @escaping (Result<[FeedbinTagging]?, Error>) -> Void) {
@ -262,7 +338,11 @@ final class FeedbinAPICaller: NSObject {
let request = URLRequest(url: callURL, credentials: credentials, conditionalGet: conditionalGet) let request = URLRequest(url: callURL, credentials: credentials, conditionalGet: conditionalGet)
transport.send(request: request, resultType: [FeedbinTagging].self) { result in transport.send(request: request, resultType: [FeedbinTagging].self) { result in
if self.suspended {
completion(.failure(TransportError.suspended))
return
}
switch result { switch result {
case .success(let (response, taggings)): case .success(let (response, taggings)):
self.storeConditionalGet(key: ConditionalGetKeys.taggings, headers: response.allHeaderFields) self.storeConditionalGet(key: ConditionalGetKeys.taggings, headers: response.allHeaderFields)
@ -291,6 +371,11 @@ final class FeedbinAPICaller: NSObject {
transport.send(request: request, method: HTTPMethod.post, payload:payload) { result in transport.send(request: request, method: HTTPMethod.post, payload:payload) { result in
if self.suspended {
completion(.failure(TransportError.suspended))
return
}
switch result { switch result {
case .success(let (response, _)): case .success(let (response, _)):
if let taggingLocation = response.valueForHTTPHeaderField(HTTPResponseHeader.location), if let taggingLocation = response.valueForHTTPHeaderField(HTTPResponseHeader.location),
@ -313,7 +398,19 @@ final class FeedbinAPICaller: NSObject {
let callURL = feedbinBaseURL.appendingPathComponent("taggings/\(taggingID).json") let callURL = feedbinBaseURL.appendingPathComponent("taggings/\(taggingID).json")
var request = URLRequest(url: callURL, credentials: credentials) var request = URLRequest(url: callURL, credentials: credentials)
request.addValue("application/json; charset=utf-8", forHTTPHeaderField: HTTPRequestHeader.contentType) request.addValue("application/json; charset=utf-8", forHTTPHeaderField: HTTPRequestHeader.contentType)
transport.send(request: request, method: HTTPMethod.delete, completion: completion) transport.send(request: request, method: HTTPMethod.delete) { result in
if self.suspended {
completion(.failure(TransportError.suspended))
return
}
switch result {
case .success:
completion(.success(()))
case .failure(let error):
completion(.failure(error))
}
}
} }
func retrieveEntries(articleIDs: [String], completion: @escaping (Result<([FeedbinEntry]?), Error>) -> Void) { func retrieveEntries(articleIDs: [String], completion: @escaping (Result<([FeedbinEntry]?), Error>) -> Void) {
@ -336,6 +433,11 @@ final class FeedbinAPICaller: NSObject {
transport.send(request: request, resultType: [FeedbinEntry].self) { result in transport.send(request: request, resultType: [FeedbinEntry].self) { result in
if self.suspended {
completion(.failure(TransportError.suspended))
return
}
switch result { switch result {
case .success(let (_, entries)): case .success(let (_, entries)):
completion(.success((entries))) completion(.success((entries)))
@ -363,6 +465,11 @@ final class FeedbinAPICaller: NSObject {
transport.send(request: request, resultType: [FeedbinEntry].self) { result in transport.send(request: request, resultType: [FeedbinEntry].self) { result in
if self.suspended {
completion(.failure(TransportError.suspended))
return
}
switch result { switch result {
case .success(let (response, entries)): case .success(let (response, entries)):
@ -399,6 +506,11 @@ final class FeedbinAPICaller: NSObject {
transport.send(request: request, resultType: [FeedbinEntry].self) { result in transport.send(request: request, resultType: [FeedbinEntry].self) { result in
if self.suspended {
completion(.failure(TransportError.suspended))
return
}
switch result { switch result {
case .success(let (response, entries)): case .success(let (response, entries)):
@ -427,6 +539,11 @@ final class FeedbinAPICaller: NSObject {
transport.send(request: request, resultType: [FeedbinEntry].self) { result in transport.send(request: request, resultType: [FeedbinEntry].self) { result in
if self.suspended {
completion(.failure(TransportError.suspended))
return
}
switch result { switch result {
case .success(let (response, entries)): case .success(let (response, entries)):
@ -449,6 +566,11 @@ final class FeedbinAPICaller: NSObject {
transport.send(request: request, resultType: [Int].self) { result in transport.send(request: request, resultType: [Int].self) { result in
if self.suspended {
completion(.failure(TransportError.suspended))
return
}
switch result { switch result {
case .success(let (response, unreadEntries)): case .success(let (response, unreadEntries)):
self.storeConditionalGet(key: ConditionalGetKeys.unreadEntries, headers: response.allHeaderFields) self.storeConditionalGet(key: ConditionalGetKeys.unreadEntries, headers: response.allHeaderFields)
@ -465,14 +587,38 @@ final class FeedbinAPICaller: NSObject {
let callURL = feedbinBaseURL.appendingPathComponent("unread_entries.json") let callURL = feedbinBaseURL.appendingPathComponent("unread_entries.json")
let request = URLRequest(url: callURL, credentials: credentials) let request = URLRequest(url: callURL, credentials: credentials)
let payload = FeedbinUnreadEntry(unreadEntries: entries) let payload = FeedbinUnreadEntry(unreadEntries: entries)
transport.send(request: request, method: HTTPMethod.post, payload: payload, completion: completion) transport.send(request: request, method: HTTPMethod.post, payload: payload) { result in
if self.suspended {
completion(.failure(TransportError.suspended))
return
}
switch result {
case .success:
completion(.success(()))
case .failure(let error):
completion(.failure(error))
}
}
} }
func deleteUnreadEntries(entries: [Int], completion: @escaping (Result<Void, Error>) -> Void) { func deleteUnreadEntries(entries: [Int], completion: @escaping (Result<Void, Error>) -> Void) {
let callURL = feedbinBaseURL.appendingPathComponent("unread_entries.json") let callURL = feedbinBaseURL.appendingPathComponent("unread_entries.json")
let request = URLRequest(url: callURL, credentials: credentials) let request = URLRequest(url: callURL, credentials: credentials)
let payload = FeedbinUnreadEntry(unreadEntries: entries) let payload = FeedbinUnreadEntry(unreadEntries: entries)
transport.send(request: request, method: HTTPMethod.delete, payload: payload, completion: completion) transport.send(request: request, method: HTTPMethod.delete, payload: payload) { result in
if self.suspended {
completion(.failure(TransportError.suspended))
return
}
switch result {
case .success:
completion(.success(()))
case .failure(let error):
completion(.failure(error))
}
}
} }
func retrieveStarredEntries(completion: @escaping (Result<[Int]?, Error>) -> Void) { func retrieveStarredEntries(completion: @escaping (Result<[Int]?, Error>) -> Void) {
@ -482,7 +628,11 @@ final class FeedbinAPICaller: NSObject {
let request = URLRequest(url: callURL, credentials: credentials, conditionalGet: conditionalGet) let request = URLRequest(url: callURL, credentials: credentials, conditionalGet: conditionalGet)
transport.send(request: request, resultType: [Int].self) { result in transport.send(request: request, resultType: [Int].self) { result in
if self.suspended {
completion(.failure(TransportError.suspended))
return
}
switch result { switch result {
case .success(let (response, starredEntries)): case .success(let (response, starredEntries)):
self.storeConditionalGet(key: ConditionalGetKeys.starredEntries, headers: response.allHeaderFields) self.storeConditionalGet(key: ConditionalGetKeys.starredEntries, headers: response.allHeaderFields)
@ -499,14 +649,38 @@ final class FeedbinAPICaller: NSObject {
let callURL = feedbinBaseURL.appendingPathComponent("starred_entries.json") let callURL = feedbinBaseURL.appendingPathComponent("starred_entries.json")
let request = URLRequest(url: callURL, credentials: credentials) let request = URLRequest(url: callURL, credentials: credentials)
let payload = FeedbinStarredEntry(starredEntries: entries) let payload = FeedbinStarredEntry(starredEntries: entries)
transport.send(request: request, method: HTTPMethod.post, payload: payload, completion: completion) transport.send(request: request, method: HTTPMethod.post, payload: payload) { result in
if self.suspended {
completion(.failure(TransportError.suspended))
return
}
switch result {
case .success:
completion(.success(()))
case .failure(let error):
completion(.failure(error))
}
}
} }
func deleteStarredEntries(entries: [Int], completion: @escaping (Result<Void, Error>) -> Void) { func deleteStarredEntries(entries: [Int], completion: @escaping (Result<Void, Error>) -> Void) {
let callURL = feedbinBaseURL.appendingPathComponent("starred_entries.json") let callURL = feedbinBaseURL.appendingPathComponent("starred_entries.json")
let request = URLRequest(url: callURL, credentials: credentials) let request = URLRequest(url: callURL, credentials: credentials)
let payload = FeedbinStarredEntry(starredEntries: entries) let payload = FeedbinStarredEntry(starredEntries: entries)
transport.send(request: request, method: HTTPMethod.delete, payload: payload, completion: completion) transport.send(request: request, method: HTTPMethod.delete, payload: payload) { result in
if self.suspended {
completion(.failure(TransportError.suspended))
return
}
switch result {
case .success:
completion(.success(()))
case .failure(let error):
completion(.failure(error))
}
}
} }
} }

View File

@ -73,10 +73,6 @@ final class FeedbinAccountDelegate: AccountDelegate {
var refreshProgress = DownloadProgress(numberOfTasks: 0) var refreshProgress = DownloadProgress(numberOfTasks: 0)
func cancelAll(for account: Account) {
caller.cancelAll()
}
func refreshAll(for account: Account, completion: @escaping (Result<Void, Error>) -> Void) { func refreshAll(for account: Account, completion: @escaping (Result<Void, Error>) -> Void) {
refreshProgress.addToNumberOfTasksAndRemaining(5) refreshProgress.addToNumberOfTasksAndRemaining(5)
@ -560,11 +556,13 @@ final class FeedbinAccountDelegate: AccountDelegate {
/// Suspend the sync database so that it can close its SQLite file. /// Suspend the sync database so that it can close its SQLite file.
func suspend() { func suspend() {
caller.suspend()
database.suspend() database.suspend()
} }
/// Resume the sync database let it reopen its SQLite file. /// Resume the sync database let it reopen its SQLite file.
func resume() { func resume() {
caller.resume()
database.resume() database.resume()
} }
} }

View File

@ -97,10 +97,6 @@ final class FeedlyAccountDelegate: AccountDelegate {
// MARK: Account API // MARK: Account API
func cancelAll(for account: Account) {
operationQueue.cancelAllOperations()
}
func refreshAll(for account: Account, completion: @escaping (Result<Void, Error>) -> Void) { func refreshAll(for account: Account, completion: @escaping (Result<Void, Error>) -> Void) {
assert(Thread.isMainThread) assert(Thread.isMainThread)
@ -515,6 +511,7 @@ final class FeedlyAccountDelegate: AccountDelegate {
/// Suspend the sync database so that it can close its SQLite file. /// Suspend the sync database so that it can close its SQLite file.
func suspend() { func suspend() {
operationQueue.cancelAllOperations()
database.suspend() database.suspend()
} }

View File

@ -79,10 +79,6 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
var refreshProgress = DownloadProgress(numberOfTasks: 0) var refreshProgress = DownloadProgress(numberOfTasks: 0)
func cancelAll(for account: Account) {
caller.cancelAll()
}
func refreshAll(for account: Account, completion: @escaping (Result<Void, Error>) -> Void) { func refreshAll(for account: Account, completion: @escaping (Result<Void, Error>) -> Void) {
refreshProgress.addToNumberOfTasksAndRemaining(6) refreshProgress.addToNumberOfTasksAndRemaining(6)
@ -441,6 +437,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
/// Suspend the sync database so that it can close its SQLite file. /// Suspend the sync database so that it can close its SQLite file.
func suspend() { func suspend() {
caller.cancelAll()
database.suspend() database.suspend()
} }

View File

@ -46,12 +46,6 @@ final class FetchRequestOperation {
} }
} }
// The account manager may have been suspended while we were queued up
if AccountManager.shared.isSuspended {
callCompletionIfNeeded()
return
}
if isCanceled { if isCanceled {
callCompletionIfNeeded() callCompletionIfNeeded()
return return

View File

@ -229,6 +229,7 @@ private extension AppDelegate {
} }
// MARK: Go To Background // MARK: Go To Background
private extension AppDelegate { private extension AppDelegate {
func waitForSyncTasksToFinish() { func waitForSyncTasksToFinish() {
@ -269,7 +270,7 @@ private extension AppDelegate {
func completeProcessing(_ suspend: Bool) { func completeProcessing(_ suspend: Bool) {
if suspend { if suspend {
AccountManager.shared.suspendAll() suspendApplication()
} }
UIApplication.shared.endBackgroundTask(self.waitBackgroundUpdateTask) UIApplication.shared.endBackgroundTask(self.waitBackgroundUpdateTask)
self.waitBackgroundUpdateTask = UIBackgroundTaskIdentifier.invalid self.waitBackgroundUpdateTask = UIBackgroundTaskIdentifier.invalid
@ -299,6 +300,16 @@ private extension AppDelegate {
} }
} }
func suspendApplication() {
CoalescingQueue.standard.performCallsImmediately()
for scene in UIApplication.shared.connectedScenes {
if let sceneDelegate = scene.delegate as? SceneDelegate {
sceneDelegate.suspend()
}
}
AccountManager.shared.suspendAll()
}
} }
// MARK: Background Tasks // MARK: Background Tasks
@ -342,9 +353,9 @@ private extension AppDelegate {
if AccountManager.shared.isSuspended { if AccountManager.shared.isSuspended {
AccountManager.shared.resumeAll() AccountManager.shared.resumeAll()
} }
AccountManager.shared.refreshAll(errorHandler: ErrorHandler.log) { AccountManager.shared.refreshAll(errorHandler: ErrorHandler.log) { [unowned self] in
AccountManager.shared.saveAll() AccountManager.shared.saveAll()
AccountManager.shared.suspendAll() self.suspendApplication()
os_log("Account refresh operation completed.", log: self.log, type: .info) os_log("Account refresh operation completed.", log: self.log, type: .info)
task?.setTaskCompleted(success: true) task?.setTaskCompleted(success: true)
} }

View File

@ -535,6 +535,11 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
// MARK: API // MARK: API
func suspend() {
fetchAndMergeArticlesQueue.performCallsImmediately()
fetchRequestQueue.cancelAllRequests()
}
func shadowNodesFor(section: Int) -> [Node] { func shadowNodesFor(section: Int) -> [Node] {
return shadowTable[section] return shadowTable[section]
} }

View File

@ -73,6 +73,10 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
coordinator.handle(response) coordinator.handle(response)
} }
func suspend() {
coordinator.suspend()
}
} }
private extension SceneDelegate { private extension SceneDelegate {

@ -1 +1 @@
Subproject commit b2ef3e111339ba9dd9070ed0ded8e053135559cb Subproject commit 0631696e7383368664a1fe47c58ebcd0ee2d49f8