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

feat: native HTTP bindings #9935

Merged
merged 5 commits into from
Apr 8, 2021
Merged

Conversation

bartlomieju
Copy link
Member

@bartlomieju bartlomieju commented Mar 30, 2021

  • streaming response
  • streaming request
  • typescript declarations
  • be able to wrap TLS stream

Towards #3995

op_crates/http/lib.rs Outdated Show resolved Hide resolved
core/core.js Outdated Show resolved Hide resolved
@ry ry force-pushed the feat_hyper_bindings branch 2 times, most recently from abc88b3 to 6af6480 Compare April 2, 2021 16:38
cli/bench/deno_http.js Outdated Show resolved Hide resolved
cli/bench/deno_http.js Outdated Show resolved Hide resolved
cli/bench/deno_http.js Outdated Show resolved Hide resolved
cli/tests/unit/http_test.ts Outdated Show resolved Hide resolved
op_crates/http/00_http.js Outdated Show resolved Hide resolved
op_crates/http/lib.rs Outdated Show resolved Hide resolved
@bartlomieju bartlomieju force-pushed the feat_hyper_bindings branch 2 times, most recently from 5a4a122 to 936af2a Compare April 7, 2021 15:05
* const requests = await Deno.startHttp(conn);
* ```
*/
export function startHttp(conn: Conn): AsyncIterableIterator<HttpEvent>;
Copy link
Collaborator

Choose a reason for hiding this comment

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

We should downgrade this return type to AsyncIterable<HttpEvent>, and now or in the future upgrade that to HttpConn extends AsyncIterable<HttpEvent> which exposes .rid and .close() if we want those things.

Returning or extending AsyncIterableIterator<_> (and overloading .return() for closing) isn't the way to go. I pushed against that for Listener early on, but unfortunately watchFs() slipped through and demonstrated this. I've been meaning to fix that for 2.0.

Copy link
Member

Choose a reason for hiding this comment

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

Returning or extending AsyncIterableIterator<_> (and overloading .return() for closing) isn't the way to go

What's your reasoning behind this?

Copy link
Collaborator

@nayeemrmn nayeemrmn Apr 7, 2021

Choose a reason for hiding this comment

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

For starters .return() is optional on AsyncIterableIterator: https://github.com/denoland/deno/blob/main/cli/tests/unit/fs_events_test.ts#L74. It doesn't align with the default implementation of .return() on generators, which is supposed to break any pending loops without error. I don't think this is supposed to be manually implemented. It's async for no reason, but closing should always be synchronous.

The iterator protocol methods like .next() aren't a nice thing to directly expose on structures with other things, it would be better to have just [Symbol.asyncIterator]() and maybe add an accept() method to await one request like with Listener. However we don't actually need to adjust the implementation from what it is now (other than removing .return()) since it's probably more efficient -- that would also match Listener.

Copy link
Member

Choose a reason for hiding this comment

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

Ok. I have changed this around so that there is no return method and that the requests iterator must be closed manually. Is this what you had in mind?

Copy link
Collaborator

@nayeemrmn nayeemrmn Apr 8, 2021

Choose a reason for hiding this comment

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

and that the requests iterator must be closed manually

The resource should still be closed automatically if connectionClosed is true, if I seemed to have suggested otherwise. (re 336728c#diff-2b90f55ce18f5dd4bac8df7c9f0ff3a77558866bc8fcf02146dee2548b4948abL50-R54)

runtime/js/40_http.js Outdated Show resolved Hide resolved
@ry ry changed the title [WIP] feat: native HTTP bindings feat: native HTTP bindings Apr 7, 2021
runtime/js/40_http.js Outdated Show resolved Hide resolved
@ry ry mentioned this pull request Apr 8, 2021
22 tasks
Copy link
Collaborator

@nayeemrmn nayeemrmn left a comment

Choose a reason for hiding this comment

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

This is how to write the rewrite the .next() calls with AsyncIterable<_>. But to have a proper API for receiving one request, I suggest adding an equivalent of Listener.accept().

respondWith(r: Response | Promise<Response>): void;
}

export interface HttpConn extends AsyncIterableIterator<RequestEvent> {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
export interface HttpConn extends AsyncIterableIterator<RequestEvent> {
export interface HttpConn extends AsyncIterable<RequestEvent> {

const listener = Deno.listen({ port: 4501 });
const conn = await listener.accept();
const requests = Deno.startHttp(conn);
const { value: { request, respondWith }, done } = await requests.next();
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
const { value: { request, respondWith }, done } = await requests.next();
const { value: { request, respondWith }, done } = await requests[Symbol.asyncIterator]().next();

const listener = Deno.listen({ port: 4501 });
const conn = await listener.accept();
const requests = Deno.startHttp(conn);
const { value: { request, respondWith } } = await requests.next();
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
const { value: { request, respondWith } } = await requests.next();
const { value: { request, respondWith } } = await requests[Symbol.asyncIterator]().next();

// TODO(ry) If we don't call requests.next() here we get "error sending
// request for url (https://localhost:4501/): connection closed before
// message completed".
const { done } = await requests.next();
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
const { done } = await requests.next();
const { done } = await requests[Symbol.asyncIterator]().next();

const listener = Deno.listen({ port: 4501 });
const conn = await listener.accept();
const requests = Deno.startHttp(conn);
const { done } = await requests.next();
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
const { done } = await requests.next();
const { done } = await requests[Symbol.asyncIterator]().next();

});
const conn = await listener.accept();
const requests = Deno.startHttp(conn);
const { value: { request, respondWith } } = await requests.next();
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
const { value: { request, respondWith } } = await requests.next();
const { value: { request, respondWith } } = await requests[Symbol.asyncIterator]().next();

// TODO(ry) If we don't call requests.next() here we get "error sending
// request for url (https://localhost:4501/): connection closed before
// message completed".
const { done } = await requests.next();
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
const { done } = await requests.next();
const { done } = await requests[Symbol.asyncIterator]().next();

@ry
Copy link
Member

ry commented Apr 8, 2021

@nayeemrmn That API seems rather annoying. Why shouldn't it be an AsyncIterableIterator?

@lucacasonato
Copy link
Member

lucacasonato commented Apr 8, 2021

I think generally nayeem is on the right path. It makes sense to me if the API looked like this:

interface HttpConn extends AsyncIterable<RequestEvent> {
    readonly rid: number;

    next(): Promise<RequestEvent | null>;
    close(): void;
}

@lucacasonato
Copy link
Member

lucacasonato commented Apr 8, 2021

next returning null could be controversial. It would replace the BadResource error for the next call. We could also do the same as listener though, where we actually throw the BadResource error. Returning null would align to how Stream#next returns an Option in Rust though.

Co-authered-by: Luca Casonato <[email protected]>
Co-authered-by: Ben Noordhuis <[email protected]>
Co-authered-by: Ryan Dahl <[email protected]>
@nayeemrmn
Copy link
Collaborator

I'm in favour of the BadResource error over being nullable. If you're trying to "receive one", you probably have an assertion that you'll get it and a possible error aligns more with that. It should be a more direct implementation in terms of calling the op. The verbose iterator protocol is still accessible through httpConn[Symbol.asyncIterator]() so the nice API should go in the opposite direction. Plus it's more consistent for now.

interface HttpConn extends AsyncIterable<RequestEvent> {
    readonly rid: number;

    accept(): Promise<RequestEvent>;
    close(): void;
}

Copy link
Member Author

@bartlomieju bartlomieju left a comment

Choose a reason for hiding this comment

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

Ship it 🚀

Comment on lines +77 to +81
if self.inner.borrow().is_some() {
Poll::Pending
} else {
Poll::Ready(Ok(()))
}
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 believe is Poll::Pending is returned this task will never be woken up again

Comment on lines +99 to +113
function readRequest(requestRid, zeroCopyBuf) {
return Deno.core.jsonOpAsync(
"op_http_request_read",
requestRid,
zeroCopyBuf,
);
}

function respond(responseSenderRid, resp, zeroCopyBuf) {
return Deno.core.jsonOpSync("op_http_response", [
responseSenderRid,
resp.status ?? 200,
flatEntries(resp.headers ?? {}),
], zeroCopyBuf);
}
Copy link
Member Author

Choose a reason for hiding this comment

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

For a follow up: these two functions seem superfluous

Copy link
Member

@ry ry left a comment

Choose a reason for hiding this comment

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

LGTM!

Although this looks like one large PR, this is actually the cumulation of months of attempts at building a binding to hyper. @bartlomieju drove this to completion but various proof-of-concepts were done by @bnoordhuis and @lucacasonato. The binding relies heavily on the serde_v8 optimization work that @AaronO has been doing - this would have been a much larger and slower undertaking without serde_v8.

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.

6 participants