Fix numerous concurrency warnings.

This commit is contained in:
Brent Simmons 2024-03-21 09:46:40 -07:00
parent 5bf5a067ab
commit fb0479f324
12 changed files with 97 additions and 85 deletions

View File

@ -74,7 +74,9 @@ final class CloudKitAccountDelegate: AccountDelegate {
op.completionBlock = { mainThreadOperaion in
completion()
}
mainThreadOperationQueue.add(op)
Task { @MainActor in
mainThreadOperationQueue.add(op)
}
}
func refreshAll(for account: Account, completion: @escaping (Result<Void, Error>) -> Void) {
@ -124,7 +126,9 @@ final class CloudKitAccountDelegate: AccountDelegate {
completion(.success(()))
}
}
mainThreadOperationQueue.add(op)
Task { @MainActor in
mainThreadOperationQueue.add(op)
}
}
func importOPML(for account:Account, opmlFile: URL, completion: @escaping (Result<Void, Error>) -> Void) {
@ -777,7 +781,9 @@ private extension CloudKitAccountDelegate {
completion(.success(()))
}
}
mainThreadOperationQueue.add(op)
Task { @MainActor in
mainThreadOperationQueue.add(op)
}
}

View File

@ -108,7 +108,7 @@ final class FeedlyAccountDelegate: AccountDelegate {
completion()
}
func refreshAll(for account: Account, completion: @escaping (Result<Void, Error>) -> Void) {
@MainActor func refreshAll(for account: Account, completion: @escaping (Result<Void, Error>) -> Void) {
assert(Thread.isMainThread)
guard currentSyncAllOperation == nil else {
@ -145,7 +145,7 @@ final class FeedlyAccountDelegate: AccountDelegate {
operationQueue.add(syncAllOperation)
}
func syncArticleStatus(for account: Account, completion: ((Result<Void, Error>) -> Void)? = nil) {
@MainActor func syncArticleStatus(for account: Account, completion: ((Result<Void, Error>) -> Void)? = nil) {
sendArticleStatus(for: account) { result in
switch result {
case .success:
@ -163,7 +163,7 @@ final class FeedlyAccountDelegate: AccountDelegate {
}
}
func sendArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
@MainActor func sendArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
// Ensure remote articles have the same status as they do locally.
let send = FeedlySendArticleStatusesOperation(database: database, service: caller, log: log)
send.completionBlock = { operation in
@ -181,7 +181,7 @@ final class FeedlyAccountDelegate: AccountDelegate {
///
/// - Parameter account: The account whose articles have a remote status.
/// - Parameter completion: Call on the main queue.
func refreshArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
@MainActor func refreshArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
guard let credentials = credentials else {
return completion(.success(()))
}
@ -314,8 +314,8 @@ final class FeedlyAccountDelegate: AccountDelegate {
}
}
func createFeed(for account: Account, url: String, name: String?, container: Container, validateFeed: Bool, completion: @escaping (Result<Feed, Error>) -> Void) {
@MainActor func createFeed(for account: Account, url: String, name: String?, container: Container, validateFeed: Bool, completion: @escaping (Result<Feed, Error>) -> Void) {
do {
guard let credentials = credentials else {
throw FeedlyAccountDelegateError.notLoggedIn
@ -374,8 +374,8 @@ final class FeedlyAccountDelegate: AccountDelegate {
feed.editedName = name
}
func addFeed(for account: Account, with feed: Feed, to container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
@MainActor func addFeed(for account: Account, with feed: Feed, to container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
do {
guard let credentials = credentials else {
throw FeedlyAccountDelegateError.notLoggedIn
@ -425,7 +425,7 @@ final class FeedlyAccountDelegate: AccountDelegate {
folder.removeFeed(feed)
}
func moveFeed(for account: Account, with feed: Feed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void) {
@MainActor func moveFeed(for account: Account, with feed: Feed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void) {
guard let from = from as? Folder, let to = to as? Folder else {
return DispatchQueue.main.async {
completion(.failure(FeedlyAccountDelegateError.addFeedChooseFolder))
@ -458,7 +458,7 @@ final class FeedlyAccountDelegate: AccountDelegate {
to.addFeed(feed)
}
func restoreFeed(for account: Account, feed: Feed, container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
@MainActor func restoreFeed(for account: Account, feed: Feed, container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
if let existingFeed = account.existingFeed(withURL: feed.url) {
account.addFeed(existingFeed, to: container) { result in
switch result {
@ -480,7 +480,7 @@ final class FeedlyAccountDelegate: AccountDelegate {
}
}
func restoreFolder(for account: Account, folder: Folder, completion: @escaping (Result<Void, Error>) -> Void) {
@MainActor func restoreFolder(for account: Account, folder: Folder, completion: @escaping (Result<Void, Error>) -> Void) {
let group = DispatchGroup()
for feed in folder.topLevelFeeds {
@ -516,10 +516,12 @@ final class FeedlyAccountDelegate: AccountDelegate {
self.database.insertStatuses(syncStatuses) { _ in
self.database.selectPendingCount { result in
if let count = try? result.get(), count > 100 {
self.sendArticleStatus(for: account) { _ in }
MainActor.assumeIsolated {
if let count = try? result.get(), count > 100 {
self.sendArticleStatus(for: account) { _ in }
}
completion(.success(()))
}
completion(.success(()))
}
}
case .failure(let error):
@ -533,7 +535,7 @@ final class FeedlyAccountDelegate: AccountDelegate {
credentials = try? account.retrieveCredentials(type: .oauthAccessToken)
}
func accountWillBeDeleted(_ account: Account) {
@MainActor func accountWillBeDeleted(_ account: Account) {
let logout = FeedlyLogoutOperation(account: account, service: caller, log: log)
// Dispatch on the shared queue because the lifetime of the account delegate is uncertain.
MainThreadOperationQueue.shared.add(logout)
@ -598,6 +600,8 @@ extension FeedlyAccountDelegate: FeedlyAPICallerDelegate {
completionHandler(refreshAccessTokenDelegate.didReauthorize && !operation.isCanceled)
}
MainThreadOperationQueue.shared.add(refreshAccessToken)
Task { @MainActor in
MainThreadOperationQueue.shared.add(refreshAccessToken)
}
}
}

View File

@ -17,7 +17,7 @@ class FeedlyAddExistingFeedOperation: FeedlyOperation, FeedlyOperationDelegate,
private let operationQueue = MainThreadOperationQueue()
var addCompletionHandler: ((Result<Void, Error>) -> ())?
init(account: Account, credentials: Credentials, resource: FeedlyFeedResourceId, service: FeedlyAddFeedToCollectionService, container: Container, progress: DownloadProgress, log: OSLog, customFeedName: String? = nil) throws {
@MainActor init(account: Account, credentials: Credentials, resource: FeedlyFeedResourceId, service: FeedlyAddFeedToCollectionService, container: Container, progress: DownloadProgress, log: OSLog, customFeedName: String? = nil) throws {
let validator = FeedlyFeedContainerValidator(container: container)
let (folder, collectionId) = try validator.getValidContainer()

View File

@ -30,8 +30,8 @@ class FeedlyAddNewFeedOperation: FeedlyOperation, FeedlyOperationDelegate, Feedl
private var feedResourceId: FeedlyFeedResourceId?
var addCompletionHandler: ((Result<Feed, Error>) -> ())?
init(account: Account, credentials: Credentials, url: String, feedName: String?, searchService: FeedlySearchService, addToCollectionService: FeedlyAddFeedToCollectionService, syncUnreadIdsService: FeedlyGetStreamIdsService, getStreamContentsService: FeedlyGetStreamContentsService, database: SyncDatabase, container: Container, progress: DownloadProgress, log: OSLog) throws {
@MainActor init(account: Account, credentials: Credentials, url: String, feedName: String?, searchService: FeedlySearchService, addToCollectionService: FeedlyAddFeedToCollectionService, syncUnreadIdsService: FeedlyGetStreamIdsService, getStreamContentsService: FeedlyGetStreamContentsService, database: SyncDatabase, container: Container, progress: DownloadProgress, log: OSLog) throws {
let validator = FeedlyFeedContainerValidator(container: container)
(self.folder, self.collectionId) = try validator.getValidContainer()
@ -75,7 +75,7 @@ class FeedlyAddNewFeedOperation: FeedlyOperation, FeedlyOperationDelegate, Feedl
super.didFinish(with: error)
}
func feedlySearchOperation(_ operation: FeedlySearchOperation, didGet response: FeedlyFeedsSearchResponse) {
@MainActor func feedlySearchOperation(_ operation: FeedlySearchOperation, didGet response: FeedlyFeedsSearchResponse) {
guard !isCanceled else {
return
}

View File

@ -21,7 +21,7 @@ class FeedlyDownloadArticlesOperation: FeedlyOperation {
private let operationQueue = MainThreadOperationQueue()
private let finishOperation: FeedlyCheckpointOperation
init(account: Account, missingArticleEntryIdProvider: FeedlyEntryIdentifierProviding, updatedArticleEntryIdProvider: FeedlyEntryIdentifierProviding, getEntriesService: FeedlyGetEntriesService, log: OSLog) {
@MainActor init(account: Account, missingArticleEntryIdProvider: FeedlyEntryIdentifierProviding, updatedArticleEntryIdProvider: FeedlyEntryIdentifierProviding, getEntriesService: FeedlyGetEntriesService, log: OSLog) {
self.account = account
self.operationQueue.suspend()
self.missingArticleEntryIdProvider = missingArticleEntryIdProvider
@ -43,27 +43,29 @@ class FeedlyDownloadArticlesOperation: FeedlyOperation {
let feedlyAPILimitBatchSize = 1000
for articleIds in Array(articleIds).chunked(into: feedlyAPILimitBatchSize) {
let provider = FeedlyEntryIdentifierProvider(entryIds: Set(articleIds))
let getEntries = FeedlyGetEntriesOperation(account: account, service: getEntriesService, provider: provider, log: log)
getEntries.delegate = self
self.operationQueue.add(getEntries)
let organiseByFeed = FeedlyOrganiseParsedItemsByFeedOperation(account: account,
parsedItemProvider: getEntries,
log: log)
organiseByFeed.delegate = self
organiseByFeed.addDependency(getEntries)
self.operationQueue.add(organiseByFeed)
let updateAccount = FeedlyUpdateAccountFeedsWithItemsOperation(account: account,
organisedItemsProvider: organiseByFeed,
log: log)
updateAccount.delegate = self
updateAccount.addDependency(organiseByFeed)
self.operationQueue.add(updateAccount)
Task { @MainActor in
let provider = FeedlyEntryIdentifierProvider(entryIds: Set(articleIds))
let getEntries = FeedlyGetEntriesOperation(account: account, service: getEntriesService, provider: provider, log: log)
getEntries.delegate = self
self.operationQueue.add(getEntries)
finishOperation.addDependency(updateAccount)
let organiseByFeed = FeedlyOrganiseParsedItemsByFeedOperation(account: account,
parsedItemProvider: getEntries,
log: log)
organiseByFeed.delegate = self
organiseByFeed.addDependency(getEntries)
self.operationQueue.add(organiseByFeed)
let updateAccount = FeedlyUpdateAccountFeedsWithItemsOperation(account: account,
organisedItemsProvider: organiseByFeed,
log: log)
updateAccount.delegate = self
updateAccount.addDependency(organiseByFeed)
self.operationQueue.add(updateAccount)
finishOperation.addDependency(updateAccount)
}
}
operationQueue.resume()

View File

@ -34,7 +34,7 @@ final class FeedlySyncAllOperation: FeedlyOperation {
///
/// Download articles for statuses at the union of those statuses without its corresponding article and those included in 3 (changed since last successful sync).
///
init(account: Account, feedlyUserId: String, lastSuccessfulFetchStartDate: Date?, markArticlesService: FeedlyMarkArticlesService, getUnreadService: FeedlyGetStreamIdsService, getCollectionsService: FeedlyGetCollectionsService, getStreamContentsService: FeedlyGetStreamContentsService, getStarredService: FeedlyGetStreamIdsService, getStreamIdsService: FeedlyGetStreamIdsService, getEntriesService: FeedlyGetEntriesService, database: SyncDatabase, downloadProgress: DownloadProgress, log: OSLog) {
@MainActor init(account: Account, feedlyUserId: String, lastSuccessfulFetchStartDate: Date?, markArticlesService: FeedlyMarkArticlesService, getUnreadService: FeedlyGetStreamIdsService, getCollectionsService: FeedlyGetCollectionsService, getStreamContentsService: FeedlyGetStreamContentsService, getStarredService: FeedlyGetStreamIdsService, getStreamIdsService: FeedlyGetStreamIdsService, getEntriesService: FeedlyGetEntriesService, database: SyncDatabase, downloadProgress: DownloadProgress, log: OSLog) {
self.syncUUID = UUID()
self.log = log
self.operationQueue.suspend()
@ -126,7 +126,7 @@ final class FeedlySyncAllOperation: FeedlyOperation {
self.operationQueue.add(finishOperation)
}
convenience init(account: Account, feedlyUserId: String, caller: FeedlyAPICaller, database: SyncDatabase, lastSuccessfulFetchStartDate: Date?, downloadProgress: DownloadProgress, log: OSLog) {
@MainActor convenience init(account: Account, feedlyUserId: String, caller: FeedlyAPICaller, database: SyncDatabase, lastSuccessfulFetchStartDate: Date?, downloadProgress: DownloadProgress, log: OSLog) {
self.init(account: account, feedlyUserId: feedlyUserId, lastSuccessfulFetchStartDate: lastSuccessfulFetchStartDate, markArticlesService: caller, getUnreadService: caller, getCollectionsService: caller, getStreamContentsService: caller, getStarredService: caller, getStreamIdsService: caller, getEntriesService: caller, database: database, downloadProgress: downloadProgress, log: log)
}

View File

@ -24,7 +24,7 @@ final class FeedlySyncStreamContentsOperation: FeedlyOperation, FeedlyOperationD
private let log: OSLog
private let finishOperation: FeedlyCheckpointOperation
init(account: Account, resource: FeedlyResourceId, service: FeedlyGetStreamContentsService, isPagingEnabled: Bool, newerThan: Date?, log: OSLog) {
@MainActor init(account: Account, resource: FeedlyResourceId, service: FeedlyGetStreamContentsService, isPagingEnabled: Bool, newerThan: Date?, log: OSLog) {
self.account = account
self.resource = resource
self.service = service
@ -41,7 +41,7 @@ final class FeedlySyncStreamContentsOperation: FeedlyOperation, FeedlyOperationD
enqueueOperations(for: nil)
}
convenience init(account: Account, credentials: Credentials, service: FeedlyGetStreamContentsService, newerThan: Date?, log: OSLog) {
@MainActor convenience init(account: Account, credentials: Credentials, service: FeedlyGetStreamContentsService, newerThan: Date?, log: OSLog) {
let all = FeedlyCategoryResourceId.Global.all(for: credentials.username)
self.init(account: account, resource: all, service: service, isPagingEnabled: true, newerThan: newerThan, log: log)
}
@ -56,7 +56,7 @@ final class FeedlySyncStreamContentsOperation: FeedlyOperation, FeedlyOperationD
super.didCancel()
}
func enqueueOperations(for continuation: String?) {
@MainActor func enqueueOperations(for continuation: String?) {
os_log(.debug, log: log, "Requesting page for %{public}@", resource.id)
let operations = pageOperations(for: continuation)
operationQueue.addOperations(operations)
@ -89,7 +89,7 @@ final class FeedlySyncStreamContentsOperation: FeedlyOperation, FeedlyOperationD
return [getPage, organiseByFeed, updateAccount]
}
func feedlyGetStreamContentsOperation(_ operation: FeedlyGetStreamContentsOperation, didGetContentsOf stream: FeedlyStream) {
@MainActor func feedlyGetStreamContentsOperation(_ operation: FeedlyGetStreamContentsOperation, didGetContentsOf stream: FeedlyStream) {
guard !isCanceled else {
os_log(.debug, log: log, "Cancelled requesting page for %{public}@", resource.id)
return

View File

@ -10,7 +10,7 @@
import Foundation
import CloudKit
public class CloudKitError: LocalizedError {
public final class CloudKitError: LocalizedError {
public let error: Error

View File

@ -27,13 +27,13 @@ public enum CloudKitZoneError: LocalizedError {
}
}
public protocol CloudKitZoneDelegate: class {
public protocol CloudKitZoneDelegate: AnyObject {
func cloudKitDidModify(changed: [CKRecord], deleted: [CloudKitRecordKey], completion: @escaping (Result<Void, Error>) -> Void);
}
public typealias CloudKitRecordKey = (recordType: CKRecord.RecordType, recordID: CKRecord.ID)
public protocol CloudKitZone: class {
public protocol CloudKitZone: AnyObject {
static var qualityOfService: QualityOfService { get }

View File

@ -15,7 +15,7 @@ import Foundation
/// When its canceled, it should do its best to stop
/// doing whatever its doing. However, it should not
/// leave data in an inconsistent state.
public protocol MainThreadOperation: class {
public protocol MainThreadOperation: AnyObject {
// These three properties are set by MainThreadOperationQueue. Dont set them.
var isCanceled: Bool { get set } // Check this at appropriate times in case the operation has been canceled.

View File

@ -8,7 +8,7 @@
import Foundation
public protocol MainThreadOperationDelegate: class {
public protocol MainThreadOperationDelegate: AnyObject {
func operationDidComplete(_ operation: MainThreadOperation)
func cancelOperation(_ operation: MainThreadOperation)
func make(_ childOperation: MainThreadOperation, dependOn parentOperation: MainThreadOperation)
@ -26,14 +26,14 @@ public protocol MainThreadOperationDelegate: class {
public final class MainThreadOperationQueue {
/// Use the shared queue when you dont need to create a separate queue.
public static let shared: MainThreadOperationQueue = {
@MainActor public static let shared: MainThreadOperationQueue = {
MainThreadOperationQueue()
}()
private var operations = [Int: MainThreadOperation]()
private var pendingOperationIDs = [Int]()
private var currentOperationID: Int?
private static var incrementingID = 0
@MainActor private static var incrementingID = 0
private var isSuspended = false
private let dependencies = MainThreadOperationDependencies()
@ -51,7 +51,7 @@ public final class MainThreadOperationQueue {
}
/// Add an operation to the queue.
public func add(_ operation: MainThreadOperation) {
@MainActor public func add(_ operation: MainThreadOperation) {
precondition(Thread.isMainThread)
operation.operationDelegate = self
let operationID = ensureOperationID(operation)
@ -67,12 +67,12 @@ public final class MainThreadOperationQueue {
/// Add multiple operations to the queue.
/// This has the same effect as calling addOperation one-by-one.
public func addOperations(_ operations: [MainThreadOperation]) {
@MainActor public func addOperations(_ operations: [MainThreadOperation]) {
operations.forEach{ add($0) }
}
/// Add a dependency. Do this *before* calling addOperation, since addOperation might run the operation right away.
public func make(_ childOperation: MainThreadOperation, dependOn parentOperation: MainThreadOperation) {
@MainActor public func make(_ childOperation: MainThreadOperation, dependOn parentOperation: MainThreadOperation) {
precondition(Thread.isMainThread)
let childOperationID = ensureOperationID(childOperation)
let parentOperationID = ensureOperationID(parentOperation)
@ -91,7 +91,7 @@ public final class MainThreadOperationQueue {
/// Cancel some operations. If any of them have dependent operations,
/// those operations will be canceled also.
public func cancelOperations(_ operations: [MainThreadOperation]) {
@MainActor public func cancelOperations(_ operations: [MainThreadOperation]) {
precondition(Thread.isMainThread)
let operationIDsToCancel = operations.map{ ensureOperationID($0) }
assert(allOperationIDsArePendingOrCurrent(operationIDsToCancel))
@ -106,7 +106,7 @@ public final class MainThreadOperationQueue {
///
/// This will cancel the current operation, not just pending operations,
/// if it has the specified name.
public func cancelOperations(named name: String) {
@MainActor public func cancelOperations(named name: String) {
precondition(Thread.isMainThread)
guard let operationsToCancel = pendingAndCurrentOperations(named: name) else {
return
@ -137,7 +137,7 @@ extension MainThreadOperationQueue: MainThreadOperationDelegate {
operationDidFinish(operation)
}
public func cancelOperation(_ operation: MainThreadOperation) {
@MainActor public func cancelOperation(_ operation: MainThreadOperation) {
cancelOperations([operation])
}
}
@ -227,13 +227,13 @@ private extension MainThreadOperationQueue {
return !operation.isCanceled && !dependencies.operationIDIsBlockedByDependency(operation.id!)
}
func createOperationID() -> Int {
@MainActor func createOperationID() -> Int {
precondition(Thread.isMainThread)
Self.incrementingID += 1
return Self.incrementingID
}
func ensureOperationID(_ operation: MainThreadOperation) -> Int {
@MainActor func ensureOperationID(_ operation: MainThreadOperation) -> Int {
if let operationID = operation.id {
return operationID
}

View File

@ -8,19 +8,19 @@
import Foundation
public protocol UndoableCommand: class {
public protocol UndoableCommand: AnyObject {
var undoActionName: String { get }
var redoActionName: String { get }
var undoManager: UndoManager { get }
@MainActor var undoActionName: String { get }
@MainActor var redoActionName: String { get }
@MainActor var undoManager: UndoManager { get }
func perform() // must call registerUndo()
func undo() // must call registerRedo()
@MainActor func perform() // must call registerUndo()
@MainActor func undo() // must call registerRedo()
}
extension UndoableCommand {
public func registerUndo() {
@MainActor public func registerUndo() {
undoManager.setActionName(undoActionName)
undoManager.registerUndo(withTarget: self) { (target) in
@ -28,7 +28,7 @@ extension UndoableCommand {
}
}
public func registerRedo() {
@MainActor public func registerRedo() {
undoManager.setActionName(redoActionName)
undoManager.registerUndo(withTarget: self) { (target) in
@ -39,30 +39,30 @@ extension UndoableCommand {
// Useful for view controllers.
public protocol UndoableCommandRunner: class {
public protocol UndoableCommandRunner: AnyObject {
var undoableCommands: [UndoableCommand] { get set }
var undoManager: UndoManager? { get }
func runCommand(_ undoableCommand: UndoableCommand)
func clearUndoableCommands()
@MainActor var undoableCommands: [UndoableCommand] { get set }
@MainActor var undoManager: UndoManager? { get }
@MainActor func runCommand(_ undoableCommand: UndoableCommand)
@MainActor func clearUndoableCommands()
}
public extension UndoableCommandRunner {
func runCommand(_ undoableCommand: UndoableCommand) {
@MainActor func runCommand(_ undoableCommand: UndoableCommand) {
pushUndoableCommand(undoableCommand)
undoableCommand.perform()
}
func pushUndoableCommand(_ undoableCommand: UndoableCommand) {
@MainActor func pushUndoableCommand(_ undoableCommand: UndoableCommand) {
undoableCommands += [undoableCommand]
}
func clearUndoableCommands() {
@MainActor func clearUndoableCommands() {
// Useful, for example, when timeline is reloaded and the list of articles changes.
// Otherwise things like Redo Mark Read are ambiguous.
// (Do they apply to the previous articles or to the current articles?)