Thrift Eio Flow transport - suggestions?

I am exploring using Thrift with Eio. Currently, the Thrift support library for OCaml uses plain old Unix sockets. But Thrift is fairly flexible in how the low-level networking is done, and abstracts away the details behind a ‘transport’ interface. The interface however does not use async I/O types, so before Eio it was not possible to adapt it to use Lwt:

class virtual t =
  object (self)
    method virtual isOpen : bool
    method virtual opn : unit
    method virtual close : unit
    method virtual read : bytes -> int -> int -> int
    method readAll (buf:bytes) off len = (* this is implemented *)
    method virtual write : bytes -> int -> int -> unit
    method virtual write_string : string -> int -> int -> unit
    method virtual flush : unit
  end

Thanks to Eio though, we can implement this virtual class to use a flow:

(* TFlowTransport.ml *)

open Thrift
module T = Transport

class t flow = object
  val mutable opened = true
  inherit Transport.t

  method isOpen = opened
  method opn = ()

  method close =
    Eio.Net.close flow;
    opened <- false

  method read buf off len =
    let cstruct = Cstruct.create len in
    try
      Eio.Flow.read_exact flow cstruct;
      Cstruct.blit_to_bytes cstruct 0 buf off len;
      len
    with
    | End_of_file ->
      raise (T.E (T.END_OF_FILE, (Printf.sprintf "TSocket: Could not read %d because: end of file" len)))
    | _ -> raise (T.E (T.UNKNOWN, (Printf.sprintf "TSocket: Could not read %d" len)))

  method write buf off len =
    let cstruct = Cstruct.of_bytes ~off ~len buf in
    Eio.Flow.write flow [cstruct]

  method write_string buf off len =
    let cstruct = Cstruct.of_string ~off ~len buf in
    Eio.Flow.write flow [cstruct]

  method flush = ()
end

But as you can see above, reading from/writing to the flow is a little clumsy because flows don’t work directly in terms of byte buffers with offsets and lengths, they work in terms of cstructs. Can anyone advise whether I am missing something or if allocating cstructs is really the way to implement these methods using Eio?

2 Likes

I don’t think there’s much you can do at the moment, but see this issue for a discussion on Cstruct vs. Bytes Decide on cstruct vs bytes · Issue #140 · ocaml-multicore/eio · GitHub. I think there’s a desire to move to bytes if the GC-story works out nicely, but I’m not an expert on that.

2 Likes

Unrelated to the EIO usage, what is the reason for wanting to use Thrift compared to something else?

I have existing Thrift services running in production and I want to interact with them/extend them.

However I think Thrift compares pretty favourably to many other options–supports a large number of languages, solid story for schema evolution, elegant and decoupled design. Sure, it has some warts and performance-wise maybe not at the top. But with RPC systems the thing is that nobody is totally satisfied with any of them :slightly_smiling_face:

1 Like

Cool good to know. It wasn’t clear that project was active or being used by anyone.
I’ve had a PR open against it since Feb to fix the mutable string/bytes warnings. Add builds for 4.13, 4.14 and 5.0 by tmcgilchrist · Pull Request #7 · vbmithr/ocaml-thrift-lib · GitHub

1 Like

I am also interested in this question. I have a lot of code that uses formatter, and I am doing something similar to @yawaramin in wrapping a flow in a formatter, but there is a mismatch between the models.

let formatter_of_flow flow =
  let out buf off len = 
    let cstruct = Cstruct.of_string ~off ~len buf in
    Eio.Flow.write flow [cstruct]
  in 
  let flush () = () in
  Format.make_formatter out flush 

I also noticed that writing to one of these formatters constructed from a flow is significantly slower than writing to a formatter constructed from an out_channel. Are there any suggestions, or should I hold off on using Eio for this?

out_channel is buffered, whereas a flow typically isn’t. You could try something like this:

let formatter_of_writer w =
  let out buf off len =
    Eio.Buf_write.string w buf ~off ~len
  in 
  let flush () = () in
  Format.make_formatter out flush
Eio.Buf_write.with_flow flow @@ fun b ->
let f = formatter_of_writer b in
...
2 Likes

@talex5 Thanks for this suggestion, that is a bit better performing.