[ANN] shuttle v0.3.1 released

Hi!,

I’d like to announce the release of version 0.3.1 of shuttle.

Installation: opam install shuttle

Shuttle provides an API for buffered I/O for applications using async. It fills the same role as the Reader/Writer modules from async, but only supports file descriptors that support non blocking IO. Feature parity with the reader/writer modules is a non-goal.

The library grew out of experiments in replacing manually orchestrated buffer management in some of my older async based applications. The goal is to have a high level api that gives a similar api as reader/writer modules, while providing a little more control over how/when the writes are scheduled. Credits for the idea go to the Janestreet engineers and their implementation of a low latency transport that’s used in async_rpc.

The initial release consists of:

  1. Shuttle → This is the core library that contains the channel implementation
  2. Shuttle_ssl → Encrypted channels using async_ssl
  3. Shuttle_http → Httpaf driver that uses shuttle instead of httpaf-async. Experimental module mostly used for testing, and some performance benchmarks.

Additional Notes:

  • The httpaf driver has been contributed as a candidate for the http benchmarks maintained by the ocaml-multicore project → Multicore OCaml: September 2021, effect handlers will be in OCaml 5.0!
  • For most use-cases people should still default to the Reader/Writer modules from async_unix. They are battle tested and cover a lot more use-cases.
  • Future work will involve a lot more testing, adding a driver for ocaml-tls, and adding minimal windows support mostly to allow at-least being able to build/develop shuttle based libraries on windows.
5 Likes

There have been some significant changes since this last release (Versions 0.4.0 and 0.5.0 are available on opam):

  • Buffered reader has a new utility method that allows reading lines
  • Shuttle now supports file descriptors that don’t support nonblocking I/O. For blocking I/O Shuttle uses async’s support for running syscalls in its thread pool
  • Buffered reader’s api has been simplified to remove read_one_chunk_at_a_time in favor of a more familiar read, read_line etc. refill operation is supported to perform a read syscall to fill a channel’s buffer, and Input_channel.view can be used to get a view into the channel’s underlying buffer.
  • Supports v0.15 series of the janestreet libraries
  • Buffered reader now uses an auto-growing buffer instead of a fixed size buffer than notified users that the internal buffer is full and no progress can be made unless some content is consumed. This should allow starting with a smaller buffer without needing to worry about implementing some client side buffering to hold unconsumed data. Channels allow configuring an upper bound on the internal buffer length, if a buffer grows beyond that an exception is raised.
  • Buffered writer’s support a richer flush interface. Flush operations report errors encountered while attempting to write any pending bytes. This results in a flush operation that returns a deferred that will resolve at some point with either a success or an error, instead of the older flush operation that would return a deferred that never resolved if there was an error during a write syscall.
4 Likes

There’s a new version (0.8.1) of the library published to opam. Some highlights:

Shuttle

  • Buffered reader’s support timeouts for the refill operation. If a file descriptor is currently not ready for a read sys call, the reader will wait for the user-defined upper bound time span and raise an exception indicating a timeout was reached if the underlying file-descriptor isn’t ready for IO within that duration.
  • The buffered reader and writer optionally accept an Async Time_source. They default to wall clock, but this input can be used to provide alternate time sources that match the Time_source interface.

Shuttle_ssl

  • Client and Server SSL connections forward the SSL connection context to the user provided callbacks. This can be useful to access the session certificates for ensuring they meet the user’s expectations.

Shuttle_http

This is a new companion library that implements the HTTP/1.1 server codec. There is more work needed to ensure we cover the HTTP 1.1 spec properly, but this initial release includes a usable implementation of the most common use-cases. The library ships with a hand-rolled parser with decent performance and good test coverage (Thanks to ocaml-afl and afl-fuzz I found some bugs I otherwise wouldn’t have!!), and simple service definitions that can support HTTP keep-alive, fixed length and chunked bodies, streaming support to help work with large bodies, request pipelining and centralized error handling for any unhandled exception in the user handler or the I/O runtime. Most of the implementation of http_async directly within the shuttle project. Future work for the HTTP runtime will happen within shuttle, and http_async will focus on providing a more opinionated higher level interface for writing async http services.

Some additional features that aren’t necessarily related to the HTTP spec include timeout support for request header parsing (this helps shut down clients that are too slow), and a public api that provides enough control to allow scheduling resources cleanup for streaming response bodies (Resources backing a stream will be shutdown when the socket connection closes, either for EOF, user-action, or any unhandled error while the runtime performs i/o).

Examples

open Shuttle_http

let hello_world (request : Request.t) =
  return (Response.create ~body:(Body.string "Hello World") `Ok)
;;
open Shuttle_http

let my_service (ctx : Server.t) (request : Request.t) =
  let%map reader =
    (* This an example to show a stream that works with an external resource. *)
    Reader.open_file "<some file path>"
  in
  (* Create a pipe from the reader that we will use as a streaming response body. *)
  let reader_pipe = Reader.pipe reader in
  (* Register a callback that's called when the server handler is closed that will also
     close the pipe, in-turn closing the Reader and the underlying file descriptor. This
     is useful in scenarios where the connection is interrupted before the response body
     is fully exhausted and helps avoid resource leaks. This example is demonstrating how
     to do this manually using the server handler. Creating a response via
     [Server.respond_stream] will automatically register the action to close a stream on
     Server connection closing. *)
  upon (Server.closed ctx) (fun () -> Pipe.close_read reader_pipe);
  let response_stream = Body.of_pipe `Chunked reader_pipe in
  Response.create ~body:response_stream `Ok
;;

(* The same service written using the utility method for creating streaming responses. *)
let my_service (ctx : Server.t) (request : Request.t) =
  let%map reader = Reader.open_file "<some file path>" in
  let reader_pipe = Reader.pipe reader in
  return (Server.respond_stream ctx (Body.Stream.of_pipe `Chunked reader_pipe))
;;

The library is available via opam. If you try the library and experience any issues, or have further questions, please report an issue on the Github Issue tracker.

2 Likes