Skip to content
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

Ensure access to PDFDocument is always concurrency safe #58

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

Supereg
Copy link
Member

@Supereg Supereg commented Aug 29, 2024

Ensure access to PDFDocument is always concurrency safe

♻️ Current situation & Problem

The PR #51 added several interactions with PDFDocument that cannot be proven to be concurrency safe. This makes the package incompatible with Swift 6 and unable to compile. The issue was tracked in #57.
This PR attempts to resolve these issues. However, this not possible without breaking existing API. So see the initial state of the PR as a starting point for discussion on how we version this change and how we provide compatibility with Swift 5.10 environment as most of the changes require Swift 6 features.

PDFDocument is a non-[Sendable](https://developer.apple.com/documentation/swift/sendable) type (and probably won't be ever as it is an open class type inheriting from NSObject, see Sendable Classes). However, non-Sendable types can cross actor boundaries when passed as sending parameters. This keyword ensures that the caller won't maintain any references and ownership is fully transferred to the callee (for non-Sendable types). As the PDFDocument is just a result type we mean to transfer to the Standard that implements the ConsentConstraint and don't require to maintain ownership within SpeziOnboarding, we can easily send the PDFDocument to the Standard and provide ownership to the Standard.

EDIT: Here is a great deep dive into non-copyable types: Consume noncopyable types in Swift.

⚙️ Release Notes

All changes are annotated if they require Swift 6 language features and if they are considered a breaking change. Note that we assume framework users to operate in Swift 5 mode. As Swift 6 isn't released yet, we do not consider any changes to concurrency annotations that do not create compiler errors in Swift 5 (even with strict concurrency checking) as a breaking change.

The following changes were made according to the approach explained above:

  • The legacy OnboardingConstraint receives its PDFDocument as a sending parameter. This requires Swift 6 and is a breaking change.
  • The OnboardingDataSource receives the PDFDocument as a sending parameter. This requires Swift 6 and is not a breaking change.
  • ConsentDocumentExport was turned into a non-Copyable type that is non-Sendable. The access to the PDF is consuming and returns the PDFDocument as a sending result. This requires Swift 6 and is a breaking change.
    • consuming can only be applied to func declarations and sending only to results types of func declarations. Therefore, we needed to replace the async pdf property.
    • We currently keep the consumePDF method async as the documentation suggest that this might be required in the future. Is that really something that will come?
  • The ConsentConstraint was updated to receive the ConsentDocumentExport as a sending and consuming parameter. This requires Swift 6 and is a breaking change.

📚 Documentation

TBA

✅ Testing

The test app was updated and existing unit tests will cover the refactored code.

📝 Code of Conduct & Contributing Guidelines

By submitting creating this pull request, you agree to follow our Code of Conduct and Contributing Guidelines:

@Supereg Supereg requested a review from PSchmiedmayer August 29, 2024 13:32
@Supereg
Copy link
Member Author

Supereg commented Aug 29, 2024

@PSchmiedmayer as explained in the PR description. I requested review on this PR draft already to discuss potential ways forward with this PR as it inevitably causes breaking changes of recently introduced API and uses Swift 6 language features that might not be available to people still using the Swift 5 toolchain.

@Supereg Supereg linked an issue Aug 29, 2024 that may be closed by this pull request
1 task
@PSchmiedmayer
Copy link
Member

Thank you for taking a look at this @Supereg!

Thank you for breaking down the breaking and non-breaking changes. As far as I can see these are breaking changes in a need that a user would need to update to an Xcode version that supports Swift 6, but would still compile the package in combination with an other package or application that only uses the Swift 5 compilation mode without any errors in the source code?

And to add this so we don't run into this in the future: We should add a GitHub Action job that builds for Swift 6 and is required for all future PRs.

I am wondering if the general workflow should be: We merge #52 and #54 trying to avoid any breaking changes (1.X) sooner than later (CC @AdritRao & @RealLast).
With these being in place, we can start to prepare a source code and functionally-related breaking change, removing the older setups, trying to remodel elements to reflect the latest changes (e.g. also moving consent into its own target), and then tag a 2.X that only supports Swift 6?

A more general question: Should we handle supporting Swift 6 only (dropping compiling with Swift 5.X) as a breaking change across all our packages or is there a way to handle these are elements as minor changes, just requiring developers to update Xcode but ensuring that elements still build with the latest Xcode versions.

@Supereg
Copy link
Member Author

Supereg commented Sep 1, 2024

As far as I can see these are breaking changes in a need that a user would need to update to an Xcode version that supports Swift 6, but would still compile the package in combination with an other package or application that only uses the Swift 5 compilation mode without any errors in the source code?

Yes, the user would need to have the latest Xcode version with a Swift 6 toolchain. However, the consuming sending attributes in the method of the constraint protocol are also a source breaking change.
To the second part of your question, you can freely compile Swift 5 packages with the latest toolchain. Note, that we not necessarily require the Swift 6 language mode with this PR, only new features of the Swift 6 toolchain.

And to add this so we don't run into this in the future: We should add a GitHub Action job that builds for Swift 6 and is required for all future PRs.
We tried this, however this was not easily possible without breaking compatibility with Xcode 15. It wasn't possible to specify the swift 6 language mode from the command line for just a single package. It would apply the command line argument to all code which would raise compiler errors in a range of different packages.
Enabling the Swift 6 language mode in the Package.swift requires the latest 6.0 swift tools version which, therefore, breaks compiling with earlier Xcode versions.

With these being in place, we can start to prepare a source code and functionally-related breaking change, removing the older setups, trying to remodel elements to reflect the latest changes (e.g. also moving consent into its own target), and then tag a 2.X that only supports Swift 6?

This sounds like a good approach.

A more general question: Should we handle supporting Swift 6 only (dropping compiling with Swift 5.X) as a breaking change across all our packages or is there a way to handle these are elements as minor changes, just requiring developers to update Xcode but ensuring that elements still build with the latest Xcode versions.

If we do not consider requiring updating the Xcode version as a source breaking change, we can see this as a minor change (just updating the swift tools version in the Package.swift). After all, we are night increasing the minimum deployment target (which is definitely a source breaking change). Updating the Xcode version is eventually required by Apple anyways. So we might be okay with seeing this as a minor change as long as the Xcode version is fully release. Otherwise, I would tag such releases as pre-releases.

@PSchmiedmayer
Copy link
Member

Agree; let's wait until Xcode 16 is released as a stable release and then tag it as a minor when we bump the swift tools version to Swift 6. As long as it can be used with apps that still support Swift 6 we should be fine. We just might need to tag these releases in the reverse order of the dependency tree and we might not be able to catch all of them ... something to discuss in our next meeting.

@PSchmiedmayer PSchmiedmayer added the enhancement New feature or request label Sep 1, 2024
Copy link
Member

@PSchmiedmayer PSchmiedmayer left a comment

Choose a reason for hiding this comment

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

Thank you for working on this @Supereg; I would be happy to see this merged in combination with the other PRs open here; we might at some point move to tag a new major version of Onboarding anyways, e.g., moving consent into a few target or even a new package. Some of the OnboardingStack elements can also move into SpeziViews in the long run.

// Something the compiler doesn't realize here is that we can send the `PDFDocument` because it is located in a non-Sendable, non-Copyable
// type and accessing it will consume the enclosing type. Therefore, the PDFDocument instance can only be accessed once (even in async method)
// and that is fully checked at compile time by the compiler :rocket:
// See similar discussion: https://forums.swift.org/t/swift-6-consume-optional-noncopyable-property-and-transfer-sending-it-out/72414/3
Copy link
Member

Choose a reason for hiding this comment

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

Thank you for the context!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
Status: Backlog
Development

Successfully merging this pull request may close these issues.

Concurrency-unsafe interactions with PDFDocument
2 participants