From 953929b39f0c3b0d945ea5aa5619f5172e00fc3c Mon Sep 17 00:00:00 2001 From: Akylbek Utekeshev Date: Wed, 18 Oct 2023 10:42:16 +0600 Subject: [PATCH] MBX-2865 System performAndWait --- Mindbox/Database/MBDatabaseRepository.swift | 163 +++++++++++------- Mindbox/Info.plist | 2 +- .../MBLoggerCoreDataManager.swift | 84 +++++---- 3 files changed, 149 insertions(+), 100 deletions(-) diff --git a/Mindbox/Database/MBDatabaseRepository.swift b/Mindbox/Database/MBDatabaseRepository.swift index e92578c0..dc29696b 100644 --- a/Mindbox/Database/MBDatabaseRepository.swift +++ b/Mindbox/Database/MBDatabaseRepository.swift @@ -57,7 +57,7 @@ class MBDatabaseRepository { if let store = persistentContainer.persistentStoreCoordinator.persistentStores.first { self.store = store } else { - Logger.common(message: MBDatabaseError.persistentStoreURLNotFound.errorDescription, level: .error, category: .database) + Logger.common(message: "Persistent store URL is missing", level: .error, category: .database) throw MBDatabaseError.persistentStoreURLNotFound } self.context = persistentContainer.newBackgroundContext() @@ -68,72 +68,77 @@ class MBDatabaseRepository { // MARK: - CRUD operations func create(event: Event) throws { - try context.performAndWait { - let entity = CDEvent(context: context) + try performAndWaitWrapper { + let entity = CDEvent(context: self.context) entity.transactionId = event.transactionId entity.timestamp = Date().timeIntervalSince1970 entity.type = event.type.rawValue entity.body = event.body - Logger.common(message: "Creating event with transactionId: \(event.transactionId)", level: .info, category: .database) - try saveEvent(withContext: context) + Logger.common(message: "Creating an event with Transaction ID: \(event.transactionId)", level: .default, category: .database) + try self.saveEvent(withContext: self.context) } } func read(by transactionId: String) throws -> CDEvent? { - try context.performAndWait { - Logger.common(message: "Reading event with transactionId: \(transactionId)", level: .info, category: .database) + var result: CDEvent? = nil + try performAndWaitWrapper { + Logger.common(message: "Attempting to read event with Transaction ID: \(transactionId)", level: .default, category: .database) let request: NSFetchRequest = CDEvent.fetchRequest(by: transactionId) - guard let entity = try findEvent(by: request) else { - Logger.common(message: "Unable to find event with transactionId: \(transactionId)", level: .error, category: .database) - return nil + if let entity = try self.findEvent(by: request) { + Logger.common(message: "Successfully read event with Transaction ID: \(entity.transactionId ?? "N/A")", level: .default, category: .database) + result = entity + } else { + Logger.common(message: "Event not found for Transaction ID: \(transactionId)", level: .error, category: .database) } - Logger.common(message: "Did read event with transactionId: \(entity.transactionId ?? "undefined")", level: .info, category: .database) - return entity } + + return result } func update(event: Event) throws { - try context.performAndWait { - Logger.common(message: "Updating event with transactionId: \(event.transactionId)", level: .info, category: .database) + try performAndWaitWrapper { + Logger.common(message: "Attempting to update event with Transaction ID: \(event.transactionId)", level: .default, category: .database) let request: NSFetchRequest = CDEvent.fetchRequest(by: event.transactionId) - guard let entity = try findEvent(by: request) else { - Logger.common(message: "Unable to find event with transactionId: \(event.transactionId)", level: .error, category: .database) + guard let entity = try self.findEvent(by: request) else { + Logger.common(message: "Event not found for update, Transaction ID: \(event.transactionId)", level: .error, category: .database) return } entity.retryTimestamp = Date().timeIntervalSince1970 - try saveEvent(withContext: context) + try self.saveEvent(withContext: self.context) } } func delete(event: Event) throws { - try context.performAndWait { - Logger.common(message: "Deleting event with transactionId: \(event.transactionId)", level: .info, category: .database) + try performAndWaitWrapper { + Logger.common(message: "Attempting to delete event with Transaction ID: \(event.transactionId)", level: .default, category: .database) let request = CDEvent.fetchRequest(by: event.transactionId) - guard let entity = try findEvent(by: request) else { - Logger.common(message: "Unable to find event with transactionId: \(event.transactionId)", level: .error, category: .database) + guard let entity = try self.findEvent(by: request) else { + Logger.common(message: "Event not found for deletion, Transaction ID: \(event.transactionId)", level: .error, category: .database) return } - context.delete(entity) - try saveEvent(withContext: context) + self.context.delete(entity) + try self.saveEvent(withContext: self.context) } } - func query(fetchLimit: Int, retryDeadline: TimeInterval = 60) throws -> [Event] { - try context.performAndWait { - Logger.common(message: "Quering events with fetchLimit: \(fetchLimit)", level: .info, category: .database) - let request: NSFetchRequest = CDEvent.fetchRequestForSend(lifeLimitDate: lifeLimitDate, retryDeadLine: retryDeadline) + func query(fetchLimit: Int, retryDeadline: TimeInterval = 60) throws -> [Event] { + var events: [Event] = [] + try performAndWaitWrapper { + let request: NSFetchRequest = CDEvent.fetchRequestForSend(lifeLimitDate: self.lifeLimitDate, retryDeadLine: retryDeadline) request.fetchLimit = fetchLimit - let events = try context.fetch(request) - guard !events.isEmpty else { - Logger.common(message: "Unable to find events", level: .info, category: .delivery) - return [] + let fetchedEvents = try self.context.fetch(request) + guard !fetchedEvents.isEmpty else { + Logger.common(message: "No events found for query", level: .error, category: .delivery) + return } - Logger.common(message: "Did query events count: \(events.count)", level: .info, category: .database) - return events.compactMap { - Logger.common(message: "Event with transactionId: \(String(describing: $0.transactionId))", level: .info, category: .database) + Logger.common(message: "Queried \(fetchedEvents.count) events successfully", level: .info, category: .database) + events = fetchedEvents.compactMap { + Logger.common(message: "Processing event with Transaction ID: \(String(describing: $0.transactionId))", level: .info, category: .database) return Event($0) } } + + return events } func query(by request: NSFetchRequest) throws -> [CDEvent] { @@ -147,19 +152,20 @@ class MBDatabaseRepository { } func countDeprecatedEvents() throws -> Int { + var count: Int = 0 let context = persistentContainer.newBackgroundContext() let request: NSFetchRequest = CDEvent.deprecatedEventsFetchRequest(lifeLimitDate: lifeLimitDate) - return try context.performAndWait { - Logger.common(message: "Counting deprecated elements", level: .info, category: .database) + + try performAndWaitWrapper { do { - let count = try context.count(for: request) - Logger.common(message: "Deprecated Events did count: \(count)", level: .info, category: .database) - return count + count = try context.count(for: request) + Logger.common(message: "Total deprecated events: \(count)", level: .default, category: .database) } catch { - Logger.common(message: "Counting events failed with error: \(error.localizedDescription)", level: .error, category: .database) + Logger.common(message: "Failed to count events: \(error.localizedDescription)", level: .error, category: .database) throw error } } + return count } func erase() throws { @@ -167,29 +173,31 @@ class MBDatabaseRepository { let eraseRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) infoUpdateVersion = nil installVersion = nil - try context.performAndWait { - try context.execute(eraseRequest) - try saveEvent(withContext: context) - try countEvents() + try performAndWaitWrapper { + try self.context.execute(eraseRequest) + try self.saveEvent(withContext: self.context) + try self.countEvents() } } @discardableResult func countEvents() throws -> Int { let request: NSFetchRequest = CDEvent.countEventsFetchRequest() - return try context.performAndWait { - Logger.common(message: "Events count limit: \(limit)", level: .info, category: .database) - Logger.common(message: "Counting events...", level: .info, category: .database) + var result = 0 + try performAndWaitWrapper { + Logger.common(message: "Counting total events", level: .default, category: .database) do { - let count = try context.count(for: request) - Logger.common(message: "Events count: \(count)", level: .info, category: .database) - cleanUp(count: count) - return count + let count = try self.context.count(for: request) + Logger.common(message: "Total events counted: \(count)", level: .default, category: .database) + self.cleanUp(count: count) + result = count } catch { - Logger.common(message: "Counting events failed with error: \(error.localizedDescription)", level: .error, category: .database) + Logger.common(message: "Failed to count events: \(error.localizedDescription)", level: .error, category: .database) throw error } } + + return result } private func cleanUp(count: Int) { @@ -201,24 +209,24 @@ class MBDatabaseRepository { do { try delete(by: request, withContext: context) } catch { - Logger.common(message: "Unable to remove elements", level: .error, category: .database) + Logger.common(message: "Failed to remove excess events", level: .error, category: .database) } } private func delete(by request: NSFetchRequest, withContext context: NSManagedObjectContext) throws { - try context.performAndWait { - Logger.common(message: "Finding elements to remove", level: .info, category: .database) + try performAndWaitWrapper { + Logger.common(message: "Searching for events to remove", level: .default, category: .database) let events = try context.fetch(request) guard !events.isEmpty else { - Logger.common(message: "Elements to remove not found", level: .info, category: .database) + Logger.common(message: "No events found for removal", level: .default, category: .database) return } events.forEach { - Logger.common(message: "Remove element with transactionId: \(String(describing: $0.transactionId)) and timestamp: \(Date(timeIntervalSince1970: $0.timestamp))", level: .info, category: .database) + Logger.common(message: "Removing event with Transaction ID: \(String(describing: $0.transactionId)) and Timestamp: \(Date(timeIntervalSince1970: $0.timestamp))", level: .default, category: .database) context.delete($0) } - try saveEvent(withContext: context) + try self.saveEvent(withContext: context) } } @@ -246,15 +254,15 @@ private extension MBDatabaseRepository { func saveContext(_ context: NSManagedObjectContext) throws { do { try context.save() - Logger.common(message: "Context did save", level: .info, category: .database) + Logger.common(message: "Successfully saved context", level: .default, category: .database) } catch { switch error { case let error as NSError where error.domain == NSSQLiteErrorDomain && error.code == 13: - Logger.common(message: "Context did save failed with SQLite Database out of space error: \(error)", level: .error, category: .database) + Logger.common(message: "Context save failed: SQLite Database out of space, Error: \(error)", level: .error, category: .database) fallthrough default: context.rollback() - Logger.common(message: "Context did save failed with error: \(error)", level: .error, category: .database) + Logger.common(message: "Context save failed: \(error)", level: .error, category: .database) } throw error } @@ -267,7 +275,7 @@ private extension MBDatabaseRepository { func getMetadata(forKey key: MetadataKey) -> T? { let value = store.metadata[key.rawValue] as? T - Logger.common(message: "Fetch metadata for key: \(key.rawValue) with value: \(String(describing: value))", level: .info, category: .database) + Logger.common(message: "Retrieved metadata for key: \(key.rawValue), Value: \(String(describing: value))", level: .default, category: .database) return value } @@ -275,12 +283,33 @@ private extension MBDatabaseRepository { store.metadata[key.rawValue] = value persistentContainer.persistentStoreCoordinator.setMetadata(store.metadata, for: store) do { - try context.performAndWait { - try saveContext(context) - Logger.common(message: "Did save metadata of \(key.rawValue) to: \(String(describing: value))", level: .info, category: .database) } + try performAndWaitWrapper { + try self.saveContext(self.context) + Logger.common(message: "Successfully saved metadata for key: \(key.rawValue), Value: \(String(describing: value))", level: .default, category: .database) + } } catch { - Logger.common(message: "Did save metadata of \(key.rawValue) failed with error: \(error.localizedDescription)", level: .error, category: .database) + Logger.common(message: "Failed to save metadata for key: \(key.rawValue), Error: \(error.localizedDescription)", level: .error, category: .database) + } + } + + func performAndWaitWrapper(_ block: @escaping () throws -> Void) throws { + var blockError: Error? = nil + if #available(iOS 15.0, *) { + try context.performAndWait { + try block() + } + } else { + context.performAndWait { + do { + try block() + } catch { + blockError = error + } + } + } + + if let error = blockError { + throw error } } - } diff --git a/Mindbox/Info.plist b/Mindbox/Info.plist index c6cff490..86c9da8d 100644 --- a/Mindbox/Info.plist +++ b/Mindbox/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 4897 + 4898 diff --git a/MindboxLogger/Shared/LoggerRepository/MBLoggerCoreDataManager.swift b/MindboxLogger/Shared/LoggerRepository/MBLoggerCoreDataManager.swift index 21274eb1..87d023cd 100644 --- a/MindboxLogger/Shared/LoggerRepository/MBLoggerCoreDataManager.swift +++ b/MindboxLogger/Shared/LoggerRepository/MBLoggerCoreDataManager.swift @@ -75,7 +75,7 @@ public class MBLoggerCoreDataManager { try delete() } - try self.context.performAndWait { + try performAndWaitWrapper { let entity = CDLogMessage(context: self.context) entity.message = message entity.timestamp = timestamp @@ -84,82 +84,81 @@ public class MBLoggerCoreDataManager { } public func getFirstLog() throws -> LogMessage? { - try context.performAndWait { + var result: LogMessage? + try performAndWaitWrapper { let fetchRequest = NSFetchRequest(entityName: Constants.model) fetchRequest.predicate = NSPredicate(value: true) fetchRequest.sortDescriptors = [NSSortDescriptor(key: "timestamp", ascending: true)] fetchRequest.fetchLimit = 1 - let results = try context.fetch(fetchRequest) - var logMessage: LogMessage? + let results = try self.context.fetch(fetchRequest) if let first = results.first { - logMessage = LogMessage(timestamp: first.timestamp, message: first.message) + result = LogMessage(timestamp: first.timestamp, message: first.message) } - - return logMessage } + + return result } - + public func getLastLog() throws -> LogMessage? { - try context.performAndWait { + var result: LogMessage? + try performAndWaitWrapper { let fetchRequest = NSFetchRequest(entityName: Constants.model) fetchRequest.predicate = NSPredicate(value: true) fetchRequest.sortDescriptors = [NSSortDescriptor(key: "timestamp", ascending: false)] fetchRequest.fetchLimit = 1 - let results = try context.fetch(fetchRequest) - var logMessage: LogMessage? + let results = try self.context.fetch(fetchRequest) if let last = results.last { - logMessage = LogMessage(timestamp: last.timestamp, message: last.message) + result = LogMessage(timestamp: last.timestamp, message: last.message) } - - return logMessage } + + return result } public func fetchPeriod(_ from: Date, _ to: Date) throws -> [LogMessage] { - try context.performAndWait { + var result: [LogMessage] = [] + try performAndWaitWrapper { let fetchRequest = NSFetchRequest(entityName: Constants.model) fetchRequest.predicate = NSPredicate(format: "timestamp >= %@ AND timestamp <= %@", from as NSDate, to as NSDate) - let logs = try context.fetch(fetchRequest) - var fetchedLogs: [LogMessage] = [] + let logs = try self.context.fetch(fetchRequest) logs.forEach { - fetchedLogs.append(LogMessage(timestamp: $0.timestamp, message: $0.message)) + result.append(LogMessage(timestamp: $0.timestamp, message: $0.message)) } - - return fetchedLogs } + + return result } public func delete() throws { - try context.performAndWait { + try performAndWaitWrapper { let request = NSFetchRequest(entityName: Constants.model) - let count = try context.count(for: request) + let count = try self.context.count(for: request) let limit: Double = (Double(count) * 0.1).rounded() // 10% percent of all records should be removed request.fetchLimit = Int(limit) request.includesPropertyValues = false - let results = try context.fetch(request) - + let results = try self.context.fetch(request) + results.compactMap { $0 as? NSManagedObject }.forEach { - context.delete($0) + self.context.delete($0) } try saveEvent(withContext: context) - queue.async { - Logger.common(message: "10% logs has been deleted", level: .debug, category: .general) - } } + + Logger.common(message: "10% logs has been deleted", level: .debug, category: .general) } public func deleteAll() throws { - try context.performAndWait { + try performAndWaitWrapper { let request = NSFetchRequest(entityName: Constants.model) request.includesPropertyValues = false - let results = try context.fetch(request) + let results = try self.context.fetch(request) results.compactMap { $0 as? NSManagedObject }.forEach { - context.delete($0) + self.context.delete($0) } - try saveEvent(withContext: context) + try self.saveEvent(withContext: self.context) } } } @@ -191,4 +190,25 @@ private extension MBLoggerCoreDataManager { let size = url.fileSize / 1024 // Bytes to Kilobytes return Int(size) } + + func performAndWaitWrapper(_ block: @escaping () throws -> Void) throws { + var blockError: Error? = nil + if #available(iOS 15.0, *) { + try context.performAndWait { + try block() + } + } else { + context.performAndWait { + do { + try block() + } catch { + blockError = error + } + } + } + + if let error = blockError { + throw error + } + } }