Skip to content

Replace Uwt with Luv#539

Merged
djs55 merged 10 commits intomoby:masterfrom
djs55:update-to-luv
Jun 19, 2022
Merged

Replace Uwt with Luv#539
djs55 merged 10 commits intomoby:masterfrom
djs55:update-to-luv

Conversation

@djs55
Copy link
Copy Markdown
Collaborator

@djs55 djs55 commented May 10, 2021

Both Uwt and Luv are bindings to libuv, the scalable IO library used by node.js.

Advantages of Uwt:

  • it is very stable and we've tested it thoroughly in production
  • it exposes an Lwt-based API for promises so works easily with the rest of our code

Disadvantages of Uwt:

Advantages of Luv:

  • it's likely to become the IO foundation for Lwt in future (Luv and Lwt have the same maintainers)
  • it builds with dune so will help us remove Cygwin, fix our Windows build and vendor our dependencies

Disadvantages of Luv:

  • it exposes the raw callback API of libuv so we need to carefully interface with the Lwt event loop
  • we haven't tested it as thoroughly

vpnkit had already separated out IO into a separate module, which was needed to migrate from Lwt_unix to Uwt in the past. However the Luv interface is a thinner layer on top of libuv which means we need to carefully interface the libuv callback-based world with the Lwt promise world. The proposal here is to run Luv on one thread and Lwt on another, and to use Mutex-protected Queues to send work between the two event loops.

A typical function which can be called from Lwt looks like this:

let do_some_io () : unit Lwt.t =
    (* Called from the Lwt universe, which is where the TCP/IP stack runs *)
    Luv_lwt.in_luv (fun return ->
        (* Now we're in the Luv event loop *)
        Luv.Do.something _ (function
            (* libuv calls callbacks when interesting things happen.
               We want to tell the Lwt caller on the other thread.
               The helper function `return` pushes a result on the return queue. *)
            | Error err -> return (Error (`Msg (Luv.Error.strerror err)))
            | Ok () -> return (Ok ())
        )
    )

where Luv_lwt.in_luv pushes the function to a queue and signals the Luv event loop, and then when a return result is finally ready, the function return pushes the value to a queue and signals the Lwt event loop. The queues and signalling is all in a small module Luv_lwt.

The callback code is bit nested in places in this style:

match foo () with
| Error _ ->
| Ok () ->
  match bar () with
  | Error _ ->

see for example binding a UDP socket (you have to expand the collapsed diff of host.ml otherwise it's not visible).

but I thought I'd write everything out before trying to be too clever with combinators.

This was a good opportunity to start writing some inline unit tests to complement the existing end-to-end tests.

It was also a good opportunity to reformat the new code with ocamlformat.

Signed-off-by: David Scott dave@recoil.org

Comment on lines +74 to +76
begin match Connection_limit.register description with
| Error (`Msg m) -> Lwt.fail_with m
| Ok idx ->
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This looks a bit like Lwt_result.get_exn but specialised for the Msg case -- https://ocsigen.org/lwt/latest/api/Lwt_result

Perhaps define a quick operator for that and use it in all the places you do the Error -> Lwt.fail_with conversion?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I added a

let ( >>*= ) m f = m >>= function
  | Error (`Msg m) -> Lwt.fail_with m
  | Ok x -> f x

and used it in a bunch of places.

djs55 added 8 commits June 19, 2022 15:50
Both `Uwt` and `Luv` are bindings to `libuv`. `Uwt` hasn't been worked
on in 3 years see fdopen/uwt#5 whereas `Luv` is
under active development, and will probably become the IO foundation for
`Lwt`, the promises library that we use.

Signed-off-by: David Scott <dave@recoil.org>
Signed-off-by: David Scott <dave@recoil.org>
Signed-off-by: David Scott <dave@recoil.org>
Signed-off-by: David Scott <dave@recoil.org>
Signed-off-by: David Scott <dave@recoil.org>
Signed-off-by: David Scott <dave@recoil.org>
Signed-off-by: David Scott <dave@recoil.org>
Signed-off-by: David Scott <dave@recoil.org>
The unit tests are run with `dune test` and should not need the network,
so should work in sandboxed environments.

The e2e tests are run with `dune build @e2e` and require the network and
the ability to increase resource limits.

Signed-off-by: David Scott <dave@recoil.org>
@djs55 djs55 changed the title WIP: Replace Uwt with Luv Replace Uwt with Luv Jun 19, 2022
Copy link
Copy Markdown
Collaborator

@avsm avsm left a comment

Choose a reason for hiding this comment

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

LGTM; just a minor question about clarifying watch (and the associated Lwt.async when calling it). Will be more important when porting to eio.

type watch

val watch: ?path:string -> unit -> (watch, [ `Msg of string ]) result
val watch: ?path:string -> unit -> (watch, [ `Msg of string ]) result Lwt.t
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Not quite clear what the Lwt.t here does. Does it block until the watching begins, or does it block indefinitely until the watch terminates?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

It blocks until the watching begins. I should clarify that in a doc, as it's not very obvious.

@djs55 djs55 merged commit 6039eac into moby:master Jun 19, 2022
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