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

Try h2 on h1 failure #1503

Merged
merged 1 commit into from
May 10, 2018
Merged

Conversation

estk
Copy link
Contributor

@estk estk commented Apr 27, 2018

Closes #1486

TODO:

  • Unit tests
  • Integration tests
  • Check for h2 preface and bail from h1 early (only check on first message)
  • Cleanup
  • Test hanging?

@estk
Copy link
Contributor Author

estk commented Apr 27, 2018

So, I'm a bit stuck at the moment, my current strategy is to wrap the ServiceFn passed to Http::serve_connection in an Arc so that the h1 Connection can hold a reference to the service in case we want to try an upgrade. The problem is by doing this, somehow NewService is no longer Sync. See the error below for details.
So, that seems fine, but the thing that really has me scratching my head is that the ServiceFn though not in Connection directly before was by proxy since it was owned by the h1::Dispatcher. Why is NewService all of a sudden not Sync now?

I'm fairly new to rust so I feel like I'm a bit out of my depth here, and would really appreciate any help.

$ RUST_LOG=hyper=trace cargo test --test h2
   Compiling hyper v0.12.0-pre.0 (file:///Users/evan.simmons/rust/hyper)
error[E0277]: `<S as service::new_service::NewService>::Service` cannot be shared between threads safely
   --> src/server/conn.rs:536:42
    |
536 |                 self.serve.protocol.exec.execute(fut);
    |                                          ^^^^^^^ `<S as service::new_service::NewService>::Service` cannot be shared between threads safely
    |
    = help: the trait `std::marker::Sync` is not implemented for `<S as service::new_service::NewService>::Service`
    = help: consider adding a `where <S as service::new_service::NewService>::Service: std::marker::Sync` bound
    = note: required because of the requirements on the impl of `std::marker::Send` for `std::sync::Arc<<S as service::new_service::NewService>::Service>`
    = note: required because it appears within the type `server::conn::Connection<<I as futures::Stream>::Item, <S as service::new_service::NewService>::Service>`
    = note: required because it appears within the type `futures::future::chain::Chain<futures::MapErr<server::conn::Connecting<<I as futures::Stream>::Item, <S as service::new_service::NewService>::Future>, fn(<S as service::new_service::NewService>::InitError) -> error::Error {error::Error::new_user_new_service::<<S as service::new_service::NewService>::InitError>}>, server::conn::Connection<<I as futures::Stream>::Item, <S as service::new_service::NewService>::Service>, [closure@src/server/conn.rs:534:31: 534:42]>`
    = note: required because it appears within the type `futures::AndThen<futures::MapErr<server::conn::Connecting<<I as futures::Stream>::Item, <S as service::new_service::NewService>::Future>, fn(<S as service::new_service::NewService>::InitError) -> error::Error {error::Error::new_user_new_service::<<S as service::new_service::NewService>::InitError>}>, server::conn::Connection<<I as futures::Stream>::Item, <S as service::new_service::NewService>::Service>, [closure@src/server/conn.rs:534:31: 534:42]>`
    = note: required because it appears within the type `futures::MapErr<futures::AndThen<futures::MapErr<server::conn::Connecting<<I as futures::Stream>::Item, <S as service::new_service::NewService>::Future>, fn(<S as service::new_service::NewService>::InitError) -> error::Error {error::Error::new_user_new_service::<<S as service::new_service::NewService>::InitError>}>, server::conn::Connection<<I as futures::Stream>::Item, <S as service::new_service::NewService>::Service>, [closure@src/server/conn.rs:534:31: 534:42]>, [closure@src/server/conn.rs:535:30: 535:65]>`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0277`.
error: Could not compile `hyper`.

To learn more, run the command again with --verbose.

@seanmonstar
Copy link
Member

So, that specific error is probably a red herring, but I can explain the problem:

  • There is a bounds that future being executed be Send. This is because the executor could be a thread pool, and so the future might move to a new thread.
  • By wrapping something in Arc, that makes it possible to send copies to different threads at the same time. Both copies would be able to access &T from the Arc<T>.
  • In order for that to be safe, T must be Sync, which states that all accesses of &T are thread-safe.
  • Some examples of non-thread-safe &T access: Rc::clone, Cell, RefCell...
  • Since those types can actually mutate T through a &T, but don't do so with synchronization, they don't implement Sync.
  • Backing out again, that means that if you have an Arc<T>, in order for Arc<T>: Send, the type it is wrapping must be T: Send + Sync.

I don't think you'll need an Arc here at all, but instead should be able to pop the service back out of the h1 dispatcher, just like the IO and read buffer.

@estk
Copy link
Contributor Author

estk commented Apr 28, 2018

Hey Sean,
Thanks for the excellent and speedy explanation. Those were the two solutions that I had considered and am glad to have you weigh in. This issue feels a bit like playing chess with the compiler.. :)

@estk
Copy link
Contributor Author

estk commented Apr 28, 2018

@seanmonstar So, I've now gone down the deconstruction path, and I saw two options:

  1. Implement and into_inner() on the trait.
  2. Wrap the dispatch with an enum such that we can match on if it is a server and return an Option in the Dispatcher::into_inner()

By the looks of it, by wrapping in the enum I'd have to implement the Dispatch trait on it which seems like a lot of boilerplate and hence probably not the right way to go.

After giving the first option a try I find myself again a bit stuck and would love a tip if you have a sec, I'm getting the following compiler message which is a bit ironic since I dont really care what the type is on client since it never returns Some. Is there a way to tell the compiler that this only returns None? If not, is there any way to reconcile with the compiler, or should I just try the enum option?

$ RUST_LOG=hyper=trace cargo test --test h2
   Compiling hyper v0.12.0-pre.0 (file:///Users/evan.simmons/rust/hyper)
error[E0207]: the type parameter `S` is not constrained by the impl trait, self type, or predicates
   --> src/proto/h1/dispatch.rs:396:9
    |
396 | impl<B, S> Dispatch for Client<B>
    |         ^ unconstrained type parameter

error: aborting due to previous error

For more information about this error, try `rustc --explain E0207`.
error: Could not compile `hyper`.

To learn more, run the command again with --verbose.

@seanmonstar
Copy link
Member

You shouldn't need to to much with the Dispatch trait. You already know the exact implementation of dispatch, Dispatch<Server<...>>. So, it should be possible to add an impl block for that type directly.

impl<generics> Dispatch<io, etc, Server<S>> {
    pub fn hello() {}
}

Though, it probably does mean the the server:: Connection needs to hold not a Dispatcher directly, but some sort of enum that can allow taking the dispatcher apart even in a &mut self context. An Option works, or occasionally a new enum can be better, depending on if there's more than 2 states, or the names would be ambigious.

@estk
Copy link
Contributor Author

estk commented Apr 29, 2018

Hokay, stuck again. Current issue is below, my guess is that since we are parameterizing for a specific I, we'll need to actually wrap that with a sum type that contains either I or RewindedAddrStream<I>. (There's definitely a better name for that but I'll leave it for cleanup.)

PS I wrote the RewindedAddrStream because I didn't see a way to rewind io given the type bounds AsyncRead + AsyncWrite. Is that true?

$ RUST_LOG=hyper=trace cargo test --test h2
   Compiling hyper v0.12.0-pre.0 (file:///Users/evan.simmons/rust/hyper)
error[E0308]: mismatched types
   --> src/server/conn.rs:394:43
    |
394 |         self.conn.get_or_insert(Either::B(h2));
    |                                           ^^ expected type parameter, found struct `server::tcp::rewinded_add_stream::RewindedAddrStream`
    |
    = note: expected type `proto::h2::server::Server<I, _, _>`
               found type `proto::h2::server::Server<server::tcp::rewinded_add_stream::RewindedAddrStream<I, std::io::Cursor<bytes::Bytes>>, _, _>`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0308`.
error: Could not compile `hyper`.

To learn more, run the command again with --verbose.

@seanmonstar
Copy link
Member

Thanks for sticking with this! I maybe should have labelled this hard instead of medium, it seems it's more involved than I originally thought XD

Yes, we'd need some sort of wrapper for the generic AsyncRead + AsyncWrite to allow "rewinding" and using the read buffer as the start of the h2 handshake. It should itself be able to be generic, not actually requiring the AddrStream. And if it's built into the internals of the Conn type, it's existence probably doesn't even need to be known. As in, the user would still just see it as Conn<MyIoType, MyService>. Does that make sense? I sometimes don't notice if I glossed over something with not enough detail.

@estk
Copy link
Contributor Author

estk commented May 1, 2018

Haha, thanks Sean, yeah part of this is that I've got about 1 month of rust experience.
I've successfully specialized the type of the AsyncRead + AsyncWrite type param to something that can be rewound. Compile issues seem to be behind me at least at the moment, now I'm just scratching my head about the following issue:

$ RUST_LOG=hyper=trace,h2=trace cargo test --test h2
   Compiling hyper v0.12.0-pre.0 (file:///Users/evan.simmons/rust/hyper)
    Finished dev [unoptimized + debuginfo] target(s) in 5.31 secs
     Running target/debug/deps/h2-ed3b5b7c313e5877

running 1 test
 DEBUG hyper::proto::h1::dispatch > starting poll_catch
 TRACE hyper::proto::h1::conn     > Conn::read_head
 TRACE hyper::proto::h1::conn     > flushed State { reading: Init, writing: Init, keep_alive: Busy, error: None, read_task: None }
 DEBUG hyper::proto::h1::dispatch > done reading flushing
 DEBUG hyper::proto::h1::dispatch > starting poll_catch
 TRACE hyper::proto::h1::conn     > Conn::read_head
 DEBUG hyper::proto::h1::io       > read 103 bytes
 TRACE hyper::proto::h1::role     > Request.parse([Header; 100], [u8; 103])
 TRACE hyper::proto::h1::conn     > State::close_read()
 DEBUG hyper::proto::h1::conn     > parse error (invalid HTTP version specified) with 103 bytes
 DEBUG hyper::proto::h1::dispatch > read_head error: invalid HTTP version specified
 DEBUG hyper::proto::h1::dispatch > or_else of poll_catch
 DEBUG hyper::server::conn        > error polling connection protocol: invalid HTTP version specified
 DEBUG h2::codec::framed_write    > send; frame=Frame::Settings(Settings { flags: SettingsFlags(0), header_table_size: None, enable_push: None, max_concurrent_streams: None, initial_window_size: None, max_frame_size: None, max_header_list_size: None })
 TRACE h2::frame::settings        > encoding SETTINGS; len=0
 TRACE h2::codec::framed_write    > encoded settings; rem=9
 DEBUG hyper::proto::h2::server   > handshaking
 TRACE h2::server                 > Handshake::poll(); state=Handshaking::Flushing(_);
 TRACE h2::codec::framed_write    > flush
 TRACE h2::codec::framed_write    >   -> not a queued data frame
 TRACE h2::codec::framed_write    > flushing buffer
 TRACE h2::server                 > Handshake::poll(); flush.poll()=Ready
 TRACE h2::proto::streams::flow_control > inc_window; sz=65535; old=0; new=65535
 TRACE h2::proto::streams::flow_control > inc_window; sz=65535; old=0; new=65535
 TRACE h2::proto::streams::prioritize   > Prioritize::new; flow=FlowControl { window_size: Window(65535), available: Window(65535) }
 TRACE h2::server                       > Handshake::poll(); connection established!
 TRACE h2::proto::settings              > send_pending_ack; pending=None
 TRACE h2::codec::framed_read           > poll
 DEBUG h2::proto::connection            > Connection::poll; err=FRAME_SIZE_ERROR
 DEBUG h2::codec::framed_write          > send; frame=Frame::GoAway(GoAway { last_stream_id: StreamId(0), error_code: FRAME_SIZE_ERROR })
 TRACE h2::frame::go_away               > encoding GO_AWAY; code=FRAME_SIZE_ERROR
 TRACE h2::codec::framed_write          > encoded go_away; rem=17
 DEBUG h2::proto::connection            > Connection::poll; err=FRAME_SIZE_ERROR
 TRACE h2::proto::connection            >     -> already going away
 TRACE h2::proto::connection            > connection closing after flush, reason=FRAME_SIZE_ERROR
 TRACE h2::codec::framed_write          > flush
 TRACE h2::codec::framed_write          >   -> not a queued data frame
 TRACE h2::codec::framed_write          > flushing buffer
 DEBUG hyper::server::conn              > conn error: http2 general error: protocol error: frame with invalid size

Meanwhile in a separate term:

$ curl -vvv --http2-prior-knowledge localhost:3000
* Rebuilt URL to: localhost:3000/
*   Trying ::1...
* TCP_NODELAY set
* Connection failed
* connect to ::1 port 3000 failed: Connection refused
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 3000 (#0)
* Using HTTP2, server supports multi-use
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* Using Stream ID: 1 (easy handle 0x7fa5b0805800)
> GET / HTTP/2
> Host: localhost:3000
> User-Agent: curl/7.54.0
> Accept: */*
>
* Connection state changed (MAX_CONCURRENT_STREAMS updated)!
* Closing connection 0
curl: (16) Error in the HTTP2 framing layer

I'm thinking ive still dropped some bytes somewhere, so that will be my next investigation, I'm thinking I'll tcpdump the traffic and compare with what is available in the rewound io object that I pass to h2.

@estk
Copy link
Contributor Author

estk commented May 1, 2018

Working, see pr description.

Copy link
Member

@seanmonstar seanmonstar left a comment

Choose a reason for hiding this comment

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

Nice work! I have some thoughts on a slightly altered approach, commented inline.

@@ -82,6 +83,12 @@ pub struct Builder<I> {
protocol: Http_,
}

/// AsyncRewind is a rewindable AsyncRead+AsyncWrite trait
pub trait AsyncRewind: AsyncRead + AsyncWrite {
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 can probably build the rewinding in internal, and not require that ever transport type a user might want to use have to implement this function. Basically, a private struct Rewound<I>. Here's a way I'd do it:

  1. In the h1 case, nothing should be needed there. If there is a parse error, we can just use deconstruct/into_inner to get the original transport and the read_buf back.
  2. If we've done number 1, and want to start the h2 handshake, then a Rewound would be constructed with those 2 pieces, and then Rewound<I> would be the AsyncRead + AsyncWrite type passed to the h2 handshake/connection.

That'd make the server::Connection type look kind of like this internally:

pub struct Connection<I, S> {
    inner: Option<
        Either<
            h1::Dispatcher<..., I>,
            h2::Server<Rewound<I>, ...>,
        >
    >
}

Copy link
Contributor Author

@estk estk May 1, 2018

Choose a reason for hiding this comment

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

I really like this idea, part of the PR cleanup I was planning was to try to internalize (to the api) all of my changes and I think this is a perfect way to do it! The only other thing I was thinking about was peeking at the first few bytes in Connection and checking against h2::PREFACE before even going down the path of either h1 or h2. The reason for taking this approach would be that you would have a clean separation between h1 and h2. The other benefit is that we dont even have to back stuff out of h1 if we check for the preface.
Thoughts?
PS I really appreciate all the mentoring on this one, I feel like I've learned so much about rust ecosystem just on this one pr. 😄

Copy link
Member

Choose a reason for hiding this comment

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

That could work... I'm not sure I've thought through all the ramifications of that. We couldn't actual require MSG_PEEK, since that specific to TCP, and the IO type could be anything. Hopefully, handling of errors is still similar, so that h1 parse errors still send an appropriate HTTP/1 response.

Though, this would solve an issue I hadn't commented about yet, which is that the h2 fallover should only be applicable on the very first request. If the stream has already spoken h1, a later parse error shouldn't try to upgrade to h2.

@@ -545,6 +552,9 @@ where I: AsyncRead + AsyncWrite,
// - Client: there is nothing we can do
// - Server: if Response hasn't been written yet, we can send a 4xx response
fn on_parse_error(&mut self, err: ::Error) -> ::Result<()> {
if self.shutdown_on_error == false {
Copy link
Member

Choose a reason for hiding this comment

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

This could probably be inside T::on_error instead. Hm, it could perhaps be useful if the on_error method were adjusted to allow accessing the current &read_buf. Then, ServerTransaction::on_error could check to see if it at least starts with the HTTP/2 magic prefix before deciding to bubble the error back up (and let server::Connection try h2), or to just return the HTTP/1 bad response.

@estk estk force-pushed the try_h2_on_h1_failure branch 2 times, most recently from 4374a2d to 256ce6d Compare May 3, 2018 20:11
@estk
Copy link
Contributor Author

estk commented May 3, 2018

You were totally right, it looks like it might be a bit of a nightmare to add the checking to the Connection.

@estk estk force-pushed the try_h2_on_h1_failure branch 4 times, most recently from 9ae9c9a to 155507b Compare May 5, 2018 16:59
@estk
Copy link
Contributor Author

estk commented May 5, 2018

@seanmonstar I'm a bit stuck again, it looks to me like things should be working, any idea why the ci seems to only pass on nightly?

@seanmonstar
Copy link
Member

@estk looking at the logs right now, it seems to be complaining about illegal syntax. It looks like a merge conflict was saved, as there's a line like <<<<<<<<<<< HEAD ;)

@estk
Copy link
Contributor Author

estk commented May 5, 2018

Lol, that’s embarrassing, i guess ill blame that on not having coffee yet. The build that i was referring to was the one before. Im mobile now but ill fix it up this evening

Copy link
Contributor Author

@estk estk left a comment

Choose a reason for hiding this comment

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

@seanmonstar, the ci issue I was referring to exists on the current build: https://travis-ci.org/hyperium/hyper/builds/375446742. I'm not really sure what to make of it since it was only on nightly with a specific set of options.

@@ -513,3 +553,165 @@ where
}
}
}

// TODO: different file? idk where to put this.
mod rewindable {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@seanmonstar, it looks like hyper as a project prefers to keep new files to a minimum, is this an appropriate place to put this?

Copy link
Member

Choose a reason for hiding this comment

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

There isn't an enforced preference, really... keeping things in less files came from two reasons: a) it can get annoying to have to import a bunch of the same stuff over and over, and b) sometimes some types make use of private fields of others (though this is less important now that there exists pub(scope)s.

If you think it's clearer in a separate file, that's perfectly fine :)

@@ -39,6 +40,24 @@ fn tcp_bind(addr: &SocketAddr, handle: &Handle) -> ::tokio::io::Result<TcpListen
TcpListener::from_std(std_listener, handle)
}

#[test]
fn try_h2() {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@seanmonstar this is a significant deviation from how other tests are performed, but I'm not really sure how else I would go about it. Any guidance here?

Copy link
Member

Choose a reason for hiding this comment

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

It's fine! I've really wanted to take some time to clean up the tests, they've become a mess. They started with just a server helper, but eventually more specific details needed to be tested, and I never took the time to group up boilerplate.

Copy link
Member

@seanmonstar seanmonstar 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 looking really really great! Thanks for the continued effort on this. I think the comments I've left are pretty minor, and then we can merge!

T: Http1Transaction,
Bs: Payload,
{
pub fn deconstruct(self) -> (I, Bytes, S) {
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 can just be Dispatcher::into_inner, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Absolutely!

}

/// Deconstructed parts of a `Connection`.
///
/// This allows taking apart a `Connection` at a later time, in order to
/// reclaim the IO object, and additional related pieces.
#[derive(Debug)]
pub struct Parts<T, S> {
pub struct Parts<T, S>
where S: Service {
Copy link
Member

Choose a reason for hiding this comment

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

Is this new bounds required?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It seems not, ive removed it.
So I know for the future, Is it a rust thing to not include bounds that will be satisfied but are not required? I would think that since this is an intermediate data structure we would want to be as specific as possible about what it contains for the end user.

Either::A(ref mut h1) => {
try_ready!(h1.poll_without_shutdown());
Ok(().into())
},
Either::B(ref mut h2) => h2.poll(),
}
}

fn try_h2(&mut self) -> Poll<(), ::Error> {
debug!("Trying to upgrade to h2");
Copy link
Member

Choose a reason for hiding this comment

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

May want to demote this to a trace!, and remove the other two debug! logs in this function.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

#[derive(Debug)]
pub struct Rewindable<T>
where
T: AsyncRead + AsyncWrite,
Copy link
Member

Choose a reason for hiding this comment

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

Ah, I see. If these bounds are removed, then they won't be needed in several other places as well. Usually, prefer bounds only on impls that need them (sometimes it's unavoidable in a struct definition, if you need to refer to an associated type).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is a great rust nugget of knowledge, thanks.

}
}
pub fn prepend(&mut self, bs: Bytes) {
self.pre.get_or_insert(bs);
Copy link
Member

Choose a reason for hiding this comment

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

Prefer self.pre = Some(bs) instead, since get_or_insert does some matching on the original field, but we aren't using it.

You could put a debug_assert!(self.pre.is_none()) before setting it, for correctness while tests are run.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done


#[inline]
fn read_buf<B: BufMut>(&mut self, buf: &mut B) -> Poll<usize, io::Error> {
if let Some(ref mut bs) = self.pre {
Copy link
Member

Choose a reason for hiding this comment

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

Perhaps here and in read, the pre should be changed to None once all the bytes have been read. That will mean then skipping the rest of this if block on latter reads, and allow the Bytes to be freed since it's no longer needed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Absolutely, actually I'm not super confident on what is in here, but getting close to something that seems right.

// then we could have two separate code paths where we dont need to stage changes like
// here which adds complexity.
#[inline]
fn read_buf<B: BufMut>(&mut self, buf: &mut B) -> Poll<usize, io::Error> {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@seanmonstar do you know if there are any guarantees around how many bytes should be written onto buf? If there are none we could drastically simplify this code.


let mut o2_cursor = Cursor::new(o2);
stream.read_buf(&mut o2_cursor).unwrap();
stream.read_buf(&mut o2_cursor).unwrap();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@seanmonstar This the crux of what im concerned about, is it okay that we're not reading out all bytes from a perf/correctness point of view?

Copy link
Member

Choose a reason for hiding this comment

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

I don't actually understand the question, or what this test is testing :D

Mind adding some comments about what each part of the test is triggering or why?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Haha sorry, was a bit of a non-question. I think ive answered it myself, I was wondering if there is an undocumented dependency of the read_buf method to read all the bytes possible into the buf in a single call.
How I'm doing it here is one of the following per call:

  • Is there a prefix? Read as many bytes in the prefix as possible out, If there are some left, put those back in the prefix field.
  • If there is not a prefix, read direct from the inner

In practice this means the buf passed in will not always be filled even though there may be bytes ready to read. I can imagine a situation where there is code relying on a small buffer being completely filled which could break.

Copy link
Member

Choose a reason for hiding this comment

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

Oh, I see what you mean. Yea, this behavior is correct. You don't want to copy the buffered bytes, and try to a syscall in the same read. Because: what's the behavior if there is an error from the syscall? Return the error? But then the buffered bytes... what about those? It's messy.

Plus, anyone relying on a read to completely fill a buffer is definitely using IO incorrectly. You never know exactly how much data might be in the kernels buffers, and you don't know if more arrived just as it partially filled your buffer, so you just have to keep reading until your buffer is full, or your get WouldBlock.

if let Some(bs) = self.pre.take() {
// If there are no remaining bytes, let the bytes get dropped.
if bs.len() > 0 {
let mut reader = bs.into_buf().reader();
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 there may be a more idiomatic way to copy between a Buf and a BufMut. I think this may work:

let cnt = buf.remaining_mut();
let mut bs = bs.into_buf().take(cnt);
buf.put(&mut bs);

// bs.into_inner() stuff

It's unfortunate the dance needed for BytesMut. It's already been decided that it was mistake that it doesn't implement Buf itself, but it can't without a breaking change. It will in bytes 0.5.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Excellent info as always.

@estk
Copy link
Contributor Author

estk commented May 9, 2018

@seanmonstar one thing that i cant seem to figure out is why the ci tests fail (but not for every build option) while locally they seem to be fine. Any ideas?

@seanmonstar
Copy link
Member

There seems to be some sort of race condition in h2 that sometimes prevents the client connection from completely closing up, resulting in the test server and client just staring at each other until Travis gives up. 😭

@estk estk force-pushed the try_h2_on_h1_failure branch 2 times, most recently from 9e35904 to 4a006ad Compare May 9, 2018 17:48
@estk estk force-pushed the try_h2_on_h1_failure branch 2 times, most recently from d61e147 to 7d58680 Compare May 10, 2018 04:07
@estk
Copy link
Contributor Author

estk commented May 10, 2018

@seanmonstar whew, it looks like we're approaching the finish line on this one. Any further comments or tests you'd like me to add?

Copy link
Member

@seanmonstar seanmonstar left a comment

Choose a reason for hiding this comment

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

You're a champ! Thanks for the continued effort on this! The comments I left are pretty minor, and I can do them myself in a bit if you're feeling tired ;)

Oh, I just thought of this: do you think we should allow this to be configurable? Should a user be able to turn this off, maybe with like http1_only, similar to how there is a http2_only config? It's not needed here, just crossed my mind.

match self.state.writing {
Writing::Init => {
if self.has_h2_prefix() {
Copy link
Member

Choose a reason for hiding this comment

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

Oh great idea, so that Http1Transaction doesn't need to get more complicated, I like it!

@@ -60,6 +60,7 @@ where
loop {
let next = match self.state {
State::Handshaking(ref mut h) => {
trace!("Polled client future, state is handshaking");
Copy link
Member

Choose a reason for hiding this comment

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

I'd probably remove these traces, I'm sure they were useful developing the feature, but afterwards, debugging other things will likely mean these just feel more like noise.

where
S: Service,
T: AsyncRead + AsyncWrite,
Copy link
Member

Choose a reason for hiding this comment

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

I believe these bounds can be removed from here and the fmt::Debug impl :)

proto::h1::Dispatcher<
proto::h1::dispatch::Server<S>,
S::ResBody,
I,
Rewind<T>,
Copy link
Member

Choose a reason for hiding this comment

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

This is just a thought, feel free to tell me it's crazy: it may be possible to actually just pass T here, and not need Rewind<T> only for h1. It seems like the only time rewind is called is when deconstructing h1 and building an h2 dispatcher, so in try_h2 is where it could be let io = Rewind::new(io); io.rewind(read_buf); .... I have a hunch this will mean that this new feature doesn't affect the performance of HTTP1 at all, since the read calls would never need to check the rewind buffer.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, this is something I thought about, I cant remember why I didn't do this, let me try to look through the code and jog my memory.

@estk estk force-pushed the try_h2_on_h1_failure branch 2 times, most recently from fa05583 to 839d647 Compare May 10, 2018 18:47
@estk
Copy link
Contributor Author

estk commented May 10, 2018

Haha thanks @seanmonstar, this has been an excellent learning experience so I'm not quite ready to give up yet. I've addressed the comments you made today, aside from the .http1only() suggestion, which I'm more happy to implement, but I probably wont have time today.

Unfortunately it seems like I haven't quite fixed the flakey-ness of the CI tests. It really has me baffled since I cannot reproduce it locally and it seems to happen on different rustc's every time. I've even gone to the trouble of running it in a debian docker container to see if I could replicate it there (no luck.) My suspicion is that it is a real issue, in h2 as you said, but without being able to reproduce it locally its very difficult to debug. If you could point me in the right direction there it would be super helpful. Also, if youre ready to just wrap this up yourself feel free, I'm sure I'll learn a ton from the code you add.

@seanmonstar seanmonstar merged commit bc6af88 into hyperium:master May 10, 2018
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