Skip to content

Add HTTPScheduledRequest and HTTPExecutingRequest #384

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 3 commits into from
Jul 7, 2021

Conversation

fabianfett
Copy link
Member

@fabianfett fabianfett commented Jun 28, 2021

Motivation

  • In the future we don't want to modify the ChannelPipeline for each request.
  • Our current API supports generic Response types: HTTPClient.Task<Response>.
    • For this reason our current TaskHandler<Delegate: HTTPClientResponseDelegate> is generic over the request's delegate.
  • If we don't want to modify our ChannelPipeline for each request in the future we need remove the generic response types somehow. The only way to achieve this is by boxing our current API in a protocol, that we use as an existential.

Changes

  • Introducing three protocols:
    • HTTPClientTask (I hate this name. Suggestions are highly welcome. Especially since the potential conflict with the current type HTTPClient.Task<Response>): An abstraction of a request, that handles back-pressure for sending the request body as well as receiving the response body.
    • HTTPTaskExecutor a protocol that can be used from the HTTPClientTask abstracting away functionality that will normally be implemented by a ChannelHandler
    • HTTPTaskQueuer a protocol that can be used from the HTTPClientTask abstracting away functionality that will normally be implemented by a ConnectionPool
  • An implementation of the HTTPClientTask called RequestBag. This shows how we can support our current API, with the new HTTPClientTask protocol

Notes for reviewers:

  • We should first agree on the three new protocols and then progress to the RequestBag implementation. I propose those changes not as two PRs to show the usage of those protocols right away.
  • This code is not triggered by any existing component. It purely exist to allow follow up prs and will enabled eventually.
  • For a new async/await compatible API, we would add a new implement that conforms to HTTPClientTask

@fabianfett fabianfett force-pushed the ff-HTTPClientTask branch 2 times, most recently from 3872998 to 461ab39 Compare June 28, 2021 11:37
@fabianfett fabianfett added this to the HTTP/2 support milestone Jun 28, 2021
@fabianfett fabianfett changed the title Add HTTPClientTask Add HTTPScheduledRequest and HTTPExecutingRequest Jun 29, 2021
func requestWasQueued(_ queuer: HTTPRequestScheduler) {
self.stateLock.withLock {
guard case .initialized = self._state else {
// There might be a race between `requestWasQueued` and `willExecuteRequest`. For
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why might there be a race?

case .initialized:
self._state = .executing(writer, .initialized, .initialized)
return true
case .queued:
Copy link
Collaborator

Choose a reason for hiding this comment

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

This can be coalesced with the state above.

}

func didSendRequestPart(_ part: IOData) {
guard self.task.eventLoop.inEventLoop else {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Same here.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Still not done.

}

func didSendRequest() {
guard self.task.eventLoop.inEventLoop else {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Same here.

func failTask(_ error: Error) {
self.task.promise.fail(error)

guard self.task.eventLoop.inEventLoop else {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Same here.

}
}

self.delegate.didReceiveError(task: self.task, error)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do you want to fail the promise first, or after this?

@fabianfett fabianfett force-pushed the ff-HTTPClientTask branch 4 times, most recently from d502d6f to 2a16ade Compare July 1, 2021 10:29
@fabianfett fabianfett requested review from glbrntt and Lukasa July 1, 2021 10:29
/// after `requestHeadSent` was called.
var requestHead: HTTPRequestHead { get }

/// The maximal `TimeAmount` that is allowed to pass between reads from the Channel.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Presumably fireChannelRead events rather than issuing read events?

/// This will be called on the Channel's EventLoop. Do **not block** during your execution!
///
/// - Returns: A bool indicating if the request should really be started. Return false if the request has already been cancelled.
/// If the request is cancelled after this method call `executor.cancel()` to stop request execution.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Who's responsible for calling executor.cancel()? It seems like the caller is responsible, if so I'm not sure this comment belongs here.

Copy link
Member Author

Choose a reason for hiding this comment

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

I tried to be more specific.

/// Resume request streaming
///
/// This will be called on the Channel's EventLoop. Do **not block** during your execution!
func resumeRequestBodyStream()
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why do we need to distinguish between start and resume here? Does it make a difference to the implementor?

/// This will be called on the Channel's EventLoop. Do **not block** during your execution!
func requestHeadSent(_: HTTPRequestHead)

/// Start request streaming
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should we document when we expect these to be called (and how often). I.e. can the implementor safely assume that startRequestBodyStream() will be called at most once?

}

mutating func writeNextRequestPart(_ part: IOData, taskEventLoop: EventLoop) -> WriteAction {
// this method is invoked with bodyPart and returns a future that signals that
Copy link
Collaborator

Choose a reason for hiding this comment

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

this seems out of place.

return .consume(byteBuffer)
}

// buffer is empty, wait for more
Copy link
Collaborator

Choose a reason for hiding this comment

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

Doesn't seem like we're waiting for more!

@fabianfett fabianfett force-pushed the ff-HTTPClientTask branch 2 times, most recently from 73e703b to dd8ec55 Compare July 5, 2021 12:28
@fabianfett fabianfett requested a review from glbrntt July 5, 2021 12:28
@fabianfett fabianfett force-pushed the ff-HTTPClientTask branch 3 times, most recently from 2d7bbce to 6f45596 Compare July 6, 2021 08:52
func receiveResponseEnd()

func fail(_ error: Error)
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: these should be documented too


// MARK: - Request -

private func willExecuteRequest0(_ executor: HTTPRequestExecutor) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should we assert we're on the event loop here?

// MARK: - Request -

private func willExecuteRequest0(_ executor: HTTPRequestExecutor) {
guard self.state.willExecuteRequest(executor) else {
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: a guard only for the else feels like an anti-pattern, if !self.state.willExecuteRequest(executor) { ... } seems more idiomatic.

guard forwardToDelegate else { return }

self.delegate.didReceiveHead(task: self.task, head)
.hop(to: self.task.eventLoop)
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think hop(to:) allocates if you're not already on the correct event loop, a consumeMoreBodyData which did the if-in-event-loop-else dance for us might help save some allocations.

Copy link
Member Author

Choose a reason for hiding this comment

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

Chatted about offline: A if-in-event-loop-else dance™️ also incurs an allocation, if we are not on the correct eventLoop. The closure context allocates.

Comment on lines 242 to 246
case .failure(let error):
self.fail(error)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why are we failing here?

@fabianfett fabianfett requested a review from glbrntt July 6, 2021 14:25
@fabianfett fabianfett added the 🆕 semver/minor Adds new public API. label Jul 6, 2021
@fabianfett
Copy link
Member Author

Adds a new error type.

@fabianfett fabianfett force-pushed the ff-HTTPClientTask branch from b595e3b to 88ceeb5 Compare July 6, 2021 16:16
Copy link
Collaborator

@glbrntt glbrntt left a comment

Choose a reason for hiding this comment

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

LGTM

}
}

mutating func receiveResponseHead(_ head: HTTPResponseHead) -> Bool {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nit: document what the return value represents

Copy link
Collaborator

@Lukasa Lukasa left a comment

Choose a reason for hiding this comment

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

You love to see it.

@fabianfett
Copy link
Member Author

CI failure because of known flaky test #371.

@fabianfett fabianfett force-pushed the ff-HTTPClientTask branch from f551059 to d4f640f Compare July 7, 2021 11:32
@fabianfett fabianfett merged commit a9aee26 into swift-server:main Jul 7, 2021
@fabianfett fabianfett deleted the ff-HTTPClientTask branch July 10, 2021 19:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🆕 semver/minor Adds new public API.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants