Skip to content

Define commands as asynchronous and use Task for preview cancellation #1049

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

Merged
merged 9 commits into from
Oct 11, 2024

Conversation

d-ronnqvist
Copy link
Contributor

@d-ronnqvist d-ronnqvist commented Oct 8, 2024

Bug/issue #, if applicable:

Summary

This is the third of many changes to redefine how documentation contexts are created.

This redefined the various commands to be asynchronous so that preview action cancellation, documentation converter cancellation, and context registration cancellation can use Task.checkCancellation() instead of sharing a Boolean wrapped in a class and polling for cancellation on a timer.

This change also supports cancelling context registration during initialization, which enables us to move away from deferred context registration in a follow up PR.

Dependencies

This builds on top of #1052

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 Updated tests
  • Ran the ./bin/test script and it succeeded
  • [ ] Updated documentation if necessary

@d-ronnqvist d-ronnqvist added the code cleanup Code cleanup *without* user facing changes label Oct 8, 2024
@d-ronnqvist
Copy link
Contributor Author

How much do we care about source compatibility for the command line actions? I find that I need to write literal anti-patterns to make it compatible and I don't know if there's an expectation that that code should be used by anyone outside of this package. Should we stop surfacing it as a "product"?

@d-ronnqvist d-ronnqvist added the source breaking DocC's public API isn't source compatible with earlier versions label Oct 8, 2024
@d-ronnqvist
Copy link
Contributor Author

How much do we care about source compatibility for the command line actions? I find that I need to write literal anti-patterns to make it compatible and I don't know if there's an expectation that that code should be used by anyone outside of this package. Should we stop surfacing it as a "product"?

I made this new commit to stop surfacing the command line actions as a library product. This would be source breaking since consumers of this repo no longer can import it.

@d-ronnqvist d-ronnqvist removed the source breaking DocC's public API isn't source compatible with earlier versions label Oct 10, 2024
@d-ronnqvist
Copy link
Contributor Author

How much do we care about source compatibility for the command line actions? I find that I need to write literal anti-patterns to make it compatible and I don't know if there's an expectation that that code should be used by anyone outside of this package. Should we stop surfacing it as a "product"?

I made this new commit to stop surfacing the command line actions as a library product. This would be source breaking since consumers of this repo no longer can import it.

I moved that change to #1052 instead.

Copy link
Contributor

@patshaughnessy patshaughnessy left a comment

Choose a reason for hiding this comment

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

Looks great.

Are there any implications for switching to a async functions throughout the CLI and top level action structs?

Is there a way we can refactor the code in DocumentationContext.register to use async notation to manage the various discovery tasks, instead of using DispatchGroup and DispatchQueue?


// If cancelled, return early before we emit diagnostics.
func isConversionCancelled() -> Bool {
Task.isCancelled || isCancelled?.sync({ $0 }) == true
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do we need isCancelled ? I don't see code that sets it anywhere. Is calling Task.isCanclled sufficient?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

DocumentationConverter is public API it's possible that a caller is passing a shared isCancelled during initialization and setting it to true to cancel the conversion.

@@ -303,11 +268,9 @@ public struct ConvertAction: Action, RecreatingContext {
let totalTimeMetric = benchmark(begin: Benchmark.Duration(id: "convert-total-time"))

// While running this method keep the `isPerforming` flag up.
isPerforming.sync({ $0 = true })
willPerformFuture?()
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we ever use willPerformFuture ? Or didPerformFuture ? Can we remove them?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Only in tests. PreviewActionIntegrationTests.testCancelsConversion() sets a willPerformFuture that sleeps to artificially slow down the conversation time to a controllable number to give the test time to modify a monitored file to trigger another conversion which cancels the previous one.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Only willPerformFuture was ever used so I renamed it to _extraTestWork to better indicate what it is and removed didPerformFuture.

@d-ronnqvist
Copy link
Contributor Author

Are there any implications for switching to a async functions throughout the CLI and top level action structs?

Not really since they're top-level. The implication of switching to asynchronous functions is that they can only be called from other asynchronous functions (or main). This implication can be seen in all the tests that needed to add await.

This problem is illustrated in the first half of What Color is Your Function?. Since asynchronous functions are able to call both asynchronous and synchronous functions but synchronous functions are only able call other synchronous functions, asynchronous functions spread outwards. Because of this, it's sometimes recommended to make updates like this from the outside it.

Is there a way we can refactor the code in DocumentationContext.register to use async notation to manage the various discovery tasks, instead of using DispatchGroup and DispatchQueue?

Yes, but it's out of scope for this PR.

Using asynchronous functions in DocumentationContext.register would require us to make it an asynchronous function which would mean that the places that its callers (DocumentationContext.init(dataProvider:diagnosticEngine:configuration:) and DocumentationContextDataProviderDelegate.dataProvider(_:didAddBundle:)) would need to become asynchronous functions which would be source breaking changes (since the caller would need to await their results and the caller would need to become asynchronous as well).

In the fourth PR in this series—or possibly fifth PR depending on how I split up the changes—I plan on deprecating both those callers and introducing a new context initializer which will be asynchronous even though it only calls synchronous code. This will open up to a future where we can make DocumentationContext.register asynchronous, either by waiting until we can remove the deprecated synchronous callers or by maintaining two separate versions (one using DispatchGroup and one using Task and async/await)

@d-ronnqvist
Copy link
Contributor Author

@swift-ci please test

@d-ronnqvist
Copy link
Contributor Author

@swift-ci please test

@d-ronnqvist
Copy link
Contributor Author

I noticed that the updated tests were flaky because of their use of wait(for:timeout:enforceOrder:) instead of the Concurrency safe alternative fulfillment(of:timeout:enforceOrder:).

While I was investigating that I also found and fixed a couple of memory leaks.

@d-ronnqvist
Copy link
Contributor Author

@swift-ci please test

@d-ronnqvist d-ronnqvist merged commit 1c05ea6 into swiftlang:main Oct 11, 2024
2 checks passed
@d-ronnqvist d-ronnqvist deleted the async-commands branch October 11, 2024 14:09
@patshaughnessy
Copy link
Contributor

In the fourth PR in this series—or possibly fifth PR depending on how I split up the changes—I plan on deprecating both those callers and introducing a new context initializer which will be asynchronous even though it only calls synchronous code.

This sounds great - looking forward to seeing that!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
code cleanup Code cleanup *without* user facing changes
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants