An echo server with Miou

Passer de notre petit scheduler à Miou est assez simple. Mise à part la règle de ne pas oublier ses enfants que nous avons introduit dans le chapitre précédent, il s'agit principalement d'utiliser Miou et Miou_unix. Prennons la fonction echo qui gère nos clients:

Moving from our custom scheduler to Miou is quite straightforward. Apart from the rule of not forgetting our children that we introduced in the previous chapter, it mainly involves using Miou and Miou_unix. Let's take the echo function that handles our clients:

let rec echo client =
  let buf = Bytes.create 0x100 in
  let len = Miou_unix.read client buf ~off:0 ~len:(Bytes.length buf) in
  if len = 0 then Miou_unix.close client
  else
    let str = Bytes.sub_string buf 0 len in
    let _ = Miou_unix.write client str ~off:0 ~len in echo client

A subtlety lies in Miou_unix.write, which expects a string instead of bytes. In a concurrent execution, it may happen that buf could be modified concurrently. Using a string ensures that the buffer we want to transmit does not change in the meantime. Once again, this comes from our experience in protocol implementation.

Then, according to the rules introduced in the previous chapter, we need to re-implement our server so that it uses an orphans value and periodically cleans up our terminated clients to avoid forgetting our children:

let clean_up orphans = match Miou.care orphans with
  | None | Some None -> ()
  | Some (Some prm) -> match Miou.await prm with
    | Ok () -> ()
    | Error exn -> raise exn

let server () =
  let socket = Miou_unix.tcpv4 () in
  let sockaddr = Unix.ADDR_INET (Unix.inet_addr_loopback, 3000) in
  Miou_unix.bind_and_listen socket sockaddr;
  let orphans = Miou.orphans () in
  while true do
    clean_up orphans;
    let client, _ = Miou_unix.accept socket in
    ignore (Miou.async ~orphans (fun () -> echo client))
  done;
  Miou_unix.close socket

let () = Miou_unix.run server

Note the use of Miou_unix.run to handle system events and the functions from this module. For more details, we will explain the interactions between the system and Miou more precisely in the next chapter.

And there we go, we've switched to Miou! All we need to do is compile our project like this:

$ opam install miou
$ ocamlfind opt -linkpkg -package miou,miou.unix main.ml
$ ./a.out &
[1] 436169
$ echo "Hello" | netcat -q0 localhost 3000
Hello
$ kill -9 436169
[1]  + 436169 killed     ./a.out

Parallelism

Since it's now possible to utilise multiple domains, let's take advantage of this to instantiate more than one server. Indeed, it's conceivable that multiple servers could exist at the same address (localhost:3000). In such a scenario, it's first come, first served. Therefore, we can envision managing multiple servers in parallel, each handling several clients concurrently.

To distribute the implementation of our server across multiple domains, we'll use Miou.parallel. We won't forget to involve dom0 (referring to our rule where dom0 would never be assigned a task from other domain) via Miou.async:

let () = Miou_unix.run @@ fun () ->
  let domains = Stdlib.Domain.recommended_domain_count () - 1 in
  let domains = List.init domains (Fun.const ()) in
  let prm = Miou.async server in
  Miou.await prm :: Miou.parallel server domains
  |> List.iter @@ function
  | Ok () -> ()
  | Error exn -> raise exn

To ensure that we're leveraging the full potential of our machine, we can check how many threads our program has (note that an OCaml domain always has 2 threads!). Thus, for a system with 32 cores:

$ ocamlfind opt -linkpkg -package miou,miou.unix main.ml
$ ./a.out &
[1] 438053
$ ls /proc/438053/task | wc -l
64
$ kill -9 438053
[1]  + 438053 killed     ./a.out

Almost for free, we've managed to launch multiple servers in parallel!

Ownership

When it comes to building system and network applications, we often deal with resources shared between the application and the system. One such resource we use here is the file descriptor. OCaml has the advantage of offering a garbage collector to handle memory management for us. However, we still need to consider releasing system resources, particularly file descriptors.

Another point to consider is the manipulation of these resources. We subtly mentioned, using Miou_unix.write, the possibility that a buffer could be concurrently modified. From our experience, the concept of resource ownership (like a buffer) specific to a particular task is lacking in OCaml and can lead to rather challenging bugs to identify and understand. In this regard, languages like Rust offer solutions that can help developers avoid a resource being manipulated by two tasks "at the same time". The problem is even more significant with tasks that can run in parallel. This is referred to as a data race.

Therefore, the best we can offer, Miou provides resource management that resembles that of Rust: a task has exclusive access to a resource once it has "proof" of ownership.

Miou offers an API, Miou.Ownership, where you can:

  • Create proof of ownership
  • Own a resource through this proof
  • Disown a resource through this proof
  • Transfer this resource to a child via this proof
  • Transfer this resource to the parent via this proof
  • Verify, before manipulating this resource, that you have exclusive access to it

Miou_unix extends this API to file descriptors. In this second part of this chapter, it's essential to ensure that we are indeed the owners of the file descriptor we are manipulating. This won't change the behavior of our server; it just allows us to sleep better tonight!

Let's start with the echo function:

let rec echo client =
  let buf = Bytes.create 0x100 in
  let len = Miou_unix.Ownership.read client buf 0 (Bytes.length buf) in
  if len = 0 then Miou_unix.Ownership.close client
  else
    let str = Bytes.sub_string buf 0 len in
    let _ = Miou_unix.Ownership.write client str 0 len in echo client

Miou_unix.Ownership.{read,write} perform the necessary checks, while Miou_unix.Ownership.close disown our file descriptor since we no longer need it. Forgetting this step would result in an error in your application, and Miou would notify you that a resource has been forgotten (via Miou.Resource_leaked). Once attached to a task, a resource must be transferred or released; otherwise, it's considered forgotten! The aim is truly to assist the developer in not forgetting anything they manipulate.

Another interesting aspect of resources is the case of an abnormal termination of our echo function via an exception. A resource is also associated with a finalizer that will be executed if the task in possession of the resource terminates abnormally. Again, the goal is to sleep well tonight.

Now, let's move on to our server function, where we need to transfer our client file descriptor to our echo task:

let server () =
  let socket = Miou_unix.Ownership.tcpv4 () in
  let sockaddr = Unix.ADDR_INET (Unix.inet_addr_loopback, 3000) in
  Miou_unix.Ownership.bind_and_listen socket sockaddr;
  let orphans = Miou.orphans () in
  while true do
    clean_up orphans;
    let client, _ = Miou_unix.Ownership.accept socket in
    ignore (Miou.async
      ~give:[ Miou_unix.Ownership.resource client ]
      ~orphans (fun () -> echo client))
  done;
  Miou_unix.Ownership.close socket

And there you have it! Assuming everything goes well, our code is correct, and we are using our resources correctly. The Miou.Ownership module is not mandatory in Miou's usage but provides a value to dynamically verify the proper use and transfer of your resources. While it's not obligatory, we strongly recommend using it.

The finalizer associated with the resource can also be genuinely beneficial, especially when cancellation occurs: it ensures there are no leaks, even in abnormal situations.

Conclusion

If you've made it this far, you've likely seen a good portion of what Miou has to offer and delved into the intricacies of asynchronous programming and system interactions. You can continue experimenting and having fun with Miou or delve deeper into our tutorial.

If you recall our initial challenge with our echo server, we divided the subject into two parts: the scheduler and system interactions. Miou also maintains this separation between the Miou module and the Miou_unix module. The next chapter will revisit system interactions, but this time with Miou. The goal will be to implement sleepers (and replicate Unix.sleep).

Finally, the last chapter is an enhancement of our echo server using Mutexes and Conditions provided by Miou. This chapter explains in detail the benefits of using these modules over those offered by OCaml and presents, once again, a concrete case.