Skip to content

Adopt the new Core Data writer API part 2 - #19186

Merged
crazytonyli merged 8 commits into
trunkfrom
core-data-adopt-the-new-writer-api-part-2
Sep 7, 2022
Merged

Adopt the new Core Data writer API part 2#19186
crazytonyli merged 8 commits into
trunkfrom
core-data-adopt-the-new-writer-api-part-2

Conversation

@crazytonyli

@crazytonyli crazytonyli commented Aug 15, 2022

Copy link
Copy Markdown
Contributor

Part 2 of #19185.

Changes

This PR introduces a new write API which accepts a throw closure, which is the main reason that this PR is separated from the one I mentioned above. This new API is implemented in Swift, since a throw closure isn't something can be done in Objective-C—unlike its "sibling APIs".

This new API is used in a few newDerivedContext usages 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:

  • blogging prompts settings are reflected in the app.
  • Add and remove an user to a site.
  • Create a page with selected layout.

Regression Notes

  1. Potential unintended areas of impact
    None.

  2. What I did to test those areas of impact (or what existing automated tests I relied on)

  • Added and removed an user from my blog.
  • Created a few pages with a few different layouts.
  1. What automated tests I added (or what prevented me from doing so)
    N/A

PR submission checklist:

  • I have completed the Regression Notes.
  • I have considered adding unit tests for my changes.
  • I have considered adding accessibility improvements for my changes.
  • I have considered if this change warrants user-facing release notes and have added them to RELEASE-NOTES.txt if necessary.

@crazytonyli crazytonyli added the Core Data Issues related to Core Data label Aug 15, 2022
@crazytonyli crazytonyli added this to the 20.6 milestone Aug 15, 2022
@crazytonyli
crazytonyli requested a review from a team August 15, 2022 08:14
@crazytonyli crazytonyli self-assigned this Aug 15, 2022
@wpmobilebot

wpmobilebot commented Aug 15, 2022

Copy link
Copy Markdown
Contributor
You can test the changes in Jetpack from this Pull Request by:
  • Clicking here or scanning the QR code below to access App Center
  • Then installing the build number pr19186-d3c4c96 on your iPhone

If you need access to App Center, please ask a maintainer to add you.

@wpmobilebot

wpmobilebot commented Aug 15, 2022

Copy link
Copy Markdown
Contributor
You can test the changes in WordPress from this Pull Request by:
  • Clicking here or scanning the QR code below to access App Center
  • Then installing the build number pr19186-d3c4c96 on your iPhone

If you need access to App Center, please ask a maintainer to add you.

@jkmassel jkmassel left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

@crazytonyli

crazytonyli commented Aug 15, 2022

Copy link
Copy Markdown
Contributor Author

@jkmassel I should've mentioned this in the description, but one question we need to answer about the ContextManager.saveUsingBlock APIs is, should an exception inside the block prevents any changes happened before it being saved?

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)
}

@jkmassel

jkmassel commented Aug 16, 2022

Copy link
Copy Markdown
Contributor

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 throws variant of save that doesn't do any error internal error handling. Where possible, we can then incrementally adopt it (though IINM we'll need to create throws variants of most of our service class methods as well, so that's not a small change).

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?

@crazytonyli

Copy link
Copy Markdown
Contributor Author

@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 saveUsingBlock:completion: function in the changed files, since the function doesn't return a success or failure result—completion always gets called as it has no way of knowing if there is any error in the block.

Do you think it's worth adding a simple performBackgroundTask and using it to replace the newDerivedContext().perform calls in the changed files here?

    func performBackgroundTask(_ block: @escaping (NSManagedObjectContext) -> Void) {
        let context = newDerivedContext()
        context.perform {
            block(context)
        }
    }

@jkmassel

Copy link
Copy Markdown
Contributor

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?

@crazytonyli

crazytonyli commented Aug 17, 2022

Copy link
Copy Markdown
Contributor Author

@jkmassel 🤦‍♂️ Of course, I somehow forgot we only need to make the performAndSave a non-throwing function. I'll take your suggestion and update this PR accordingly. Thanks!

BTW, as discussed previously, I think I'll leave the async version out for now. Actually... I'll include the async version as using it in unit test is quite nice.

@mokagio mokagio modified the milestones: 20.6, 20.7 Aug 21, 2022
@mokagio

mokagio commented Aug 21, 2022

Copy link
Copy Markdown
Contributor

@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.

@crazytonyli
crazytonyli enabled auto-merge August 22, 2022 02:09
@crazytonyli

Copy link
Copy Markdown
Contributor Author

Friendly PR review reminder to @jkmassel and @mokagio 😄

@mokagio
mokagio disabled auto-merge August 31, 2022 03:56

@mokagio mokagio left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: Would you consider a more verbose version of the failure message?

Suggested change
XCTFail("The above call should throw")
XCTFail("Expected `performAndSave` to throw error, but this code was reached so no error was thrown.")

}
}

extension CoreDataStack {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +101 to +109
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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 Good call.

return
}
newPrompt.configure(with: remotePrompt, for: self.siteID.int32Value)
contextManager.performAndSave { derivedContext in

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I was trying to minimize the diff 😄

mokagio and others added 2 commits August 31, 2022 14:05
@mokagio mokagio removed this from the 20.7 milestone Sep 5, 2022
@mokagio mokagio added this to the 20.8 milestone Sep 5, 2022
@mokagio

mokagio commented Sep 5, 2022

Copy link
Copy Markdown
Contributor

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.

@crazytonyli

Copy link
Copy Markdown
Contributor Author

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 😺

@crazytonyli
crazytonyli dismissed jkmassel’s stale review September 7, 2022 00:25

Let me know if you have any further comments on this change and I'll address in a separate PR if needed

@crazytonyli
crazytonyli merged commit 0de35b1 into trunk Sep 7, 2022
@crazytonyli
crazytonyli deleted the core-data-adopt-the-new-writer-api-part-2 branch September 7, 2022 00:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Core Data Issues related to Core Data

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants