I've been reading the section on framing in the tokio tutorial. In it, a trial-and-error technique is described to determine when a new Redis protocol frame has arrived. In other words, code checks if the data that has arrived so far constitutes a full protocol frame and processes it if so. If the frame is not complete, it'll repeat the checking process once more data has arrived. Depending on the packetization and buffering performed by lower layers this may add overhead (the amount of which is subject to debate for the common case.)
By contrast, in event-based programming we typically use a state machine so that protocol processing can advance with a single pass over the data received so far, see, for instance, the http parser used in nginx. In this style, once more data is received, the protocol state machine will continue in the middle of a frame where it left off rather than repeating the process of trying to find frame boundaries.
Why is the tokio mini-redis sample application in the tutorial not written in a similar event-based style? It would be my intuition that Rust's async support would allow that, in theory at least, are there other reasons for why this wouldn't work?
PS: a preliminary look at Hyper (the http engine that's recommended(?) with tokio) shows that it adopts the same approach.
Ok, but hyper (which aims to be production ready?) uses the same approach.
In my experience, the performance edge nginx has over non-event-based servers stems to a significant part not only from its adoption of event-based I/O but comes from maintaining a low-level state machine.
Rust, to my knowledge, is the first compiled language that provides support for async (I may be wrong here in terms of timing - perhaps the second after Microsoft's C++?) - so wouldn't it be interesting to see how much overhead the compiler-generated state machine that async produces adds and whether it's competitive with a hand-written state machine like nginx's? Are you aware of anyone who's done that/is doing that in async Rust?
Yes. Basically, you would have an async fn get_byte() that returns the next byte from the input stream. If any bytes are already buffered, the next byte is returned immediately, otherwise it'll do an asynchronous buffered read on the underlying stream.
In this case, the parser's state would be kept in the local variables on the call stack of the suspended async functions and the parser could be written without regard to the buffering/packetization of the underlying layers in a nonblocking fashion. In theory, at least.