Skip to content

Commit

Permalink
MBX-2865 System performAndWait
Browse files Browse the repository at this point in the history
  • Loading branch information
Akylbek Utekeshev committed Oct 23, 2023
1 parent 5ec31bb commit 953929b
Show file tree
Hide file tree
Showing 3 changed files with 149 additions and 100 deletions.
163 changes: 96 additions & 67 deletions Mindbox/Database/MBDatabaseRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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> = 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> = 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> = CDEvent.fetchRequestForSend(lifeLimitDate: lifeLimitDate, retryDeadLine: retryDeadline)
func query(fetchLimit: Int, retryDeadline: TimeInterval = 60) throws -> [Event] {
var events: [Event] = []
try performAndWaitWrapper {
let request: NSFetchRequest<CDEvent> = 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<CDEvent>) throws -> [CDEvent] {
Expand All @@ -147,49 +152,52 @@ class MBDatabaseRepository {
}

func countDeprecatedEvents() throws -> Int {
var count: Int = 0
let context = persistentContainer.newBackgroundContext()
let request: NSFetchRequest<CDEvent> = 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 {
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "CDEvent")
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> = 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) {
Expand All @@ -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<CDEvent>, 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)
}
}

Expand Down Expand Up @@ -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
}
Expand All @@ -267,20 +275,41 @@ private extension MBDatabaseRepository {

func getMetadata<T>(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
}

func setMetadata<T>(_ value: T?, forKey key: MetadataKey) {
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
}
}

}
2 changes: 1 addition & 1 deletion Mindbox/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>4897</string>
<string>4898</string>
</dict>
</plist>
Loading

0 comments on commit 953929b

Please sign in to comment.