Skip to content

Adopt the new Core Data writer API part 2#19186

Merged
crazytonyli merged 8 commits intotrunkfrom
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 intotrunkfrom
core-data-adopt-the-new-writer-api-part-2

Conversation

@crazytonyli
Copy link
Contributor

@crazytonyli crazytonyli commented Aug 15, 2022

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
Copy link
Contributor

wpmobilebot commented Aug 15, 2022

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
Copy link
Contributor

wpmobilebot commented Aug 15, 2022

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.

Copy link
Contributor

@jkmassel jkmassel left a comment

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
Copy link
Contributor Author

crazytonyli commented Aug 15, 2022

@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
Copy link
Contributor

jkmassel commented Aug 16, 2022

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
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
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
Copy link
Contributor Author

crazytonyli commented Aug 17, 2022

@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
Copy link
Contributor

mokagio commented Aug 21, 2022

@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
Contributor Author

Friendly PR review reminder to @jkmassel and @mokagio 😄

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

@mokagio mokagio left a comment

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
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
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
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
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
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
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
Copy link
Contributor

mokagio commented Sep 5, 2022

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