From 6d9d1762aa5847fb220471b8610f4be9f314ef8b Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sun, 21 Apr 2024 19:59:45 -0700 Subject: [PATCH] Fix some concurrency warnings. --- .../Sources/CloudKitExtras/CloudKitZone.swift | 12 +- .../CloudKitSync/CloudKitArticlesZone.swift | 138 +++++++++--------- 2 files changed, 78 insertions(+), 72 deletions(-) diff --git a/CloudKitExtras/Sources/CloudKitExtras/CloudKitZone.swift b/CloudKitExtras/Sources/CloudKitExtras/CloudKitZone.swift index bf5c13e2e..005dbb13b 100644 --- a/CloudKitExtras/Sources/CloudKitExtras/CloudKitZone.swift +++ b/CloudKitExtras/Sources/CloudKitExtras/CloudKitZone.swift @@ -36,11 +36,11 @@ public typealias CloudKitRecordKey = (recordType: CKRecord.RecordType, recordID: public protocol CloudKitZone: AnyObject { - @MainActor static var qualityOfService: QualityOfService { get } + static var qualityOfService: QualityOfService { get } var zoneID: CKRecordZone.ID { get } - @MainActor var log: OSLog { get } + var log: OSLog { get } @MainActor var container: CKContainer? { get } @MainActor var database: CKDatabase? { get } @@ -64,7 +64,7 @@ public protocol CloudKitZone: AnyObject { // My observation has been that QoS is treated differently for CloudKit operations on macOS vs iOS. // .userInitiated is too aggressive on iOS and can lead the UI slowing down and appearing to block. // .default (or lower) on macOS will sometimes hang for extended periods of time and appear to hang. - static var qualityOfService: QualityOfService { + nonisolated static var qualityOfService: QualityOfService { #if os(macOS) || targetEnvironment(macCatalyst) return .userInitiated #else @@ -121,7 +121,7 @@ public protocol CloudKitZone: AnyObject { } } - func retryIfPossible(after: Double, block: @escaping @MainActor () -> ()) { + nonisolated func retryIfPossible(after: Double, block: @escaping @Sendable @MainActor () -> ()) { let delayTime = DispatchTime.now() + after DispatchQueue.main.asyncAfter(deadline: delayTime, execute: { Task { @MainActor in @@ -846,7 +846,7 @@ public protocol CloudKitZone: AnyObject { } /// Modify and delete the supplied CKRecords and CKRecord.IDs - func modify(recordsToSave: [CKRecord], recordIDsToDelete: [CKRecord.ID], completion: @escaping (Result) -> Void) { + func modify(recordsToSave: [CKRecord], recordIDsToDelete: [CKRecord.ID], completion: @escaping @Sendable (Result) -> Void) { guard !(recordsToSave.isEmpty && recordIDsToDelete.isEmpty) else { DispatchQueue.main.async { @@ -905,7 +905,7 @@ public protocol CloudKitZone: AnyObject { var recordToSaveChunks = recordsToSave.chunked(into: 200) var recordIDsToDeleteChunks = recordIDsToDelete.chunked(into: 200) - @MainActor func saveChunks(completion: @escaping (Result) -> Void) { + func saveChunks(completion: @escaping @Sendable (Result) -> Void) { if !recordToSaveChunks.isEmpty { let records = recordToSaveChunks.removeFirst() diff --git a/CloudKitSync/Sources/CloudKitSync/CloudKitArticlesZone.swift b/CloudKitSync/Sources/CloudKitSync/CloudKitArticlesZone.swift index d885b59f2..821939d40 100644 --- a/CloudKitSync/Sources/CloudKitSync/CloudKitArticlesZone.swift +++ b/CloudKitSync/Sources/CloudKitSync/CloudKitArticlesZone.swift @@ -95,52 +95,45 @@ public protocol CloudKitFeedInfoDelegate { try await delete(ckQuery: ckQuery) } - @MainActor public func modifyArticles(_ statusUpdates: [CloudKitArticleStatusUpdate], completion: @escaping ((Result) -> Void)) { + public func modifyArticles(_ statusUpdates: [CloudKitArticleStatusUpdate], completion: @escaping ((Result) -> Void)) { guard !statusUpdates.isEmpty else { completion(.success(())) return } - - var modifyRecords = [CKRecord]() - var newRecords = [CKRecord]() - var deleteRecordIDs = [CKRecord.ID]() - - for statusUpdate in statusUpdates { - switch statusUpdate.record { - case .all: - modifyRecords.append(self.makeStatusRecord(statusUpdate)) - modifyRecords.append(self.makeArticleRecord(statusUpdate.article!)) - case .new: - newRecords.append(self.makeStatusRecord(statusUpdate)) - newRecords.append(self.makeArticleRecord(statusUpdate.article!)) - case .delete: - deleteRecordIDs.append(CKRecord.ID(recordName: self.statusID(statusUpdate.articleID), zoneID: zoneID)) - case .statusOnly: - modifyRecords.append(self.makeStatusRecord(statusUpdate)) - deleteRecordIDs.append(CKRecord.ID(recordName: self.articleID(statusUpdate.articleID), zoneID: zoneID)) - } - } - compressionQueue.async { [newRecords] in - let compressedModifyRecords = self.compressArticleRecords(modifyRecords) - self.modify(recordsToSave: compressedModifyRecords, recordIDsToDelete: deleteRecordIDs) { result in - switch result { - case .success: - let compressedNewRecords = self.compressArticleRecords(newRecords) - self.saveIfNew(compressedNewRecords) { result in - switch result { - case .success: - completion(.success(())) - case .failure(let error): - completion(.failure(error)) - } - } - case .failure(let error): - Task { @MainActor in - self.handleModifyArticlesError(error, statusUpdates: statusUpdates, completion: completion) - } + Task { @MainActor in + + var modifyRecords = [CKRecord]() + var newRecords = [CKRecord]() + var deleteRecordIDs = [CKRecord.ID]() + + for statusUpdate in statusUpdates { + switch statusUpdate.record { + case .all: + modifyRecords.append(self.makeStatusRecord(statusUpdate)) + modifyRecords.append(self.makeArticleRecord(statusUpdate.article!)) + case .new: + newRecords.append(self.makeStatusRecord(statusUpdate)) + newRecords.append(self.makeArticleRecord(statusUpdate.article!)) + case .delete: + deleteRecordIDs.append(CKRecord.ID(recordName: self.statusID(statusUpdate.articleID), zoneID: zoneID)) + case .statusOnly: + modifyRecords.append(self.makeStatusRecord(statusUpdate)) + deleteRecordIDs.append(CKRecord.ID(recordName: self.articleID(statusUpdate.articleID), zoneID: zoneID)) } } + + let compressedModifyRecords = await compressedArticleRecords(modifyRecords) + + do { + try await self.modify(recordsToSave: compressedModifyRecords, recordIDsToDelete: deleteRecordIDs) + + let compressedNewRecords = await compressedArticleRecords(newRecords) + try await self.saveIfNew(compressedNewRecords) + + } catch { + self.handleModifyArticlesError(error, statusUpdates: statusUpdates, completion: completion) + } } } } @@ -236,34 +229,47 @@ private extension CloudKitArticlesZone { return record } - nonisolated func compressArticleRecords(_ records: [CKRecord]) -> [CKRecord] { - var result = [CKRecord]() - - for record in records { - - if record.recordType == CloudKitArticle.recordType { - - if let contentHTML = record[CloudKitArticle.Fields.contentHTML] as? String { - let data = Data(contentHTML.utf8) as NSData - if let compressedData = try? data.compressed(using: .lzfse) { - record[CloudKitArticle.Fields.contentHTMLData] = compressedData as Data - record[CloudKitArticle.Fields.contentHTML] = nil - } - } - - if let contentText = record[CloudKitArticle.Fields.contentText] as? String { - let data = Data(contentText.utf8) as NSData - if let compressedData = try? data.compressed(using: .lzfse) { - record[CloudKitArticle.Fields.contentTextData] = compressedData as Data - record[CloudKitArticle.Fields.contentText] = nil - } - } - + func compressedArticleRecords(_ records: [CKRecord]) async -> [CKRecord] { + + await withCheckedContinuation { continuation in + self._compressedArticleRecords(records) { records in + continuation.resume(returning: records) } - - result.append(record) } - - return result + } + + func _compressedArticleRecords(_ records: [CKRecord], completion: @escaping @Sendable ([CKRecord]) -> Void) { + + compressionQueue.async { + + var result = [CKRecord]() + + for record in records { + + if record.recordType == CloudKitArticle.recordType { + + if let contentHTML = record[CloudKitArticle.Fields.contentHTML] as? String { + let data = Data(contentHTML.utf8) as NSData + if let compressedData = try? data.compressed(using: .lzfse) { + record[CloudKitArticle.Fields.contentHTMLData] = compressedData as Data + record[CloudKitArticle.Fields.contentHTML] = nil + } + } + + if let contentText = record[CloudKitArticle.Fields.contentText] as? String { + let data = Data(contentText.utf8) as NSData + if let compressedData = try? data.compressed(using: .lzfse) { + record[CloudKitArticle.Fields.contentTextData] = compressedData as Data + record[CloudKitArticle.Fields.contentText] = nil + } + } + + } + + result.append(record) + } + + completion(result) + } } }