Adopt the new Core Data writer API part 2#19186
Conversation
You can test the changes in Jetpack from this Pull Request by:
|
You can test the changes in WordPress from this Pull Request by:
|
jkmassel
left a comment
There was a problem hiding this comment.
Given the fact that both the old and new implementations swallow the error, do we need this? I wonder if just continuing to do do/catch inside the core data block is preferable from a "smaller diffs" perspective?
As much as I'd like to improve on our error handling, IMHO the time to do it is when we're able to create a new fully async method that propagates the error instead of hoisting it up one level then discarding it.
WDYT?
|
@jkmassel I should've mentioned this in the description, but one question we need to answer about the I don't think we can make the decision for the call sites. What we can do is providing two variants, one accepts a non-throwing block and the other accepts a throwing block. As a result, they can decide whether to save the changes on call site: // Example 1: Error is ignored and the data is saved.
contextManager.save { context in
let foo = Foo(context)
foo.bar = try? context.fetch(Bar.self)
}
// Example 2: Error is propagated outside and the data isn't saved.
do {
try contextManager.save { context in
let foo = Foo(context)
foo.bar = try context.fetch(Bar.self)
}
} catch {
handle(error)
} |
|
Ah this is interesting! As of right now, I believe that every instance is currently an example of #1. IMHO (assuming that's true), it'd make sense to create a proper My suggestion would be to find one path we can completely convert to throwing methods (ie – a single fetch operation from top to bottom) and do so – that allows us to design an API that's immediately used (thus hopefully shaking out any design issues). From there, we can incrementally convert other parts of the codebase as we see the opportunity. WDYT? |
|
@jkmassel Can you please expand on what you mean by "a single fetch operation from top to bottom"? Do you mean like a query-only operation? An incremental approach makes sense to me. For this PR though, we can't use the existing Do you think it's worth adding a simple func performBackgroundTask(_ block: @escaping (NSManagedObjectContext) -> Void) {
let context = newDerivedContext()
context.perform {
block(context)
}
} |
|
Could we use something like this to get the error back to the caller? func performAndSave(_ block: @escaping (NSManagedObjectContext) throws -> Void, completion: @escaping (Result<Void, Error>) -> Void) {
let context = newDerivedContext()
context.perform {
do {
try block(context)
self.saveContextAndWait(context)
completion(.success(Void()))
}
catch {
completion(.failure(error))
}
}
}The async version then becomes: func save(_ block: @escaping (NSManagedObjectContext) throws -> Void) async throws {
try await withCheckedThrowingContinuation { ct in
performAndSave(block) { ct.resume(with: $0) }
}
}WDYT? |
|
@jkmassel 🤦♂️ Of course, I somehow forgot we only need to make the
|
|
@crazytonyli I bumped this to the next release because we'll be code freezing 20.7 today and this hasn't been approved yet. If this cannot wait two weeks and it's important that it makes it into this release, let me know and we'll organize a new beta once ready. |
mokagio
left a comment
There was a problem hiding this comment.
Love that we are using Result here. IMHO, it's. a poorly understood type that should be used much more often.
The implementation looks good to me, I left only a couple of nitpicks.
I think this PR addresses the point @jkmassel's raised in that we have the two throws implementation, and two production use cases for the sync one. As for the async version, Tony's explanation for having it here, "I'll include the async version as using it in unit test is quite nice", is reasonable. 👍
I'm going to approve to unblock, but I took the liberty to disable auto-merge so @jkmassel can have another look. This should give @crazytonyli the flexibility to merge if necessary.
| account.username = "Unknown User" | ||
| throw NSError(domain: "save", code: 1) | ||
| } | ||
| XCTFail("The above call should throw") |
There was a problem hiding this comment.
Nitpick: Would you consider a more verbose version of the failure message?
| XCTFail("The above call should throw") | |
| XCTFail("Expected `performAndSave` to throw error, but this code was reached so no error was thrown.") |
| } | ||
| } | ||
|
|
||
| extension CoreDataStack { |
There was a problem hiding this comment.
I was going to suggest moving this in a dedicated file, then I noticed CoreDataHelper.swift already contains various extensions. I guess it's worth sticking to the existing pattern for now.
| ContextManager.shared.performAndSave({ context in | ||
| guard let blog = context.object(with: blogPersistentID) as? Blog else { | ||
| let userInfo = [NSLocalizedFailureReasonErrorKey: "Couldn't find blog to save the fetched results to."] | ||
| throw NSError(domain: "PageLayoutService.persistToCoreData", code: 0, userInfo: userInfo) | ||
| } | ||
| ContextManager.shared.save(context) | ||
| completion(.success(())) | ||
| } | ||
| cleanUpStoredLayouts(forBlog: blog, context: context) | ||
| try persistCategoriesToCoreData(blog, layouts.categories, context: context) | ||
| try persistLayoutsToCoreData(blog, layouts.layouts, context: context) | ||
| }, completion: completion) |
There was a problem hiding this comment.
Readability nitpick: Given the second closure is named, it looks odd to me that the first isn't.
What would you think of something like
ContextManager.shared.performAndSave(
attempt: { context in
...
},
completion: completion
)| return | ||
| } | ||
| newPrompt.configure(with: remotePrompt, for: self.siteID.int32Value) | ||
| contextManager.performAndSave { derivedContext in |
There was a problem hiding this comment.
I assume derivedContext is so named because that's what it was in the previous code. I wonder if it's useful to keep the "derived" information in the name, or if context would work just as well and be shorter, more straight to the point.
There was a problem hiding this comment.
Yeah, I was trying to minimize the diff 😄
This `Result` initializer takes a throwing closure as input and returns a `Result` wrapping its result. See https://developer.apple.com/documentation/swift/result/init(catching:)
|
I bumped this to the next milestone, 20.8, even though I had approved it. Given this is not user facing, hence we don't have any particular rush in shipping it, I'd rather let @crazytonyli merge when satisfied. Of course, the sooner we ship, the sooner we'll get real world feedback, but I think in this particular case it's okay to hold back. However, @crazytonyli let me know if you want this in the 20.7 build, and I'll take over merging and ship a new beta for it. |
|
I'm going to merge this PR. @jkmassel Let me know if you have any further comments on this change and I'll address in a separate PR if needed 😺 |
Let me know if you have any further comments on this change and I'll address in a separate PR if needed
Part 2 of #19185.
Changes
This PR introduces a new write API which accepts a
throwclosure, which is the main reason that this PR is separated from the one I mentioned above. This new API is implemented in Swift, since athrowclosure isn't something can be done in Objective-C—unlike its "sibling APIs".This new API is used in a few
newDerivedContextusages where the Core Data operations throw.Review tip: turning on "Hide whitespace" may make this PR easier to review.
Test instructions
I think (according to my understanding of the codebase, not the product...), here is a list of area that are changed in this PR. To test, make sure they are working as expected:
Regression Notes
Potential unintended areas of impact
None.
What I did to test those areas of impact (or what existing automated tests I relied on)
N/A
PR submission checklist:
RELEASE-NOTES.txtif necessary.