Skip to content

Update DocumentationContext initializer to be async #1244

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

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

Conversation

d-ronnqvist
Copy link
Contributor

@d-ronnqvist d-ronnqvist commented Jun 26, 2025

Bug/issue #, if applicable:

Summary

Today we use GCD in DocumentationContext.register(_:) to concurrently load articles, decode json, and do other work.
Because DispatchGroup.wait() waits synchronously, register(_:) can also be a synchronous call.

This is a 200 line change with 2000 lines of unavoidable modifications in tests.

Assuming that we want to start using Swift Concurrency in this code someday, we need to make this an asynchronous call instead.
We can mark an API as async without making any awaiting calls in its implementation but all callers need to await when calling this API (and the callers needs to be marked async which means that their callers need to await etc.). See also What Color is Your Function?.

Because DocumentationContext.register(_:) is used by the deprecated public DocumentationContext.init(dataProvider:diagnosticEngine:configuration:) initializer, we would need to make that initializer async if we made register(_:) asynchronous but that would be a source breaking change because callers who depend on Swift DocC would need to add an await keyword in their code (and potentially mark their calling code async as well).
However, we can mark the new DocumentationContext.init(bundle:dataProvider:diagnosticEngine:configuration:) initializer as asynchronous without breaking library consumers because it is still only accessible within the package.

Why do this now?

After 6.2 is released we will remove the deprecated DocumentationContext initializer.
However, we don't have any other API for library consumers to create a documentation context, so we need to add some public replacement.

If we promote the new initializer from package access to public access while it is still a synchronous call, we add another synchronous caller to register(_:) and therefore delay when we can start adopting Swift Concurrency by at least another release cycle (because we'd need to deprecate the new initializer, and introduce an asynchronous replacement, and wait until we can remove the new deprecated initializer before we can make anything it calls asynchronous).

However, if we make the new public replacement API for creating documentation contexts asynchronous from the start, we have nothing technically preventing us from adopting Swift Concurrency in this code after 6.2 is released.

What "shape" should this new API have?

It's not really important how this new API for creating documentation context "looks". For our ability to start adopting Swift Concurrency it only matters that it's asynchronous.

Three possible alternatives could be:

  • An initializer (like in this PR)

    let context = try await DocumentationContext(...)
  • An static method on the context type itself (or another type)

    let context = try await DocumentationContext.makeContext(...)
  • An (consuming) method on another type itself (like a hypothetical "builder")

    let builder = DocumentationContext.Builder(...)
    let context = try await builder.build()

The "shape" of this public API doesn't meaningfully impact the internal implementation. For example, we could have a private builder type internal to the initializer if we wanted to separate the context creation code from the rest of the context code.

Also, needing to deprecate one "shape" of asynchronous API to replace it with another "shape" of asynchronous API doesn't affect our ability to adopt Swift Concurrency.

More details about these change

The key change in this PR is adding the async keyword to the package access DocumentationContext initializer here

Two of the three places that calls this initializer (except for tests) is ConvertAction.perform(logHandle:) and EmitGeneratedCurationAction.perform(logHandle:) and which are already asynchronous.
The ConvertAction code needed one small update to a signpost call to support awaiting the context creation.

The only other place that calls this initializer is the ConvertService.process(_:completion:).
Despite the completion handler parameter, this is actually a synchronous API which is possibly unexpected for the caller.
However, the fact that it has a completion handler means that we can easily make it non-blocking for the caller.
I did this my introducing an async alternative and having the completion handler version of the API call the asynchronous API inside a Task.
The internal implementation of process(...) made heavy use of Result type to no real benefit but negative impacts on its readability—requiring the reader to jump between 3 private helper functions that unwrapped an optional, decoded a codable value, and encoded a codable value—and the ability to use asynchronous calls.
By inlining these simple one-liners and abandoning the use of Result types in this code, the code got half as long and all the basic message processing code can be read linearly in one place with only the business logic in a separate private helper function.

All the other ~2000 lines modified are all in unit tests. These changes are unavoidable no matter the "shape" of the asynchronous context creation API.

Dependencies

None

Testing

Nothing in particular. This isn't a user-facing change.

Checklist

Make sure you check off the following items. If they cannot be completed, provide a reason.

  • Added tests
  • Ran the ./bin/test script and it succeeded
  • Updated documentation if necessary

@d-ronnqvist
Copy link
Contributor Author

@swift-ci please test

@d-ronnqvist
Copy link
Contributor Author

@swift-ci please test

@d-ronnqvist d-ronnqvist marked this pull request as draft June 26, 2025 15:48
@d-ronnqvist
Copy link
Contributor Author

Because this change makes it harder to cherrypick fixes with new tests from main to the 6.2 release branch, we plan on holding this for a handful of weeks until we don't think that there will be any more 6.2 cherrypicks (or at least very very few).

Copy link
Contributor

@QuietMisdreavus QuietMisdreavus left a comment

Choose a reason for hiding this comment

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

Changes look good. Thanks for laying the groundwork to asyncify the code!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants