-
Notifications
You must be signed in to change notification settings - Fork 211
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improve database container read and reset #3365
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1154,6 +1154,53 @@ extension NSManagedObjectContext: MessageDatabaseSession { | |
$0.localState = .pendingUpload | ||
} | ||
} | ||
|
||
func loadMessage( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Moved these under DatabaseSession so that I can use DatabaseSession in async version of the read() |
||
before id: MessageId, | ||
cid: String | ||
) throws -> MessageDTO? { | ||
try MessageDTO.loadMessage( | ||
before: id, | ||
cid: cid, | ||
deletedMessagesVisibility: deletedMessagesVisibility ?? .alwaysVisible, | ||
shouldShowShadowedMessages: shouldShowShadowedMessages ?? true, | ||
context: self | ||
) | ||
} | ||
|
||
func loadMessages( | ||
from fromIncludingDate: Date, | ||
to toIncludingDate: Date, | ||
in cid: ChannelId, | ||
sortAscending: Bool | ||
) throws -> [MessageDTO] { | ||
try MessageDTO.loadMessages( | ||
from: fromIncludingDate, | ||
to: toIncludingDate, | ||
in: cid, | ||
sortAscending: sortAscending, | ||
deletedMessagesVisibility: deletedMessagesVisibility ?? .alwaysVisible, | ||
shouldShowShadowedMessages: shouldShowShadowedMessages ?? true, | ||
context: self | ||
) | ||
} | ||
|
||
func loadReplies( | ||
from fromIncludingDate: Date, | ||
to toIncludingDate: Date, | ||
in messageId: MessageId, | ||
sortAscending: Bool | ||
) throws -> [MessageDTO] { | ||
try MessageDTO.loadReplies( | ||
from: fromIncludingDate, | ||
to: toIncludingDate, | ||
in: messageId, | ||
sortAscending: sortAscending, | ||
deletedMessagesVisibility: deletedMessagesVisibility ?? .alwaysVisible, | ||
shouldShowShadowedMessages: shouldShowShadowedMessages ?? true, | ||
context: self | ||
) | ||
} | ||
} | ||
|
||
extension MessageDTO { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -253,13 +253,29 @@ class DatabaseContainer: NSPersistentContainer { | |
} | ||
} | ||
|
||
func read<T>(_ actions: @escaping (DatabaseSession) throws -> T, completion: @escaping (Result<T, Error>) -> Void) { | ||
let context = backgroundReadOnlyContext | ||
func write(_ actions: @escaping (DatabaseSession) throws -> Void) async throws { | ||
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in | ||
write(actions) { error in | ||
if let error { | ||
continuation.resume(throwing: error) | ||
} else { | ||
continuation.resume(returning: ()) | ||
} | ||
} | ||
} | ||
} | ||
|
||
private func read<T>( | ||
from context: NSManagedObjectContext, | ||
_ actions: @escaping (DatabaseSession) throws -> T, | ||
completion: @escaping (Result<T, Error>) -> Void | ||
) { | ||
context.perform { | ||
do { | ||
let changeCounts = context.currentChangeCounts() | ||
let results = try actions(context) | ||
if context.hasChanges { | ||
assertionFailure("Background context is read only, but calling actions() created changes") | ||
if changeCounts != context.currentChangeCounts() { | ||
assertionFailure("Context is read only, but actions created changes: (updated=\(context.updatedObjects), inserted=\(context.insertedObjects), deleted=\(context.deletedObjects)") | ||
Comment on lines
+275
to
+278
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Did not figure out why deleted message is kept tracked by the context which triggered this assert. Just changed it around so that we only trigger it when actually the actions closure changed something. |
||
} | ||
completion(.success(results)) | ||
} catch { | ||
|
@@ -268,45 +284,20 @@ class DatabaseContainer: NSPersistentContainer { | |
} | ||
} | ||
|
||
func write(_ actions: @escaping (DatabaseSession) throws -> Void) async throws { | ||
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in | ||
write(actions) { error in | ||
if let error { | ||
continuation.resume(throwing: error) | ||
} else { | ||
continuation.resume(returning: ()) | ||
} | ||
} | ||
} | ||
func read<T>(_ actions: @escaping (DatabaseSession) throws -> T, completion: @escaping (Result<T, Error>) -> Void) { | ||
read(from: backgroundReadOnlyContext, actions, completion: completion) | ||
} | ||
|
||
func read<T>(_ actions: @escaping (NSManagedObjectContext) throws -> T) async throws -> T { | ||
let context = stateLayerContext | ||
return try await withCheckedThrowingContinuation { continuation in | ||
context.perform { | ||
do { | ||
let results = try actions(context) | ||
if context.hasChanges { | ||
assertionFailure("State layer context is read only, but calling actions() created changes") | ||
} | ||
continuation.resume(returning: results) | ||
} catch { | ||
continuation.resume(throwing: error) | ||
} | ||
func read<T>(_ actions: @escaping (DatabaseSession) throws -> T) async throws -> T { | ||
try await withCheckedThrowingContinuation { continuation in | ||
read(from: stateLayerContext, actions) { result in | ||
continuation.resume(with: result) | ||
} | ||
} | ||
} | ||
|
||
/// Removes all data from the local storage. | ||
func removeAllData(completion: ((Error?) -> Void)? = nil) { | ||
/// Cleanup the current user cache for all manage object contexts. | ||
allContext.forEach { context in | ||
context.perform { | ||
context.invalidateCurrentUserCache() | ||
context.reset() | ||
} | ||
} | ||
|
||
let entityNames = managedObjectModel.entities.compactMap(\.name) | ||
writableContext.perform { [weak self] in | ||
self?.canWriteData = false | ||
|
@@ -332,11 +323,24 @@ class DatabaseContainer: NSPersistentContainer { | |
} | ||
if !deletedObjectIds.isEmpty, let contexts = self?.allContext { | ||
log.debug("Merging \(deletedObjectIds.count) deletions to contexts", subsystems: .database) | ||
// Merging changes triggers DB observers to react to deletions | ||
NSManagedObjectContext.mergeChanges( | ||
fromRemoteContextSave: [NSDeletedObjectsKey: deletedObjectIds], | ||
into: contexts | ||
) | ||
} | ||
// Finally reset states of all the contexts after batch delete and deletion propagation. | ||
if let writableContext = self?.writableContext, let allContext = self?.allContext { | ||
writableContext.invalidateCurrentUserCache() | ||
writableContext.reset() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Did you also tried reseting these context when a delete is performed on There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Did not test that. Slightly afraid to do it because we use these contexts for DB observers. Thinking about letting it be 😄 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah probably better 👍 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Out of curiosity, I tested it and it works. Maybe if we still have problems with this in the future we can go back here and apply the following change before the if !self.backgroundReadOnlyContext.deletedObjects.isEmpty {
self.backgroundReadOnlyContext.reset()
} |
||
|
||
for context in allContext where context != writableContext { | ||
context.performAndWait { | ||
context.invalidateCurrentUserCache() | ||
context.reset() | ||
} | ||
} | ||
} | ||
Comment on lines
+332
to
+343
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Moved these here because context should be reset after we are done with the batch delete. Otherwise deletes stick around to contexts (because I added |
||
self?.canWriteData = true | ||
completion?(lastEncounteredError) | ||
} | ||
|
@@ -452,6 +456,14 @@ extension NSManagedObjectContext { | |
deletedObjects.forEach { discardChanges(for: $0) } | ||
} | ||
|
||
fileprivate func currentChangeCounts() -> [String: Int] { | ||
[ | ||
"inserted": insertedObjects.count, | ||
"updated": updatedObjects.count, | ||
"deleted": deletedObjects.count | ||
] | ||
} | ||
|
||
func observeChanges(in otherContext: NSManagedObjectContext) -> NSObjectProtocol { | ||
assert(!automaticallyMergesChangesFromParent, "Duplicate change handling") | ||
return NotificationCenter.default | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If data is empty, error is logged which can happen when trying to convert deleted DTO