Spawning a fiber will disable effect handlers installed after main loop

I have encountered this exact problem while experimenting with an effect-based interface for logs, which enable amongst other things to define a logger in a pure manner instead of using a global ref, change the logger for a subscope, indent or add a tag to all nested logs, …

let () =
  Logs.with_reporter reporter @@ fun () ->
  Logs.with_info (fun m -> m "indent nested logs ") @@ fun () ->
  for x = 1 to 3 do
     Logs.info (fun m -> m "%d" x)
  done

(*
logs_eio.exe: [INFO] indent nested logs
logs_eio.exe: [INFO]   x = 1
logs_eio.exe: [INFO]   x = 2
logs_eio.exe: [INFO]   x = 3
*)

To my initial surprise, this didn’t work once combined with eio:

let () =
  Eio_main.run @@ fun _ ->
  Logs.with_reporter reporter @@ fun () ->
  Logs.with_info (fun m -> m "indent nested logs") @@ fun () ->
  Eio.Fiber.both
    (fun () ->
      for x = 1 to 3 do
        Logs.info (fun m -> m "x = %d" x);
        Eio.Fiber.yield ()
      done)
    (fun () ->
      for y = 1 to 3 do
        Logs.info (fun m -> m "y = %d" y);
        Eio.Fiber.yield ()
      done)

(*
logs_eio.exe: [INFO] scope 
Fatal error: exception Stdlib.Effect.Unhandled(Logs.Report(_, 0, 3, _, _, _))
*)

As explained by previous posts, it is logical given the implementation of forking in eio, and AFAICT it’s not possible to make it work out of the box since continuation can only be resumed once. It is however IMO counterintuitive and quite disappointing, as I really want this to work. The with_reporter could be moved above Eio_main.run, but being able to change reporter for a scope sounds useful (say redirect to a file) and my nesting/indentation is still lost.

I did find a reasonable workaround though, which consists in intercepting eio forks by tapping directly into its private effects and reinstalling the effect handler in the forked fiber:

  (** Installs [install] on [f] and reinstalls it on all forked fibers *)
  let reinstall install f =
    install @@ fun () ->
    Effect.Deep.try_with f ()
      {
        effc =
          (fun (type a) (eff : a Effect.t) ->
            match eff with
            | Eio.Private.Effects.Fork (context, fiber) ->
                let fiber () = install fiber in
                Some
                  (fun (continuation : (a, _) Effect.Deep.continuation) ->
                    Effect.Deep.continue continuation
                    @@ Effect.perform
                         (Eio.Private.Effects.Fork (context, fiber)))
            | _ -> None);
      }

  let with_reporter reporter f = reinstall (with_reporter reporter) f
  let with_info msgf f = reinstall (with_info msgf) f

(*
logs_eio.exe: [INFO] scope 
logs_eio.exe: [INFO]   x = 1
logs_eio.exe: [INFO]   y = 1
logs_eio.exe: [INFO]   x = 2
logs_eio.exe: [INFO]   y = 2
logs_eio.exe: [INFO]   x = 3
logs_eio.exe: [INFO]   y = 3
*)

I think this could be enabled automatically by libraries when eio is detected. Since a new handler is installed, it also solves the question of “what happens if the fiber keeps running after we get out of scope of the initial handler”. I’m still only beginning toying with this though.

1 Like