Make MainThreadOperationQueue MainActor.

This commit is contained in:
Brent Simmons 2024-03-24 12:05:39 -07:00
parent e867487031
commit ae4dcc0b27
16 changed files with 165 additions and 144 deletions

View File

@ -42,7 +42,7 @@ final class CloudKitAccountDelegate: AccountDelegate {
private let accountZone: CloudKitAccountZone private let accountZone: CloudKitAccountZone
private let articlesZone: CloudKitArticlesZone private let articlesZone: CloudKitArticlesZone
private let mainThreadOperationQueue = MainThreadOperationQueue() @MainActor private let mainThreadOperationQueue = MainThreadOperationQueue()
private lazy var refresher: LocalAccountRefresher = { private lazy var refresher: LocalAccountRefresher = {
let refresher = LocalAccountRefresher() let refresher = LocalAccountRefresher()

View File

@ -27,7 +27,7 @@ class CloudKitReceiveStatusOperation: MainThreadOperation {
self.articlesZone = articlesZone self.articlesZone = articlesZone
} }
func run() { @MainActor func run() {
guard let articlesZone = articlesZone else { guard let articlesZone = articlesZone else {
self.operationDelegate?.operationDidComplete(self) self.operationDelegate?.operationDidComplete(self)
return return

View File

@ -31,7 +31,7 @@ class CloudKitRemoteNotificationOperation: MainThreadOperation {
self.userInfo = userInfo self.userInfo = userInfo
} }
func run() { @MainActor func run() {
guard let accountZone = accountZone, let articlesZone = articlesZone else { guard let accountZone = accountZone, let articlesZone = articlesZone else {
self.operationDelegate?.operationDidComplete(self) self.operationDelegate?.operationDidComplete(self)
return return

View File

@ -41,12 +41,13 @@ class CloudKitSendStatusOperation: MainThreadOperation {
self.database = database self.database = database
} }
func run() { @MainActor func run() {
os_log(.debug, log: log, "Sending article statuses...") os_log(.debug, log: log, "Sending article statuses...")
if showProgress { if showProgress {
database.selectPendingCount() { result in database.selectPendingCount() { result in
MainActor.assumeIsolated {
switch result { switch result {
case .success(let count): case .success(let count):
let ticks = count / self.blockSize let ticks = count / self.blockSize
@ -57,6 +58,7 @@ class CloudKitSendStatusOperation: MainThreadOperation {
self.operationDelegate?.cancelOperation(self) self.operationDelegate?.cancelOperation(self)
} }
} }
}
} else { } else {
@ -72,10 +74,12 @@ private extension CloudKitSendStatusOperation {
func selectForProcessing() { func selectForProcessing() {
database.selectForProcessing(limit: blockSize) { result in database.selectForProcessing(limit: blockSize) { result in
MainActor.assumeIsolated {
switch result { switch result {
case .success(let syncStatuses): case .success(let syncStatuses):
func stopProcessing() { @MainActor func stopProcessing() {
if self.showProgress { if self.showProgress {
self.refreshProgress?.completeTask() self.refreshProgress?.completeTask()
} }
@ -102,6 +106,7 @@ private extension CloudKitSendStatusOperation {
} }
} }
} }
}
func processStatuses(_ syncStatuses: [SyncStatus], completion: @escaping (Bool) -> Void) { func processStatuses(_ syncStatuses: [SyncStatus], completion: @escaping (Bool) -> Void) {
guard let account = account, let articlesZone = articlesZone else { guard let account = account, let articlesZone = articlesZone else {

View File

@ -65,7 +65,7 @@ final class FeedlyAccountDelegate: AccountDelegate {
private let database: SyncDatabase private let database: SyncDatabase
private weak var currentSyncAllOperation: MainThreadOperation? private weak var currentSyncAllOperation: MainThreadOperation?
private let operationQueue = MainThreadOperationQueue() @MainActor private let operationQueue = MainThreadOperationQueue()
init(dataFolder: String, transport: Transport?, api: FeedlyAPICaller.API, secretsProvider: SecretsProvider) { init(dataFolder: String, transport: Transport?, api: FeedlyAPICaller.API, secretsProvider: SecretsProvider) {
// Many operations have their own operation queues, such as the sync all operation. // Many operations have their own operation queues, such as the sync all operation.
@ -550,9 +550,11 @@ final class FeedlyAccountDelegate: AccountDelegate {
/// Suspend all network activity /// Suspend all network activity
func suspendNetwork() { func suspendNetwork() {
MainActor.assumeIsolated {
caller.suspend() caller.suspend()
operationQueue.cancelAllOperations() operationQueue.cancelAllOperations()
} }
}
/// Suspend the SQLLite databases /// Suspend the SQLLite databases
func suspendDatabase() { func suspendDatabase() {
@ -572,7 +574,7 @@ final class FeedlyAccountDelegate: AccountDelegate {
extension FeedlyAccountDelegate: FeedlyAPICallerDelegate { extension FeedlyAccountDelegate: FeedlyAPICallerDelegate {
func reauthorizeFeedlyAPICaller(_ caller: FeedlyAPICaller, completionHandler: @escaping (Bool) -> ()) { @MainActor func reauthorizeFeedlyAPICaller(_ caller: FeedlyAPICaller, completionHandler: @escaping (Bool) -> ()) {
guard let account = initializedAccount else { guard let account = initializedAccount else {
completionHandler(false) completionHandler(false)
return return
@ -600,8 +602,6 @@ extension FeedlyAccountDelegate: FeedlyAPICallerDelegate {
completionHandler(refreshAccessTokenDelegate.didReauthorize && !operation.isCanceled) completionHandler(refreshAccessTokenDelegate.didReauthorize && !operation.isCanceled)
} }
Task { @MainActor in
MainThreadOperationQueue.shared.add(refreshAccessToken) MainThreadOperationQueue.shared.add(refreshAccessToken)
} }
} }
}

View File

@ -51,7 +51,7 @@ public enum OAuthAccountAuthorizationOperationError: LocalizedError {
self.oauthClient = Account.oauthAuthorizationClient(for: accountType, secretsProvider: secretsProvider) self.oauthClient = Account.oauthAuthorizationClient(for: accountType, secretsProvider: secretsProvider)
} }
public func run() { @MainActor public func run() {
assert(presentationAnchor != nil, "\(self) outlived presentation anchor.") assert(presentationAnchor != nil, "\(self) outlived presentation anchor.")
let request = Account.oauthAuthorizationCodeGrantRequest(for: accountType, secretsProvider: secretsProvider) let request = Account.oauthAuthorizationCodeGrantRequest(for: accountType, secretsProvider: secretsProvider)
@ -101,6 +101,7 @@ public enum OAuthAccountAuthorizationOperationError: LocalizedError {
} }
private func didEndAuthentication(url: URL?, error: Error?) { private func didEndAuthentication(url: URL?, error: Error?) {
MainActor.assumeIsolated {
guard !isCanceled else { guard !isCanceled else {
didFinish() didFinish()
return return
@ -125,6 +126,7 @@ public enum OAuthAccountAuthorizationOperationError: LocalizedError {
didFinish(error) didFinish(error)
} }
} }
}
public func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor { public func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
guard let anchor = presentationAnchor else { guard let anchor = presentationAnchor else {
@ -174,12 +176,12 @@ public enum OAuthAccountAuthorizationOperationError: LocalizedError {
// MARK: Managing Operation State // MARK: Managing Operation State
private func didFinish() { @MainActor private func didFinish() {
assert(Thread.isMainThread) assert(Thread.isMainThread)
operationDelegate?.operationDidComplete(self) operationDelegate?.operationDidComplete(self)
} }
private func didFinish(_ error: Error) { @MainActor private func didFinish(_ error: Error) {
assert(Thread.isMainThread) assert(Thread.isMainThread)
delegate?.oauthAccountAuthorizationOperation(self, didFailWith: error) delegate?.oauthAccountAuthorizationOperation(self, didFailWith: error)
didFinish() didFinish()

View File

@ -12,7 +12,7 @@ import RSWeb
import Secrets import Secrets
import Core import Core
class FeedlyAddExistingFeedOperation: FeedlyOperation, FeedlyOperationDelegate, FeedlyCheckpointOperationDelegate { @MainActor final class FeedlyAddExistingFeedOperation: FeedlyOperation, FeedlyOperationDelegate, FeedlyCheckpointOperationDelegate {
private let operationQueue = MainThreadOperationQueue() private let operationQueue = MainThreadOperationQueue()
var addCompletionHandler: ((Result<Void, Error>) -> ())? var addCompletionHandler: ((Result<Void, Error>) -> ())?

View File

@ -23,6 +23,7 @@ final class FeedlyFetchIdsForMissingArticlesOperation: FeedlyOperation, FeedlyEn
override func run() { override func run() {
account.fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate { result in account.fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate { result in
MainActor.assumeIsolated {
switch result { switch result {
case .success(let articleIds): case .success(let articleIds):
self.entryIds.formUnion(articleIds) self.entryIds.formUnion(articleIds)
@ -34,3 +35,4 @@ final class FeedlyFetchIdsForMissingArticlesOperation: FeedlyOperation, FeedlyEn
} }
} }
} }
}

View File

@ -78,6 +78,7 @@ final class FeedlyIngestStarredArticleIdsOperation: FeedlyOperation {
} }
database.selectPendingStarredStatusArticleIDs { result in database.selectPendingStarredStatusArticleIDs { result in
MainActor.assumeIsolated {
switch result { switch result {
case .success(let pendingArticleIds): case .success(let pendingArticleIds):
self.remoteEntryIds.subtract(pendingArticleIds) self.remoteEntryIds.subtract(pendingArticleIds)
@ -89,6 +90,7 @@ final class FeedlyIngestStarredArticleIdsOperation: FeedlyOperation {
} }
} }
} }
}
private func updateStarredStatuses() { private func updateStarredStatuses() {
guard !isCanceled else { guard !isCanceled else {
@ -97,6 +99,7 @@ final class FeedlyIngestStarredArticleIdsOperation: FeedlyOperation {
} }
account.fetchStarredArticleIDs { result in account.fetchStarredArticleIDs { result in
MainActor.assumeIsolated {
switch result { switch result {
case .success(let localStarredArticleIDs): case .success(let localStarredArticleIDs):
self.processStarredArticleIDs(localStarredArticleIDs) self.processStarredArticleIDs(localStarredArticleIDs)
@ -106,6 +109,7 @@ final class FeedlyIngestStarredArticleIdsOperation: FeedlyOperation {
} }
} }
} }
}
func processStarredArticleIDs(_ localStarredArticleIDs: Set<String>) { func processStarredArticleIDs(_ localStarredArticleIDs: Set<String>) {
guard !isCanceled else { guard !isCanceled else {

View File

@ -53,6 +53,7 @@ class FeedlyIngestStreamArticleIdsOperation: FeedlyOperation {
case .success(let streamIds): case .success(let streamIds):
account.createStatusesIfNeeded(articleIDs: Set(streamIds.ids)) { databaseError in account.createStatusesIfNeeded(articleIDs: Set(streamIds.ids)) { databaseError in
MainActor.assumeIsolated {
if let error = databaseError { if let error = databaseError {
self.didFinish(with: error) self.didFinish(with: error)
return return
@ -66,6 +67,8 @@ class FeedlyIngestStreamArticleIdsOperation: FeedlyOperation {
self.getStreamIds(continuation) self.getStreamIds(continuation)
} }
}
case .failure(let error): case .failure(let error):
didFinish(with: error) didFinish(with: error)
} }

View File

@ -79,6 +79,7 @@ final class FeedlyIngestUnreadArticleIdsOperation: FeedlyOperation {
} }
database.selectPendingReadStatusArticleIDs { result in database.selectPendingReadStatusArticleIDs { result in
MainActor.assumeIsolated {
switch result { switch result {
case .success(let pendingArticleIds): case .success(let pendingArticleIds):
self.remoteEntryIds.subtract(pendingArticleIds) self.remoteEntryIds.subtract(pendingArticleIds)
@ -90,6 +91,7 @@ final class FeedlyIngestUnreadArticleIdsOperation: FeedlyOperation {
} }
} }
} }
}
private func updateUnreadStatuses() { private func updateUnreadStatuses() {
guard !isCanceled else { guard !isCanceled else {
@ -98,6 +100,7 @@ final class FeedlyIngestUnreadArticleIdsOperation: FeedlyOperation {
} }
account.fetchUnreadArticleIDs { result in account.fetchUnreadArticleIDs { result in
MainActor.assumeIsolated {
switch result { switch result {
case .success(let localUnreadArticleIDs): case .success(let localUnreadArticleIDs):
self.processUnreadArticleIDs(localUnreadArticleIDs) self.processUnreadArticleIDs(localUnreadArticleIDs)
@ -107,6 +110,7 @@ final class FeedlyIngestUnreadArticleIdsOperation: FeedlyOperation {
} }
} }
} }
}
private func processUnreadArticleIDs(_ localUnreadArticleIDs: Set<String>) { private func processUnreadArticleIDs(_ localUnreadArticleIDs: Set<String>) {
guard !isCanceled else { guard !isCanceled else {

View File

@ -18,7 +18,7 @@ protocol FeedlyOperationDelegate: AnyObject {
/// ///
/// Normally we dont do inheritance but in this case /// Normally we dont do inheritance but in this case
/// its the best option. /// its the best option.
class FeedlyOperation: MainThreadOperation { @MainActor class FeedlyOperation: MainThreadOperation {
weak var delegate: FeedlyOperationDelegate? weak var delegate: FeedlyOperationDelegate?
var downloadProgress: DownloadProgress? { var downloadProgress: DownloadProgress? {

View File

@ -29,6 +29,7 @@ final class FeedlySendArticleStatusesOperation: FeedlyOperation {
os_log(.debug, log: log, "Sending article statuses...") os_log(.debug, log: log, "Sending article statuses...")
database.selectForProcessing { result in database.selectForProcessing { result in
MainActor.assumeIsolated {
if self.isCanceled { if self.isCanceled {
self.didFinish() self.didFinish()
return return
@ -43,6 +44,7 @@ final class FeedlySendArticleStatusesOperation: FeedlyOperation {
} }
} }
} }
}
private extension FeedlySendArticleStatusesOperation { private extension FeedlySendArticleStatusesOperation {

View File

@ -28,6 +28,7 @@ final class FeedlyUpdateAccountFeedsWithItemsOperation: FeedlyOperation {
let feedIDsAndItems = organisedItemsProvider.parsedItemsKeyedByFeedId let feedIDsAndItems = organisedItemsProvider.parsedItemsKeyedByFeedId
account.update(feedIDsAndItems: feedIDsAndItems, defaultRead: true) { databaseError in account.update(feedIDsAndItems: feedIDsAndItems, defaultRead: true) { databaseError in
MainActor.assumeIsolated {
if let error = databaseError { if let error = databaseError {
self.didFinish(with: error) self.didFinish(with: error)
return return
@ -38,3 +39,4 @@ final class FeedlyUpdateAccountFeedsWithItemsOperation: FeedlyOperation {
} }
} }
} }
}

View File

@ -75,15 +75,15 @@ public protocol MainThreadOperation: AnyObject {
public extension MainThreadOperation { public extension MainThreadOperation {
func cancel() { @MainActor func cancel() {
operationDelegate?.cancelOperation(self) operationDelegate?.cancelOperation(self)
} }
func addDependency(_ parentOperation: MainThreadOperation) { @MainActor func addDependency(_ parentOperation: MainThreadOperation) {
operationDelegate?.make(self, dependOn: parentOperation) operationDelegate?.make(self, dependOn: parentOperation)
} }
func informOperationDelegateOfCompletion() { @MainActor func informOperationDelegateOfCompletion() {
guard !isCanceled else { guard !isCanceled else {
return return
} }

View File

@ -9,9 +9,10 @@
import Foundation import Foundation
public protocol MainThreadOperationDelegate: AnyObject { public protocol MainThreadOperationDelegate: AnyObject {
func operationDidComplete(_ operation: MainThreadOperation)
func cancelOperation(_ operation: MainThreadOperation) @MainActor func operationDidComplete(_ operation: MainThreadOperation)
func make(_ childOperation: MainThreadOperation, dependOn parentOperation: MainThreadOperation) @MainActor func cancelOperation(_ operation: MainThreadOperation)
@MainActor func make(_ childOperation: MainThreadOperation, dependOn parentOperation: MainThreadOperation)
} }
/// Manage a queue of MainThreadOperation tasks. /// Manage a queue of MainThreadOperation tasks.
@ -23,7 +24,7 @@ public protocol MainThreadOperationDelegate: AnyObject {
/// Use this only on the main thread. /// Use this only on the main thread.
/// The operation can be suspended and resumed. /// The operation can be suspended and resumed.
/// It is *not* suspended on creation. /// It is *not* suspended on creation.
public final class MainThreadOperationQueue { @MainActor public final class MainThreadOperationQueue {
/// Use the shared queue when you dont need to create a separate queue. /// Use the shared queue when you dont need to create a separate queue.
@MainActor public static let shared: MainThreadOperationQueue = { @MainActor public static let shared: MainThreadOperationQueue = {
@ -46,10 +47,6 @@ public final class MainThreadOperationQueue {
// Silence compiler complaint about init not being public. // Silence compiler complaint about init not being public.
} }
deinit {
cancelAllOperations()
}
/// Add an operation to the queue. /// Add an operation to the queue.
@MainActor public func add(_ operation: MainThreadOperation) { @MainActor public func add(_ operation: MainThreadOperation) {
precondition(Thread.isMainThread) precondition(Thread.isMainThread)
@ -132,7 +129,7 @@ public final class MainThreadOperationQueue {
extension MainThreadOperationQueue: MainThreadOperationDelegate { extension MainThreadOperationQueue: MainThreadOperationDelegate {
public func operationDidComplete(_ operation: MainThreadOperation) { @MainActor public func operationDidComplete(_ operation: MainThreadOperation) {
precondition(Thread.isMainThread) precondition(Thread.isMainThread)
operationDidFinish(operation) operationDidFinish(operation)
} }