Skip to content

async/await execute #524

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 12 commits into from
Dec 14, 2021
Merged

Conversation

dnadoba
Copy link
Collaborator

@dnadoba dnadoba commented Dec 13, 2021

This is the last missing piece to actually execute a HTTPClientRequest and await a HTTPClientResponse.
It brings everything together but first preparing the HTTPClientRequest and then executing the Transaction using the HTTPConnectionPool.Manager. We optional follow redirects if the configurations allows it and the HTTPClientRequest.Body can be consumed multiple times.

Comment on lines 53 to 63
guard
preparedRequest.body.canBeConsumedMultipleTimes,
let redirectState = redirectState,
let redirectURL = response.headers.extractRedirectTarget(
status: response.status,
originalURL: preparedRequest.url,
originalScheme: preparedRequest.poolKey.scheme
)
else {
return response
}
Copy link
Member

Choose a reason for hiding this comment

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

I think this would be easier to read as an if.

private func followRedirect(
redirectURL: URL,
redirectState: RedirectState,
request: HTTPClientRequest.Prepared,
Copy link
Member

Choose a reason for hiding this comment

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

the naming of request feels ambiguous here.

Comment on lines 85 to 96
let (method, headers, body) = transformRequestForRedirect(
from: request.url,
method: request.head.method,
headers: request.head.headers,
body: request.body,
to: redirectURL,
status: response.status
)
var newRequest = HTTPClientRequest(url: redirectURL.absoluteString)
newRequest.method = method
newRequest.headers = headers
newRequest.body = body
Copy link
Member

@fabianfett fabianfett Dec 13, 2021

Choose a reason for hiding this comment

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

I think this would look much nicer here and would make the code more readable

var newRequest = originalRequest.followingRedirect(to: redirectURL.absoluteString, redirectStatus: response.status)

Comment on lines 108 to 86
/// There is currently no good way to asynchronously cancel an object that is initiated inside the `body` closure of `with*Continuation`.
/// As a workaround we use `TransactionCancelHandler` which will take care of the race between instantiation of `Transaction`
/// in the `body` closure and cancelation from the `onCancel` closure of `with*Continuation`.
actor TransactionCancelHandler {
private enum State {
case initialised
case register(Transaction)
case cancelled
}

private var state: State = .initialised

init() {}

func registerTransaction(_ transaction: Transaction) {
switch self.state {
case .initialised:
self.state = .register(transaction)
case .cancelled:
transaction.cancel()
case .register:
preconditionFailure("transaction already set")
}
}

func cancel() {
switch self.state {
case .register(let bag):
self.state = .cancelled
bag.cancel()
case .cancelled:
break
case .initialised:
self.state = .cancelled
}
}
}
Copy link
Member

Choose a reason for hiding this comment

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

I think we should probably pull this one out of this function... just put it on the file end with private access.

responseContinuation: continuation
)

_Concurrency.Task {
Copy link
Member

Choose a reason for hiding this comment

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

Comment re naming conflict on Task.

@dnadoba dnadoba force-pushed the dn-async-await-execute branch from 6ad7bbc to 9568ecb Compare December 13, 2021 18:19
@dnadoba dnadoba added the 🔨 semver/patch No public API change. label Dec 14, 2021
@fabianfett fabianfett requested a review from Lukasa December 14, 2021 08:08
redirectState: RedirectState?
) async throws -> HTTPClientResponse {
let preparedRequest = try HTTPClientRequest.Prepared(request)
let response = try await executeWithoutFollowingRedirects(preparedRequest, deadline: deadline, logger: logger)
Copy link
Member

Choose a reason for hiding this comment

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

This method has nothing really to do with followingRedirects, but cancellation is its feature. WDYT about executeCancellable?

status: response.status,
originalURL: preparedRequest.url,
originalScheme: preparedRequest.poolKey.scheme
) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

This conditional is very hard to read. Can we split this into multiple lines with temporary variables to make it clearer?

Additionally, it's not clear that this logic is right. Not all redirects require re-consuming the body: for example, a 302 on POST converts to GET and drops the body, which is intentional.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good catch! We now check if the body of the new request can be send multiple times instead of looking at the original body. If we drop the body for the new request, the body will be nil and canBeConsumedMultipleTimes will return true.

var redirectState = redirectState
try redirectState.redirect(to: redirectURL.absoluteString)

return try await self.executeAndFollowRedirectsIfNeeded(
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can I propose that we don't recurse through our redirects? Constructing a large call stack here is unnecessary, and it would be much better if we could iterate through them instead.

}, onCancel: {
// `HTTPClient.Task` conflicts with Swift Concurrency Task and `Swift.Task` doesn't work
_Concurrency.Task {
await cancelHandler.cancel()
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do we need cancel to be awaitable? Constructing these child tasks is a bit awkward.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We don't need it to be awaitable but we use an actor as our synchronisation primitive so all methods are automatically async/awaitable from outside.
I see two options:

  1. We could make the current cancel() method private (and rename it) and add a new nonisolated cancel() method which would internal create the Task and call the private cancel() method.
  2. We could also rewrite TransactionCancelHandler to use a lock instead and make it a class.

WDYT?

Copy link
Collaborator

Choose a reason for hiding this comment

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

We can leave it as-is, I guess. I'm always a bit nervous about creating lots of tasks but we can always change it later.

@dnadoba dnadoba force-pushed the dn-async-await-execute branch from dfc66a1 to 0b4b559 Compare December 14, 2021 14:56
@dnadoba
Copy link
Collaborator Author

dnadoba commented Dec 14, 2021

@swift-server-bot test this please

@dnadoba dnadoba requested review from Lukasa and fabianfett December 14, 2021 15:46
Copy link
Member

@fabianfett fabianfett left a comment

Choose a reason for hiding this comment

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

Awesome! Thanks!

@dnadoba dnadoba force-pushed the dn-async-await-execute branch from 011ab0c to d0b8ac7 Compare December 14, 2021 19:11
@dnadoba dnadoba merged commit 5db7719 into swift-server:main Dec 14, 2021
@dnadoba dnadoba deleted the dn-async-await-execute branch December 14, 2021 19:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🔨 semver/patch No public API change.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants