mirror of
https://github.com/ocaml-multicore/eio.git
synced 2025-08-29 00:03:47 -04:00
Split cancellation out from Switch
Cancellation and grouping are easier to handle separately. `Fibre.both` no longer takes a switch, but instead just creates a new cancellation context.
This commit is contained in:
parent
ed2382bed5
commit
3713d9470b
87
README.md
87
README.md
@ -15,7 +15,8 @@ This is an unreleased repository, as it's very much a work-in-progress.
|
||||
* [Testing with Mocks](#testing-with-mocks)
|
||||
* [Fibres](#fibres)
|
||||
* [Tracing](#tracing)
|
||||
* [Switches, Errors, and Cancellation](#switches-errors-and-cancellation)
|
||||
* [Cancellation](#cancellation)
|
||||
* [Switches](#switches)
|
||||
* [Design Note: Results vs Exceptions](#design-note-results-vs-exceptions)
|
||||
* [Performance](#performance)
|
||||
* [Networking](#networking)
|
||||
@ -153,8 +154,7 @@ Here's an example running two threads of execution (fibres) concurrently:
|
||||
|
||||
```ocaml
|
||||
let main _env =
|
||||
Switch.top @@ fun sw ->
|
||||
Fibre.both ~sw
|
||||
Fibre.both
|
||||
(fun () -> for x = 1 to 3 do traceln "x = %d" x; Fibre.yield () done)
|
||||
(fun () -> for y = 1 to 3 do traceln "y = %d" y; Fibre.yield () done);;
|
||||
```
|
||||
@ -204,17 +204,14 @@ The file is a ring buffer, so when it gets full, old events will start to be ove
|
||||
This shows the two counting threads and the lifetime of the `sw` switch.
|
||||
Note that the output from `traceln` appears in the trace as well as on the console.
|
||||
|
||||
## Switches, Errors, and Cancellation
|
||||
## Cancellation
|
||||
|
||||
A switch is used to group fibres together, so they can be cancelled or waited on together.
|
||||
This is a form of [structured concurrency][].
|
||||
|
||||
Here's what happens if one of the two threads above fails:
|
||||
Every fibre has a cancellation context.
|
||||
If one of the `Fibre.both` fibres fails, the other is cancelled:
|
||||
|
||||
```ocaml
|
||||
# Eio_main.run @@ fun _env ->
|
||||
Switch.top @@ fun sw ->
|
||||
Fibre.both ~sw
|
||||
Fibre.both
|
||||
(fun () -> for x = 1 to 3 do traceln "x = %d" x; Fibre.yield () done)
|
||||
(fun () -> failwith "Simulated error");;
|
||||
+x = 1
|
||||
@ -223,17 +220,27 @@ Exception: Failure "Simulated error".
|
||||
|
||||
What happened here was:
|
||||
|
||||
1. The first fibre ran, printed `x = 1` and yielded.
|
||||
2. The second fibre raised an exception.
|
||||
3. `Fibre.both` caught the exception and turned off the switch.
|
||||
4. The first thread's `yield` saw the switch was off and raised a `Cancelled` exception there.
|
||||
5. Once both threads had finished, `Fibre.both` re-raised the exception.
|
||||
1. `Fibre.both` created a new cancellation context for the child fibres.
|
||||
2. The first fibre ran, printed `x = 1` and yielded.
|
||||
3. The second fibre raised an exception.
|
||||
4. `Fibre.both` caught the exception and cancelled the context.
|
||||
5. The first thread's `yield` raised a `Cancelled` exception there.
|
||||
6. Once both threads had finished, `Fibre.both` re-raised the original exception.
|
||||
|
||||
Switches can also be used to wait for threads even when there isn't an error. e.g.
|
||||
You should assume that any operation that can switch fibres can also raise a `Cancelled` exception if a sibling fibre crashes.
|
||||
|
||||
If you want to make an operation non-cancellable, wrap it with `Cancel.protect`
|
||||
(this creates a new context that isn't cancelled with its parent).
|
||||
|
||||
## Switches
|
||||
|
||||
A switch is used to group fibres together, so they can be waited on together.
|
||||
This is a form of [structured concurrency][].
|
||||
For example:
|
||||
|
||||
```ocaml
|
||||
# Eio_main.run @@ fun _env ->
|
||||
Switch.top (fun sw ->
|
||||
Switch.run (fun sw ->
|
||||
Fibre.fork_ignore ~sw
|
||||
(fun () -> for i = 1 to 3 do traceln "i = %d" i; Fibre.yield () done);
|
||||
traceln "First thread forked";
|
||||
@ -254,15 +261,31 @@ Switches can also be used to wait for threads even when there isn't an error. e.
|
||||
- : unit = ()
|
||||
```
|
||||
|
||||
`Switch.top` is used for top-level switches. You can also use `Fibre.fork_sub_ignore` to create a child sub-switch.
|
||||
`Switch.run fn` creates a new switch `sw` and runs `fn sw`.
|
||||
`fn` may spawn new fibres and attach them to the switch.
|
||||
It may also attach other resources such as open file handles.
|
||||
`Switch.run` waits until `fn` and all other attached fibres have finished, and then
|
||||
releases any attached resources (e.g. closing all attached file handles).
|
||||
|
||||
If you call a function without giving it access to a switch,
|
||||
then when the function returns you can be sure that any fibres it spawned have finished,
|
||||
and any files it opened have been closed.
|
||||
So, a `Switch.run` puts a bound on the lifetime of things created within it,
|
||||
leading to clearer code and avoiding resource leaks.
|
||||
|
||||
For example, `fork_ignore` creates a new fibre that continues running after `fork_ignore` returns,
|
||||
so it needs to take a switch argument.
|
||||
|
||||
Every switch also creates a new cancellation context,
|
||||
and you can turn off the switch to cancel all fibres within it.
|
||||
|
||||
You can also use `Fibre.fork_sub_ignore` to create a child sub-switch.
|
||||
Turning off the parent switch will also turn off the child switch, but turning off the child doesn't disable the parent.
|
||||
|
||||
For example, a web-server might use one switch for the whole server and then create one sub-switch for each incoming connection.
|
||||
This allows you to end all fibres handling a single connection by turning off that connection's switch,
|
||||
or to exit the whole application using the top-level switch.
|
||||
|
||||
If you want to make an operation non-cancellable, wrap it in a `Switch.top` to create a fresh switch.
|
||||
|
||||
## Design Note: Results vs Exceptions
|
||||
|
||||
The OCaml standard library uses exceptions to report errors in most cases.
|
||||
@ -334,11 +357,11 @@ Eio provides a simple high-level API for networking.
|
||||
Here is a client that connects to address `addr` using `network` and sends a message:
|
||||
|
||||
```ocaml
|
||||
let run_client ~sw ~net ~addr =
|
||||
let run_client ~net ~addr =
|
||||
traceln "Connecting to server...";
|
||||
Switch.run @@ fun sw ->
|
||||
let flow = Eio.Net.connect ~sw net addr in
|
||||
Eio.Flow.copy_string "Hello from client" flow;
|
||||
Eio.Flow.shutdown flow `Send
|
||||
Eio.Flow.copy_string "Hello from client" flow
|
||||
```
|
||||
|
||||
Note: the `flow` is attached to `sw` and will be closed automatically when it finishes.
|
||||
@ -346,13 +369,14 @@ Note: the `flow` is attached to `sw` and will be closed automatically when it fi
|
||||
Here is a server that listens on `socket` and handles a single connection by reading a message:
|
||||
|
||||
```ocaml
|
||||
let run_server ~sw socket =
|
||||
let run_server socket =
|
||||
Switch.run @@ fun sw ->
|
||||
Eio.Net.accept_sub socket ~sw (fun ~sw flow _addr ->
|
||||
traceln "Server accepted connection from client";
|
||||
let b = Buffer.create 100 in
|
||||
Eio.Flow.copy flow (Eio.Flow.buffer_sink b);
|
||||
traceln "Server received: %S" (Buffer.contents b)
|
||||
) ~on_error:(fun ex -> traceln "Error handling connection: %s" (Printexc.to_string ex));
|
||||
) ~on_error:(traceln "Error handling connection: %a" Fmt.exn);
|
||||
traceln "(normally we'd loop and accept more connections here)"
|
||||
```
|
||||
|
||||
@ -366,12 +390,12 @@ We can test them in a single process using `Fibre.both`:
|
||||
|
||||
```ocaml
|
||||
let main ~net ~addr =
|
||||
Switch.top @@ fun sw ->
|
||||
Switch.run @@ fun sw ->
|
||||
let server = Eio.Net.listen net ~sw ~reuse_addr:true ~backlog:5 addr in
|
||||
traceln "Server ready...";
|
||||
Fibre.both ~sw
|
||||
(fun () -> run_server ~sw server)
|
||||
(fun () -> run_client ~sw ~net ~addr)
|
||||
Fibre.both
|
||||
(fun () -> run_server server)
|
||||
(fun () -> run_client ~net ~addr)
|
||||
```
|
||||
|
||||
```ocaml
|
||||
@ -496,7 +520,7 @@ You can use `open_dir` (or `with_open_dir`) to create a restricted capability to
|
||||
- : unit = ()
|
||||
```
|
||||
|
||||
Please note: you only need to use `open_dir` if you want to create a new sandboxed environment.
|
||||
You only need to use `open_dir` if you want to create a new sandboxed environment.
|
||||
You can use a single directory object to access all paths beneath it,
|
||||
and this allows following symlinks within that subtree.
|
||||
|
||||
@ -545,13 +569,12 @@ We can use `Eio.Domain_manager` to run this in a separate domain:
|
||||
|
||||
```ocaml
|
||||
let main ~domain_mgr =
|
||||
Switch.top @@ fun sw ->
|
||||
let test n =
|
||||
traceln "sum 1..%d = %d" n
|
||||
(Eio.Domain_manager.run_compute_unsafe domain_mgr
|
||||
(fun () -> sum_to n))
|
||||
in
|
||||
Fibre.both ~sw
|
||||
Fibre.both
|
||||
(fun () -> test 100000)
|
||||
(fun () -> test 50000)
|
||||
```
|
||||
|
@ -7,7 +7,7 @@ let main ~clock =
|
||||
n_fibres |> List.iter (fun n_fibres ->
|
||||
let n_iters = 1000000 / n_fibres in
|
||||
let t0 = Eio.Time.now clock in
|
||||
Switch.top (fun sw ->
|
||||
Switch.run (fun sw ->
|
||||
for _ = 1 to n_fibres do
|
||||
Fibre.fork_ignore ~sw (fun () ->
|
||||
for _ = 1 to n_iters do
|
||||
|
104
lib_eio/cancel.ml
Normal file
104
lib_eio/cancel.ml
Normal file
@ -0,0 +1,104 @@
|
||||
open EffectHandlers
|
||||
|
||||
exception Cancel_hook_failed of exn list
|
||||
|
||||
exception Cancelled of exn
|
||||
|
||||
let () =
|
||||
Printexc.register_printer @@ function
|
||||
| Cancel_hook_failed exns -> Some ("During cancellation:\n" ^ String.concat "\nand\n" (List.map Printexc.to_string exns))
|
||||
| Cancelled ex -> Some ("Cancelled: " ^ Printexc.to_string ex)
|
||||
| _ -> None
|
||||
|
||||
type state =
|
||||
| On of (exn -> unit) Lwt_dllist.t
|
||||
| Cancelling of exn * Printexc.raw_backtrace
|
||||
| Finished
|
||||
|
||||
type t = {
|
||||
mutable state : state;
|
||||
}
|
||||
|
||||
(* A dummy value for bootstrapping *)
|
||||
let boot = {
|
||||
state = Finished;
|
||||
}
|
||||
|
||||
type _ eff += Set_cancel : t -> t eff
|
||||
|
||||
let check t =
|
||||
match t.state with
|
||||
| On _ -> ()
|
||||
| Cancelling (ex, _) -> raise (Cancelled ex)
|
||||
| Finished -> invalid_arg "Cancellation context finished!"
|
||||
|
||||
let get_error t =
|
||||
match t.state with
|
||||
| On _ -> None
|
||||
| Cancelling (ex, _) -> Some (Cancelled ex)
|
||||
| Finished -> Some (Invalid_argument "Cancellation context finished!")
|
||||
|
||||
let is_finished t =
|
||||
match t.state with
|
||||
| Finished -> true
|
||||
| On _ | Cancelling _ -> false
|
||||
|
||||
let with_t fn =
|
||||
let q = Lwt_dllist.create () in
|
||||
let t = { state = On q } in
|
||||
Fun.protect (fun () -> fn t)
|
||||
~finally:(fun () -> t.state <- Finished)
|
||||
|
||||
let protect_full fn =
|
||||
with_t @@ fun t ->
|
||||
let x =
|
||||
let old = perform (Set_cancel t) in
|
||||
Fun.protect (fun () -> fn t)
|
||||
~finally:(fun () -> ignore (perform (Set_cancel old)))
|
||||
in
|
||||
check t;
|
||||
x
|
||||
|
||||
let protect fn = protect_full (fun (_ : t) -> fn ())
|
||||
|
||||
let add_hook_unwrapped t hook =
|
||||
match t.state with
|
||||
| Finished -> invalid_arg "Cancellation context finished!"
|
||||
| Cancelling (ex, _) -> protect (fun () -> hook ex); Hook.null
|
||||
| On q ->
|
||||
let node = Lwt_dllist.add_r hook q in
|
||||
(fun () -> Lwt_dllist.remove node)
|
||||
|
||||
let add_hook t hook = add_hook_unwrapped t (fun ex -> hook (Cancelled ex))
|
||||
|
||||
let cancel t ex =
|
||||
match t.state with
|
||||
| Finished -> invalid_arg "Cancellation context finished!"
|
||||
| Cancelling _ -> ()
|
||||
| On q ->
|
||||
let bt = Printexc.get_raw_backtrace () in
|
||||
t.state <- Cancelling (ex, bt);
|
||||
let rec aux () =
|
||||
match Lwt_dllist.take_opt_r q with
|
||||
| None -> []
|
||||
| Some f ->
|
||||
match f ex with
|
||||
| () -> aux ()
|
||||
| exception ex2 -> ex2 :: aux ()
|
||||
in
|
||||
match protect aux with
|
||||
| [] -> ()
|
||||
| exns -> raise (Cancel_hook_failed exns)
|
||||
|
||||
let sub fn =
|
||||
with_t @@ fun t ->
|
||||
let x =
|
||||
let old = perform (Set_cancel t) in
|
||||
Fun.protect (fun () ->
|
||||
let unhook = add_hook_unwrapped old (cancel t) in
|
||||
Fun.protect (fun () -> fn t) ~finally:unhook
|
||||
)
|
||||
~finally:(fun () -> ignore (perform (Set_cancel old)))
|
||||
in
|
||||
check t;
|
||||
x
|
@ -1,5 +1,8 @@
|
||||
open EffectHandlers
|
||||
|
||||
module Hook = Hook
|
||||
module Cancel = Cancel
|
||||
|
||||
module Std = struct
|
||||
module Promise = Promise
|
||||
module Fibre = Fibre
|
||||
@ -13,6 +16,7 @@ end
|
||||
|
||||
module Semaphore = Semaphore
|
||||
module Stream = Stream
|
||||
module Multiple_exn = Multiple_exn
|
||||
|
||||
open Std
|
||||
|
||||
@ -62,7 +66,6 @@ module Flow = struct
|
||||
inherit source
|
||||
|
||||
method private read_source_buffer fn =
|
||||
Fibre.yield ();
|
||||
let rec aux () =
|
||||
match data with
|
||||
| [] -> raise End_of_file
|
||||
@ -75,7 +78,6 @@ module Flow = struct
|
||||
[ Read_source_buffer self#read_source_buffer ]
|
||||
|
||||
method read_into dst =
|
||||
Fibre.yield ();
|
||||
let avail, src = Cstruct.fillv ~dst ~src:data in
|
||||
if avail = 0 then raise End_of_file;
|
||||
data <- src;
|
||||
@ -217,14 +219,14 @@ module Dir = struct
|
||||
let open_dir ~sw (t:#t) = t#open_dir ~sw
|
||||
let mkdir (t:#t) = t#mkdir
|
||||
|
||||
let with_open_in ?sw (t:#t) path fn =
|
||||
Switch.sub_opt sw @@ fun sw -> fn (open_in ~sw t path)
|
||||
let with_open_in (t:#t) path fn =
|
||||
Switch.run @@ fun sw -> fn (open_in ~sw t path)
|
||||
|
||||
let with_open_out ?sw ?append ~create (t:#t) path fn =
|
||||
Switch.sub_opt sw @@ fun sw -> fn (open_out ~sw ?append ~create t path)
|
||||
let with_open_out ?append ~create (t:#t) path fn =
|
||||
Switch.run @@ fun sw -> fn (open_out ~sw ?append ~create t path)
|
||||
|
||||
let with_open_dir ?sw (t:#t) path fn =
|
||||
Switch.sub_opt sw @@ fun sw -> fn (open_dir ~sw t path)
|
||||
let with_open_dir (t:#t) path fn =
|
||||
Switch.run @@ fun sw -> fn (open_dir ~sw t path)
|
||||
end
|
||||
|
||||
module Stdenv = struct
|
||||
@ -250,16 +252,19 @@ module Stdenv = struct
|
||||
end
|
||||
|
||||
module Private = struct
|
||||
type context = Suspend.context = {
|
||||
tid : Ctf.id;
|
||||
mutable cancel : Cancel.t;
|
||||
}
|
||||
|
||||
module Effects = struct
|
||||
type 'a enqueue = 'a Suspend.enqueue
|
||||
type _ eff +=
|
||||
| Suspend = Suspend.Suspend
|
||||
| Suspend_unchecked = Suspend.Suspend_unchecked
|
||||
| Suspend = Suspend.Suspend
|
||||
| Fork = Fibre.Fork
|
||||
| Fork_ignore = Fibre.Fork_ignore
|
||||
| Trace = Std.Trace
|
||||
| Yield = Fibre.Yield
|
||||
| Set_switch = Switch.Set_switch
|
||||
| Set_cancel = Cancel.Set_cancel
|
||||
end
|
||||
let boot_switch = Switch.boot_switch
|
||||
let boot_cancel = Cancel.boot
|
||||
end
|
||||
|
212
lib_eio/eio.mli
212
lib_eio/eio.mli
@ -1,47 +1,44 @@
|
||||
(** Effects based parallel IO for OCaml *)
|
||||
|
||||
(** Reporting multiple failures at once. *)
|
||||
module Multiple_exn : sig
|
||||
exception T of exn list
|
||||
(** Raised if multiple fibres fail, to report all the exceptions. *)
|
||||
end
|
||||
|
||||
(** Handles for removing callbacks. *)
|
||||
module Hook : sig
|
||||
type t
|
||||
|
||||
val remove : t -> unit
|
||||
(** [remove t] removes a previously-added hook.
|
||||
If the hook has already been removed, this does nothing. *)
|
||||
|
||||
val null : t
|
||||
(** A dummy hook. Removing it does nothing. *)
|
||||
end
|
||||
|
||||
(** {1 Concurrency primitives} *)
|
||||
|
||||
(** Commonly used standard features. This module is intended to be [open]ed. *)
|
||||
module Std : sig
|
||||
(** Controlling the lifetime of fibres (groups, exceptions, cancellations, timeouts). *)
|
||||
|
||||
(** Grouping fibres and other resources. *)
|
||||
module Switch : sig
|
||||
type t
|
||||
(** A switch controls a group of fibres.
|
||||
Once a switch is turned off, all activities in that context should cancel themselves. *)
|
||||
(** A switch contains a group of fibres and other resources (such as open file handles).
|
||||
Once a switch is turned off, the fibres should cancel themselves.
|
||||
A switch is created with [Switch.run fn],
|
||||
which does not return until all fibres attached to the switch have finished,
|
||||
and all attached resources have been closed.
|
||||
Each switch includes its own {!Cancel.t} context. *)
|
||||
|
||||
type hook
|
||||
(** A handle to a cancellation hook. *)
|
||||
|
||||
exception Multiple_exceptions of exn list
|
||||
|
||||
exception Cancelled of exn
|
||||
(** [Cancelled ex] indicates that the switch was turned off with exception [ex].
|
||||
It is usually not necessary to report a [Cancelled] exception to the user,
|
||||
as the original problem will be handled elsewhere. *)
|
||||
|
||||
val top : (t -> 'a) -> 'a
|
||||
(** [top fn] runs [fn] with a fresh top-level switch (initially on).
|
||||
When [fn] exits, [top] waits for all operations registered with the switch to finish
|
||||
val run : (t -> 'a) -> 'a
|
||||
(** [run fn] runs [fn] with a fresh switch (initially on).
|
||||
When [fn] exits, [run] waits for all operations registered with the switch to finish
|
||||
(it does not turn the switch off itself).
|
||||
If the switch is turned off before it returns, [top] re-raises the switch's exception(s).
|
||||
@raise Multiple_exceptions If [turn_off] is called more than once. *)
|
||||
|
||||
val sub : ?on_release:(unit -> unit) -> t -> on_error:(exn -> 'a) -> (t -> 'a) -> 'a
|
||||
(** [sub sw ~on_error fn] is like [top fn], but the new switch is a child of [t], so that
|
||||
cancelling [t] also cancels the child (but not the other way around).
|
||||
If [fn] raises an exception then it is passed to [on_error].
|
||||
If you only want to use [sub] to wait for a group of threads to finish, but not to contain
|
||||
errors, you can use [~on_error:raise].
|
||||
@param on_release Register this function with [Switch.on_release sub] once the sub-switch is created.
|
||||
If creating the sub-switch fails, run it immediately. *)
|
||||
|
||||
val sub_opt : ?on_release:(unit -> unit) -> t option -> (t -> 'a) -> 'a
|
||||
(** Run a function with a new switch, optionally a child of another switch.
|
||||
[sub_opt (Some sw)] is [sub sw ~on_error:raise].
|
||||
[sub None] is [top].
|
||||
@param on_release Register this function with [Switch.on_release sub] once the new switch is created.
|
||||
If creating the switch fails, run it immediately. *)
|
||||
If the switch is turned off before it returns, [run] re-raises the switch's exception(s).
|
||||
@raise Multiple_exn.T If [turn_off] is called more than once. *)
|
||||
|
||||
val check : t -> unit
|
||||
(** [check t] checks that [t] is still on.
|
||||
@ -66,27 +63,18 @@ module Std : sig
|
||||
If you want to allow other release handlers to run concurrently, you can start the release
|
||||
operation and then call [on_release] again from within [fn] to register a function to await the result.
|
||||
This will be added to a fresh batch of handlers, run after the original set have finished.
|
||||
Note that [fn] must work even if the switch has been turned off,
|
||||
so using [sub t] or similar within [fn] is usually a bad idea. *)
|
||||
Note that [fn] is called within a {!Cancel.protect}, since aborting clean-up actions is usually a bad idea
|
||||
and the switch may have been cancelled by the time it runs. *)
|
||||
|
||||
val on_release_cancellable : t -> (unit -> unit) -> hook
|
||||
val on_release_cancellable : t -> (unit -> unit) -> Hook.t
|
||||
(** Like [on_release], but the handler can be removed later. *)
|
||||
|
||||
val add_cancel_hook : t -> (exn -> unit) -> hook
|
||||
(** [add_cancel_hook t cancel] registers shutdown function [cancel] with [t].
|
||||
val add_cancel_hook : t -> (exn -> unit) -> Hook.t
|
||||
(** [add_cancel_hook t cancel] registers cancel function [cancel] with [t].
|
||||
When [t] is turned off, [cancel] is called.
|
||||
This can be used to encourage other fibres to exit.
|
||||
If [Switch.run] returns successfully, the hook will not run.
|
||||
If [t] is already off, it calls [cancel] immediately. *)
|
||||
|
||||
val add_cancel_hook_opt : t option -> (exn -> unit) -> hook
|
||||
(**[add_cancel_hook_opt (Some t)] is [add_cancel_hook t].
|
||||
If called with [None], it does nothing and returns {!null_hook}. *)
|
||||
|
||||
val remove_hook : hook -> unit
|
||||
(** [remove_hook h] removes a hook.
|
||||
If the hook has already been removed, this does nothing. *)
|
||||
|
||||
val null_hook : hook
|
||||
(** A dummy hook. Removing it does nothing. *)
|
||||
end
|
||||
|
||||
module Promise : sig
|
||||
@ -100,16 +88,15 @@ module Std : sig
|
||||
(** [create ()] is a fresh promise/resolver pair.
|
||||
The promise is initially unresolved. *)
|
||||
|
||||
val await : ?sw:Switch.t -> 'a t -> 'a
|
||||
val await : 'a t -> 'a
|
||||
(** [await t] blocks until [t] is resolved.
|
||||
If [t] is already resolved then this returns immediately.
|
||||
If [t] is broken, it raises the exception.
|
||||
@param sw Cancel wait if [sw] is turned off. *)
|
||||
If [t] is broken, it raises the exception. *)
|
||||
|
||||
val await_result : ?sw:Switch.t -> 'a t -> ('a, exn) result
|
||||
val await_result : 'a t -> ('a, exn) result
|
||||
(** [await_result t] is like [await t], but returns [Error ex] if [t] is broken
|
||||
instead of raising an exception.
|
||||
Note that turning off [sw] still raises an exception. *)
|
||||
Note that if the [await_result] itself is cancelled then it still raises. *)
|
||||
|
||||
val fulfill : 'a u -> 'a -> unit
|
||||
(** [fulfill u v] successfully resolves [u]'s promise with the value [v].
|
||||
@ -149,21 +136,29 @@ module Std : sig
|
||||
end
|
||||
|
||||
module Fibre : sig
|
||||
val both : sw:Switch.t -> (unit -> unit) -> (unit -> unit) -> unit
|
||||
(** [both ~sw f g] runs [f ()] and [g ()] concurrently.
|
||||
If either raises an exception, [sw] is turned off.
|
||||
[both] waits for both functions to finish even if one raises. *)
|
||||
val both : (unit -> unit) -> (unit -> unit) -> unit
|
||||
(** [both f g] runs [f ()] and [g ()] concurrently.
|
||||
They run in a new cancellation sub-context, and
|
||||
if either raises an exception, the other is cancelled.
|
||||
[both] waits for both functions to finish even if one raises
|
||||
(it will then re-raise the original exception).
|
||||
@raise Multiple_exn.T if both fibres raise exceptions (excluding {!Cancel.Cancelled}). *)
|
||||
|
||||
val fork_ignore : sw:Switch.t -> (unit -> unit) -> unit
|
||||
(** [fork_ignore ~sw fn] runs [fn ()] in a new fibre, but does not wait for it to complete.
|
||||
The new fibre is attached to [sw] (which can't finish until the fibre ends).
|
||||
The new fibre inherits [sw]'s cancellation context.
|
||||
If the fibre raises an exception, [sw] is turned off.
|
||||
If [sw] is already off then [fn] fails immediately, but the calling thread continues. *)
|
||||
|
||||
val fork_sub_ignore : ?on_release:(unit -> unit) -> sw:Switch.t -> on_error:(exn -> unit) -> (Switch.t -> unit) -> unit
|
||||
(** [fork_sub_ignore ~sw ~on_error fn] is like [fork_ignore], but it creates a new sub-switch for the fibre.
|
||||
This means that you can cancel the child switch without cancelling the parent.
|
||||
This is a convenience function for running {!Switch.sub} inside a {!fork_ignore}. *)
|
||||
This is a convenience function for running {!Switch.run} inside a {!fork_ignore}.
|
||||
@param on_release If given, this function is called when the new fibre ends.
|
||||
If the fibre cannot be created (e.g. because [sw] is already off), it runs immediately.
|
||||
@param on_error This is called if the fibre raises an exception.
|
||||
If it raises in turn, the parent switch is turned off. *)
|
||||
|
||||
val fork : sw:Switch.t -> exn_turn_off:bool -> (unit -> 'a) -> 'a Promise.t
|
||||
(** [fork ~sw ~exn_turn_off fn] starts running [fn ()] in a new fibre and returns a promise for its result.
|
||||
@ -215,10 +210,9 @@ module Semaphore : sig
|
||||
If other fibres are waiting on [t], the one that has been waiting the longest is resumed.
|
||||
@raise Sys_error if the value of the semaphore would overflow [max_int] *)
|
||||
|
||||
val acquire : ?sw:Switch.t -> t -> unit
|
||||
val acquire : t -> unit
|
||||
(** [acquire t] blocks the calling fibre until the value of semaphore [t]
|
||||
is not zero, then atomically decrements the value of [t] and returns.
|
||||
@param sw Abort if the switch is turned off. *)
|
||||
is not zero, then atomically decrements the value of [t] and returns. *)
|
||||
|
||||
val get_value : t -> int
|
||||
(** [get_value t] returns the current value of semaphore [t]. *)
|
||||
@ -233,17 +227,64 @@ module Stream : sig
|
||||
(** [create capacity] is a new stream which can hold up to [capacity] items without blocking writers.
|
||||
If [capacity = 0] then writes block until a reader is ready. *)
|
||||
|
||||
val add : ?sw:Switch.t -> 'a t -> 'a -> unit
|
||||
val add : 'a t -> 'a -> unit
|
||||
(** [add t item] adds [item] to [t].
|
||||
If this would take [t] over capacity, it blocks until there is space.
|
||||
@param sw Stop waiting if the switch is turned off. *)
|
||||
If this would take [t] over capacity, it blocks until there is space. *)
|
||||
|
||||
val take : ?sw:Switch.t -> 'a t -> 'a
|
||||
val take : 'a t -> 'a
|
||||
(** [take t] takes the next item from the head of [t].
|
||||
If no items are available, it waits until one becomes available.
|
||||
@param sw Stop waiting if the switch is turned off. *)
|
||||
If no items are available, it waits until one becomes available. *)
|
||||
end
|
||||
|
||||
(** Cancelling other fibres when an exception occurs. *)
|
||||
module Cancel : sig
|
||||
(** This is the low-level interface to cancellation.
|
||||
Every {!Switch} includes a cancellation context and most users will just use that API instead. *)
|
||||
|
||||
type t
|
||||
(** A cancellation context. *)
|
||||
|
||||
exception Cancelled of exn
|
||||
(** [Cancelled ex] indicates that the context was cancelled with exception [ex].
|
||||
It is usually not necessary to report a [Cancelled] exception to the user,
|
||||
as the original problem will be handled elsewhere. *)
|
||||
|
||||
exception Cancel_hook_failed of exn list
|
||||
(** Raised by {!cancel} if any of the cancellation hooks themselves fail. *)
|
||||
|
||||
val sub : (t -> 'a) -> 'a
|
||||
(** [sub fn] installs a new cancellation context [t], runs [fn t] inside it, and then restores the old context.
|
||||
If the old context is cancelled while [fn] is running then [t] is cancelled too.
|
||||
[t] cannot be used after [sub] returns. *)
|
||||
|
||||
val protect : (unit -> 'a) -> 'a
|
||||
(** [protect fn] runs [fn] in a new cancellation context that isn't cancelled when its parent is.
|
||||
This can be used to clean up resources on cancellation.
|
||||
However, it is usually better to use {!Switch.on_release} (which calls this for you). *)
|
||||
|
||||
val protect_full : (t -> 'a) -> 'a
|
||||
(** [protect_full fn] is like {!protect}, but also gives access to the new context. *)
|
||||
|
||||
val check : t -> unit
|
||||
(** [check t] checks that [t] hasn't been cancelled.
|
||||
@raise Cancelled If the context has been cancelled. *)
|
||||
|
||||
val get_error : t -> exn option
|
||||
(** [get_error t] is like [check t] except that it returns the exception instead of raising it.
|
||||
If [t] is finished, this returns (rather than raising) the [Invalid_argument] exception too. *)
|
||||
|
||||
val add_hook : t -> (exn -> unit) -> Hook.t
|
||||
(** [add_hook t fn] registers cancellation function [fn] with [t].
|
||||
When [t] is cancelled, [protect (fun () -> fn ex)] is called.
|
||||
If [t] is already cancelled, it calls [fn] immediately. *)
|
||||
|
||||
val cancel : t -> exn -> unit
|
||||
(** [cancel t ex] marks [t] as cancelled and then calls all registered hooks,
|
||||
passing [ex] as the argument.
|
||||
All hooks are run, even if some of them raise exceptions.
|
||||
@raise Cancel_hook_failed if one or more hooks fail. *)
|
||||
end
|
||||
|
||||
(** {1 Cross-platform OS API} *)
|
||||
|
||||
(** A base class for objects that can be queried at runtime for extra features. *)
|
||||
@ -456,7 +497,7 @@ module Dir : sig
|
||||
(** [open_in ~sw t path] opens [t/path] for reading.
|
||||
Note: files are always opened in binary mode. *)
|
||||
|
||||
val with_open_in : ?sw:Switch.t -> #t -> path -> (<Flow.source; Flow.close> -> 'a) -> 'a
|
||||
val with_open_in : #t -> path -> (<Flow.source; Flow.close> -> 'a) -> 'a
|
||||
(** [with_open_in] is like [open_in], but calls [fn flow] with the new flow and closes
|
||||
it automatically when [fn] returns (if it hasn't already been closed by then). *)
|
||||
|
||||
@ -471,7 +512,6 @@ module Dir : sig
|
||||
@param create Controls whether to create the file, and what permissions to give it if so. *)
|
||||
|
||||
val with_open_out :
|
||||
?sw:Switch.t ->
|
||||
?append:bool ->
|
||||
create:create ->
|
||||
#t -> path -> (<rw; Flow.close> -> 'a) -> 'a
|
||||
@ -485,7 +525,7 @@ module Dir : sig
|
||||
(** [open_dir ~sw t path] opens [t/path].
|
||||
This can be passed to functions to grant access only to the subtree [t/path]. *)
|
||||
|
||||
val with_open_dir : ?sw:Switch.t -> #t -> path -> (<t; Flow.close> -> 'a) -> 'a
|
||||
val with_open_dir : #t -> path -> (<t; Flow.close> -> 'a) -> 'a
|
||||
(** [with_open_dir] is like [open_dir], but calls [fn dir] with the new directory and closes
|
||||
it automatically when [fn] returns (if it hasn't already been closed by then). *)
|
||||
end
|
||||
@ -527,6 +567,11 @@ end
|
||||
|
||||
(** API for use by the scheduler implementation. *)
|
||||
module Private : sig
|
||||
type context = {
|
||||
tid : Ctf.id;
|
||||
mutable cancel : Cancel.t;
|
||||
}
|
||||
|
||||
module Effects : sig
|
||||
open EffectHandlers
|
||||
|
||||
@ -534,16 +579,12 @@ module Private : sig
|
||||
(** A function provided by the scheduler to reschedule a previously-suspended thread. *)
|
||||
|
||||
type _ eff +=
|
||||
| Suspend : (Ctf.id -> 'a enqueue -> unit) -> 'a eff
|
||||
| Suspend : (context -> 'a enqueue -> unit) -> 'a eff
|
||||
(** [Suspend fn] is performed when a fibre must be suspended
|
||||
(e.g. because it called {!Promise.await} on an unresolved promise).
|
||||
The effect handler runs [fn tid enqueue] in the scheduler context,
|
||||
passing it the suspended fibre's thread ID (for tracing) and a function to resume it.
|
||||
[fn] should arrange for [enqueue] to be called once the thread is ready to run again.
|
||||
If a cancellation is pending, this will raise it. *)
|
||||
|
||||
| Suspend_unchecked : (Ctf.id -> 'a enqueue -> unit) -> 'a eff
|
||||
(** [Suspend_unchecked] is like [Suspend], but doesn't raise pending exceptions. *)
|
||||
The effect handler runs [fn fibre enqueue] in the scheduler context,
|
||||
passing it the suspended fibre's context and a function to resume it.
|
||||
[fn] should arrange for [enqueue] to be called once the thread is ready to run again. *)
|
||||
|
||||
| Fork : (unit -> 'a) -> 'a Promise.t eff
|
||||
(** See {!Fibre.fork} *)
|
||||
@ -551,18 +592,17 @@ module Private : sig
|
||||
| Fork_ignore : (unit -> unit) -> unit eff
|
||||
(** See {!Fibre.fork_ignore} *)
|
||||
|
||||
| Yield : unit eff
|
||||
|
||||
| Trace : (?__POS__:(string * int * int * int) -> ('a, Format.formatter, unit, unit) format4 -> 'a) eff
|
||||
(** [perform Trace fmt] writes trace logging to the configured trace output.
|
||||
It must not switch fibres, as tracing must not affect scheduling.
|
||||
If the system is not ready to receive the trace output,
|
||||
the whole domain must block until it is. *)
|
||||
|
||||
| Set_switch : Switch.t -> Switch.t eff
|
||||
| Set_cancel : Cancel.t -> Cancel.t eff
|
||||
(** [Set_cancel c] sets the running fibre's cancel context to [c] and returns the previous context. *)
|
||||
end
|
||||
|
||||
val boot_switch : Switch.t
|
||||
val boot_cancel : Cancel.t
|
||||
(** A dummy context which is useful briefly during start up before the backend calls {!Cancel.protect}
|
||||
to install a proper context. *)
|
||||
end
|
||||
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
open EffectHandlers
|
||||
|
||||
type _ eff += Fork : (unit -> 'a) -> 'a Promise.t eff
|
||||
type _ eff += Yield : unit eff
|
||||
|
||||
let fork ~sw ~exn_turn_off f =
|
||||
let f () =
|
||||
@ -18,40 +17,54 @@ type _ eff += Fork_ignore : (unit -> unit) -> unit eff
|
||||
let fork_ignore ~sw f =
|
||||
let f () =
|
||||
Switch.with_op sw @@ fun () ->
|
||||
try f ()
|
||||
try
|
||||
Cancel.protect_full @@ fun c ->
|
||||
let hook = Switch.add_cancel_hook sw (Cancel.cancel c) in
|
||||
Fun.protect f
|
||||
~finally:(fun () -> Hook.remove hook)
|
||||
with ex ->
|
||||
Switch.turn_off sw ex;
|
||||
raise ex
|
||||
Switch.turn_off sw ex
|
||||
in
|
||||
perform (Fork_ignore f)
|
||||
|
||||
let yield () =
|
||||
perform Yield
|
||||
let c = ref Cancel.boot in
|
||||
Suspend.enter (fun fibre enqueue ->
|
||||
c := fibre.cancel;
|
||||
enqueue (Ok ())
|
||||
);
|
||||
Cancel.check !c
|
||||
|
||||
let both ~sw f g =
|
||||
let x = fork ~sw ~exn_turn_off:true f in
|
||||
begin
|
||||
try g ()
|
||||
with ex -> Switch.turn_off sw ex
|
||||
end;
|
||||
ignore (Promise.await_result x : (unit, exn) result);
|
||||
match sw.state with
|
||||
| On _ -> ()
|
||||
| Off (ex, bt) ->
|
||||
Switch.raise_with_extras sw ex bt
|
||||
| Finished -> assert false
|
||||
let both f g =
|
||||
Cancel.sub @@ fun cancel ->
|
||||
let f () =
|
||||
try f ()
|
||||
with ex -> Cancel.cancel cancel ex; raise ex
|
||||
in
|
||||
let x = perform (Fork f) in
|
||||
match g () with
|
||||
| () -> Promise.await x (* [g] succeeds - just report [f]'s result *)
|
||||
| exception gex ->
|
||||
Cancel.cancel cancel gex;
|
||||
match Cancel.protect (fun () -> Promise.await_result x) with
|
||||
| Ok () | Error (Cancel.Cancelled _) -> raise gex (* [g] fails, nothing to report for [f] *)
|
||||
| Error fex ->
|
||||
match gex with
|
||||
| Cancel.Cancelled _ -> raise fex (* [f] fails, nothing to report for [g] *)
|
||||
| _ -> raise (Multiple_exn.T [fex; gex]) (* Both fail *)
|
||||
|
||||
let fork_sub_ignore ?on_release ~sw ~on_error f =
|
||||
if Switch.is_finished sw then (
|
||||
(* If the switch is finished then we have no way to report the error after forking,
|
||||
so do it now. *)
|
||||
Option.iter (fun f -> f ()) on_release;
|
||||
invalid_arg "Switch finished!"
|
||||
);
|
||||
let f () =
|
||||
try Switch.sub ?on_release sw ~on_error f
|
||||
with ex ->
|
||||
Switch.turn_off sw ex;
|
||||
raise ex
|
||||
in
|
||||
perform (Fork_ignore f)
|
||||
let did_attach = ref false in
|
||||
fork_ignore ~sw (fun () ->
|
||||
try Switch.run (fun sw -> Option.iter (Switch.on_release sw) on_release; did_attach := true; f sw)
|
||||
with ex ->
|
||||
try on_error ex
|
||||
with ex2 ->
|
||||
Switch.turn_off sw ex;
|
||||
Switch.turn_off sw ex2
|
||||
);
|
||||
if not !did_attach then (
|
||||
Option.iter Cancel.protect on_release;
|
||||
Switch.check sw;
|
||||
assert false
|
||||
)
|
||||
|
5
lib_eio/hook.ml
Normal file
5
lib_eio/hook.ml
Normal file
@ -0,0 +1,5 @@
|
||||
type t = unit -> unit (* A function to remove the hook *)
|
||||
|
||||
let null = ignore
|
||||
|
||||
let remove t = t ()
|
6
lib_eio/multiple_exn.ml
Normal file
6
lib_eio/multiple_exn.ml
Normal file
@ -0,0 +1,6 @@
|
||||
exception T of exn list
|
||||
|
||||
let () =
|
||||
Printexc.register_printer @@ function
|
||||
| T exns -> Some ("Multiple exceptions:\n" ^ String.concat "\nand\n" (List.map Printexc.to_string exns))
|
||||
| _ -> None
|
@ -31,27 +31,22 @@ let broken ex =
|
||||
Ctf.note_created id Ctf.Promise;
|
||||
{ id; state = Broken ex }
|
||||
|
||||
let await ?sw t =
|
||||
Option.iter Switch.check sw;
|
||||
let await_result t =
|
||||
match t.state with
|
||||
| Fulfilled x ->
|
||||
Ctf.note_read t.id;
|
||||
x
|
||||
Ok x
|
||||
| Broken ex ->
|
||||
Ctf.note_read t.id;
|
||||
raise ex
|
||||
Error ex
|
||||
| Unresolved q ->
|
||||
Ctf.note_try_read t.id;
|
||||
Switch.await ?sw q t.id
|
||||
Switch.await q t.id
|
||||
|
||||
let await_result ?sw t =
|
||||
match await ?sw t with
|
||||
| x ->
|
||||
Option.iter Switch.check sw;
|
||||
Ok x
|
||||
| exception ex ->
|
||||
Option.iter Switch.check sw;
|
||||
Error ex
|
||||
let await t =
|
||||
match await_result t with
|
||||
| Ok x -> x
|
||||
| Error ex -> raise ex
|
||||
|
||||
let fulfill t v =
|
||||
match t.state with
|
||||
|
@ -27,14 +27,14 @@ let release t =
|
||||
| `Queue_empty ->
|
||||
t.state <- Free 1
|
||||
|
||||
let rec acquire ?sw t =
|
||||
let rec acquire t =
|
||||
match t.state with
|
||||
| Waiting q ->
|
||||
Ctf.note_try_read t.id;
|
||||
Switch.await ?sw q t.id
|
||||
Switch.await q t.id |> Switch.or_raise
|
||||
| Free 0 ->
|
||||
t.state <- Waiting (Waiters.create ());
|
||||
acquire ?sw t
|
||||
acquire t
|
||||
| Free n ->
|
||||
Ctf.note_read t.id;
|
||||
t.state <- Free (pred n)
|
||||
|
@ -29,8 +29,7 @@ let create capacity =
|
||||
writers = Waiters.create ();
|
||||
}
|
||||
|
||||
let add ?sw t item =
|
||||
Option.iter Switch.check sw;
|
||||
let add t item =
|
||||
match Waiters.wake_one t.readers (Ok item) with
|
||||
| `Ok -> ()
|
||||
| `Queue_empty ->
|
||||
@ -38,8 +37,8 @@ let add ?sw t item =
|
||||
if Queue.length t.items < t.capacity then Queue.add item t.items
|
||||
else (
|
||||
(* The queue is full. Wait for our turn first. *)
|
||||
Suspend.enter @@ fun tid enqueue ->
|
||||
Switch.await_internal ?sw t.writers t.id tid (fun r ->
|
||||
Suspend.enter @@ fun ctx enqueue ->
|
||||
Switch.await_internal t.writers t.id ctx (fun r ->
|
||||
if Result.is_ok r then (
|
||||
(* We get here immediately when called by [take], either:
|
||||
1. after removing an item, so there is space, or
|
||||
@ -48,17 +47,16 @@ let add ?sw t item =
|
||||
);
|
||||
enqueue r
|
||||
)
|
||||
)
|
||||
) |> Switch.or_raise
|
||||
|
||||
let take ?sw t =
|
||||
Option.iter Switch.check sw;
|
||||
let take t =
|
||||
match Queue.take_opt t.items with
|
||||
| None ->
|
||||
(* There aren't any items, so we probably need to wait for one.
|
||||
However, there's also the special case of a zero-capacity queue to deal with.
|
||||
[is_empty writers || capacity = 0] *)
|
||||
begin match Waiters.wake_one t.writers (Ok ()) with
|
||||
| `Queue_empty -> Switch.await ?sw t.readers t.id
|
||||
| `Queue_empty -> Switch.await t.readers t.id |> Switch.or_raise
|
||||
| `Ok ->
|
||||
(* [capacity = 0] (this is the only way we can get waiters and no items).
|
||||
[wake_one] has just added an item to the queue, so remove it quickly to restore the invariant. *)
|
||||
|
@ -1,8 +1,17 @@
|
||||
open EffectHandlers
|
||||
|
||||
type 'a enqueue = ('a, exn) result -> unit
|
||||
type _ eff += Suspend : (Ctf.id -> 'a enqueue -> unit) -> 'a eff
|
||||
type _ eff += Suspend_unchecked : (Ctf.id -> 'a enqueue -> unit) -> 'a eff
|
||||
type context = {
|
||||
tid : Ctf.id;
|
||||
mutable cancel : Cancel.t;
|
||||
}
|
||||
|
||||
let enter fn = perform (Suspend fn)
|
||||
let enter_unchecked fn = perform (Suspend_unchecked fn)
|
||||
type 'a enqueue = ('a, exn) result -> unit
|
||||
type _ eff += Suspend : (context -> 'a enqueue -> unit) -> 'a eff
|
||||
|
||||
let enter_unchecked fn = perform (Suspend fn)
|
||||
|
||||
let enter fn =
|
||||
enter_unchecked @@ fun fibre enqueue ->
|
||||
match Cancel.get_error fibre.cancel with
|
||||
| None -> fn fibre enqueue
|
||||
| Some ex -> enqueue (Error ex)
|
||||
|
@ -1,101 +1,36 @@
|
||||
open EffectHandlers
|
||||
|
||||
exception Multiple_exceptions of exn list
|
||||
|
||||
exception Cancelled of exn
|
||||
|
||||
type hook = unit -> unit (* A function to remove the hook *)
|
||||
|
||||
let () =
|
||||
Printexc.register_printer @@ function
|
||||
| Multiple_exceptions exns -> Some ("Multiple exceptions:\n" ^ String.concat "\nand\n" (List.map Printexc.to_string exns))
|
||||
| Cancelled ex -> Some ("Cancelled: " ^ Printexc.to_string ex)
|
||||
| _ -> None
|
||||
|
||||
type state =
|
||||
| On of (exn -> unit) Lwt_dllist.t
|
||||
| Off of exn * Printexc.raw_backtrace
|
||||
| Finished
|
||||
|
||||
type t = {
|
||||
id : Ctf.id;
|
||||
mutable state : state;
|
||||
mutable fibres : int;
|
||||
mutable extra_exceptions : exn list;
|
||||
on_release : (unit -> unit) Lwt_dllist.t;
|
||||
waiter : unit Waiters.t; (* The main [top]/[sub] function may wait here for fibres to finish. *)
|
||||
cancel : Cancel.t;
|
||||
}
|
||||
|
||||
(* A dummy switch for bootstrapping *)
|
||||
let boot_switch = {
|
||||
id = Ctf.mint_id ();
|
||||
state = Finished;
|
||||
fibres = 0;
|
||||
extra_exceptions = [];
|
||||
on_release = Lwt_dllist.create ();
|
||||
waiter = Waiters.create ();
|
||||
}
|
||||
|
||||
type _ eff += Set_switch : t -> t eff
|
||||
|
||||
let with_switch t fn =
|
||||
let old = perform (Set_switch t) in
|
||||
Fun.protect fn
|
||||
~finally:(fun () -> ignore (perform (Set_switch old)))
|
||||
|
||||
let null_hook = ignore
|
||||
|
||||
let remove_hook h = h ()
|
||||
let is_finished t = Cancel.is_finished t.cancel
|
||||
|
||||
let check t =
|
||||
match t.state with
|
||||
| On _ -> ()
|
||||
| Off (ex, _) -> raise (Cancelled ex)
|
||||
| Finished -> invalid_arg "Switch finished!"
|
||||
if is_finished t then invalid_arg "Switch finished!";
|
||||
Cancel.check t.cancel
|
||||
|
||||
let get_error t =
|
||||
match t.state with
|
||||
| On _ -> None
|
||||
| Off (ex, _) -> Some (Cancelled ex)
|
||||
| Finished -> Some (Invalid_argument "Switch finished!")
|
||||
|
||||
let is_finished t =
|
||||
match t.state with
|
||||
| Finished -> true
|
||||
| On _ | Off _ -> false
|
||||
Cancel.get_error t.cancel
|
||||
|
||||
let rec turn_off t ex =
|
||||
match t.state with
|
||||
match t.cancel.state with
|
||||
| Finished -> invalid_arg "Switch finished!"
|
||||
| Off (orig, _) when orig == ex || List.memq ex t.extra_exceptions -> ()
|
||||
| Off _ ->
|
||||
| Cancelling (orig, _) when orig == ex || List.memq ex t.extra_exceptions -> ()
|
||||
| Cancelling _ ->
|
||||
begin match ex with
|
||||
| Cancelled _ -> () (* The original exception will be reported elsewhere *)
|
||||
| Multiple_exceptions exns -> List.iter (turn_off t) exns
|
||||
| Cancel.Cancelled _ -> () (* The original exception will be reported elsewhere *)
|
||||
| Multiple_exn.T exns -> List.iter (turn_off t) exns
|
||||
| _ -> t.extra_exceptions <- ex :: t.extra_exceptions
|
||||
end
|
||||
| On q ->
|
||||
| On _ ->
|
||||
Ctf.note_resolved t.id ~ex:(Some ex);
|
||||
t.state <- Off (ex, Printexc.get_raw_backtrace ());
|
||||
let rec aux () =
|
||||
match Lwt_dllist.take_opt_r q with
|
||||
| None -> ()
|
||||
| Some f ->
|
||||
begin
|
||||
try f ex
|
||||
with ex2 -> turn_off t ex2
|
||||
end;
|
||||
aux ()
|
||||
in
|
||||
aux ()
|
||||
Cancel.cancel t.cancel ex
|
||||
|
||||
let add_cancel_hook t hook =
|
||||
match t.state with
|
||||
| Finished -> invalid_arg "Switch finished!"
|
||||
| Off (ex, _) -> hook ex; ignore
|
||||
| On q ->
|
||||
let node = Lwt_dllist.add_r hook q in
|
||||
(fun () -> Lwt_dllist.remove node)
|
||||
let add_cancel_hook t hook = Cancel.add_hook t.cancel hook
|
||||
|
||||
let add_cancel_hook_opt t hook =
|
||||
match t with
|
||||
@ -112,29 +47,32 @@ let with_op t fn =
|
||||
Waiters.wake_all t.waiter (Ok ())
|
||||
)
|
||||
|
||||
let await_internal ?sw waiters id tid enqueue =
|
||||
let await_internal waiters id (ctx:Suspend.context) enqueue =
|
||||
let cleanup_hooks = Queue.create () in
|
||||
let when_resolved r =
|
||||
Queue.iter Waiters.remove_waiter cleanup_hooks;
|
||||
Ctf.note_read ~reader:id tid;
|
||||
Ctf.note_read ~reader:id ctx.tid;
|
||||
enqueue r
|
||||
in
|
||||
let cancel ex = when_resolved (Error ex) in
|
||||
sw |> Option.iter (fun sw ->
|
||||
let cancel_waiter = add_cancel_hook sw cancel in
|
||||
Queue.add cancel_waiter cleanup_hooks;
|
||||
);
|
||||
let resolved_waiter = Waiters.add_waiter waiters when_resolved in
|
||||
let cancel_waiter = Cancel.add_hook ctx.cancel cancel in
|
||||
Queue.add cancel_waiter cleanup_hooks;
|
||||
let resolved_waiter = Waiters.add_waiter waiters (fun x -> when_resolved (Ok x)) in
|
||||
Queue.add resolved_waiter cleanup_hooks
|
||||
|
||||
let await ?sw waiters id =
|
||||
Suspend.enter_unchecked (await_internal ?sw waiters id)
|
||||
(* Returns a result if the wait succeeds, or raises if cancelled. *)
|
||||
let await waiters id =
|
||||
Suspend.enter (await_internal waiters id)
|
||||
|
||||
let or_raise = function
|
||||
| Ok x -> x
|
||||
| Error ex -> raise ex
|
||||
|
||||
let rec await_idle t =
|
||||
(* Wait for fibres to finish: *)
|
||||
while t.fibres > 0 do
|
||||
Ctf.note_try_read t.id;
|
||||
await t.waiter t.id
|
||||
await t.waiter t.id |> or_raise;
|
||||
done;
|
||||
(* Call on_release handlers: *)
|
||||
let queue = Lwt_dllist.create () in
|
||||
@ -152,103 +90,64 @@ let rec await_idle t =
|
||||
in
|
||||
release ()
|
||||
|
||||
let await_idle t = Cancel.protect (fun _ -> await_idle t)
|
||||
|
||||
let raise_with_extras t ex bt =
|
||||
match t.extra_exceptions with
|
||||
| [] -> Printexc.raise_with_backtrace ex bt
|
||||
| exns -> Printexc.raise_with_backtrace (Multiple_exceptions (ex :: List.rev exns)) bt
|
||||
| exns -> Printexc.raise_with_backtrace (Multiple_exn.T (ex :: List.rev exns)) bt
|
||||
|
||||
let top fn =
|
||||
let run fn =
|
||||
let id = Ctf.mint_id () in
|
||||
Ctf.note_created id Ctf.Switch;
|
||||
let q = Lwt_dllist.create () in
|
||||
Cancel.sub @@ fun cancel ->
|
||||
let t = {
|
||||
id;
|
||||
state = On q;
|
||||
fibres = 0;
|
||||
extra_exceptions = [];
|
||||
waiter = Waiters.create ();
|
||||
on_release = Lwt_dllist.create ();
|
||||
cancel;
|
||||
} in
|
||||
with_switch t @@ fun () ->
|
||||
match fn t with
|
||||
| v ->
|
||||
await_idle t;
|
||||
begin match t.state with
|
||||
begin match t.cancel.state with
|
||||
| Finished -> assert false
|
||||
| On _ ->
|
||||
(* Success. Just mark the switch as unusable now. *)
|
||||
t.state <- Finished;
|
||||
(* Success. *)
|
||||
Ctf.note_read t.id;
|
||||
v
|
||||
| Off (ex, bt) ->
|
||||
| Cancelling (ex, bt) ->
|
||||
(* Function succeeded, but got failure waiting for fibres to finish. *)
|
||||
t.state <- Finished;
|
||||
Ctf.note_read t.id;
|
||||
raise_with_extras t ex bt
|
||||
end
|
||||
| exception ex ->
|
||||
(* Main function failed.
|
||||
Turn the switch off to cancel any running fibres, if it's not off already. *)
|
||||
turn_off t ex;
|
||||
begin
|
||||
try turn_off t ex
|
||||
with Cancel.Cancel_hook_failed _ as ex ->
|
||||
t.extra_exceptions <- ex :: t.extra_exceptions
|
||||
end;
|
||||
await_idle t;
|
||||
Ctf.note_read t.id;
|
||||
match t.state with
|
||||
match t.cancel.state with
|
||||
| On _ | Finished -> assert false
|
||||
| Off (ex, bt) ->
|
||||
t.state <- Finished;
|
||||
raise_with_extras t ex bt
|
||||
| Cancelling (ex, bt) -> raise_with_extras t ex bt
|
||||
|
||||
let on_release_cancellable t fn =
|
||||
match t.state with
|
||||
let on_release_full t fn =
|
||||
match t.cancel.state with
|
||||
| On _ | Cancelling _ -> Lwt_dllist.add_r fn t.on_release
|
||||
| Finished ->
|
||||
fn ();
|
||||
invalid_arg "Switch finished!"
|
||||
| On _ | Off _ ->
|
||||
let node = Lwt_dllist.add_r fn t.on_release in
|
||||
(fun () -> Lwt_dllist.remove node)
|
||||
match Cancel.protect fn with
|
||||
| () -> invalid_arg "Switch finished!"
|
||||
| exception ex -> raise (Multiple_exn.T [ex; Invalid_argument "Switch finished!"])
|
||||
|
||||
let on_release t fn =
|
||||
match t.state with
|
||||
| Finished ->
|
||||
fn ();
|
||||
invalid_arg "Switch finished!"
|
||||
| On _ | Off _ ->
|
||||
let _ : _ Lwt_dllist.node = Lwt_dllist.add_r fn t.on_release in
|
||||
()
|
||||
ignore (on_release_full t fn : _ Lwt_dllist.node)
|
||||
|
||||
let sub ?on_release:release sw ~on_error fn =
|
||||
match sw.state with
|
||||
| Finished ->
|
||||
(* Can't create child switch. Run release hooks immediately. *)
|
||||
Option.iter (fun f -> f ()) release;
|
||||
invalid_arg "Switch finished!"
|
||||
| Off (ex, _) ->
|
||||
(* Can't create child switch. Run release hooks immediately. *)
|
||||
Option.iter (fun f -> f ()) release;
|
||||
raise (Cancelled ex)
|
||||
| On _ ->
|
||||
with_op sw @@ fun () ->
|
||||
let w = ref ignore in
|
||||
match
|
||||
top (fun child ->
|
||||
w := add_cancel_hook sw (turn_off child);
|
||||
Option.iter (on_release child) release;
|
||||
try fn child
|
||||
with ex -> turn_off child ex; raise ex
|
||||
)
|
||||
with
|
||||
| v ->
|
||||
Waiters.remove_waiter !w;
|
||||
v
|
||||
| exception ex ->
|
||||
Waiters.remove_waiter !w;
|
||||
on_error ex
|
||||
|
||||
let sub_opt ?on_release:release t fn =
|
||||
match t with
|
||||
| Some t -> sub ?on_release:release ~on_error:raise t fn
|
||||
| None ->
|
||||
top (fun child ->
|
||||
Option.iter (on_release child) release;
|
||||
fn child
|
||||
)
|
||||
let on_release_cancellable t fn =
|
||||
let node = on_release_full t fn in
|
||||
(fun () -> Lwt_dllist.remove node)
|
||||
|
@ -43,7 +43,7 @@ type _ eff += Close : Unix.file_descr -> int eff
|
||||
module FD = struct
|
||||
type t = {
|
||||
seekable : bool;
|
||||
mutable release_hook : Switch.hook; (* Use this on close to remove switch's [on_release] hook. *)
|
||||
mutable release_hook : Eio.Hook.t; (* Use this on close to remove switch's [on_release] hook. *)
|
||||
mutable fd : [`Open of Unix.file_descr | `Closed]
|
||||
}
|
||||
|
||||
@ -59,7 +59,7 @@ module FD = struct
|
||||
Ctf.label "close";
|
||||
let fd = get "close" t in
|
||||
t.fd <- `Closed;
|
||||
Switch.remove_hook t.release_hook;
|
||||
Eio.Hook.remove t.release_hook;
|
||||
let res = perform (Close fd) in
|
||||
Log.debug (fun l -> l "close: woken up");
|
||||
if res < 0 then
|
||||
@ -76,7 +76,7 @@ module FD = struct
|
||||
let to_unix = get "to_unix"
|
||||
|
||||
let of_unix_no_hook ~seekable fd =
|
||||
{ seekable; fd = `Open fd; release_hook = Switch.null_hook }
|
||||
{ seekable; fd = `Open fd; release_hook = Eio.Hook.null }
|
||||
|
||||
let of_unix ~sw ~seekable fd =
|
||||
let t = of_unix_no_hook ~seekable fd in
|
||||
@ -97,7 +97,7 @@ type rw_req = {
|
||||
action : int Suspended.t;
|
||||
}
|
||||
|
||||
type cancel_hook = Switch.hook ref
|
||||
type cancel_hook = Eio.Hook.t ref
|
||||
|
||||
(* Type of user-data attached to jobs. *)
|
||||
type io_job =
|
||||
@ -108,7 +108,6 @@ type io_job =
|
||||
|
||||
type runnable =
|
||||
| Thread : 'a Suspended.t * 'a -> runnable
|
||||
| Thread_checked : unit Suspended.t -> runnable
|
||||
| Failed_thread : 'a Suspended.t * exn -> runnable
|
||||
|
||||
type t = {
|
||||
@ -152,40 +151,40 @@ let cancel job =
|
||||
(* Cancellation
|
||||
|
||||
For operations that can be cancelled we need to attach a callback to the
|
||||
switch to trigger the cancellation, and we need to remove that callback once
|
||||
the operation is complete. The typical sequence is:
|
||||
cancellation context, and we need to remove that callback once the operation
|
||||
is complete. The typical sequence is:
|
||||
|
||||
1. We create an io_job with an empty [cancel_hook] (because we haven't registered it yet).
|
||||
2. We submit the operation, getting back a uring job (needed for cancellation).
|
||||
3. We register a cancellation hook with the switch. The hook uses the uring job to cancel.
|
||||
3. We register a cancellation hook with the context. The hook uses the uring job to cancel.
|
||||
4. We update the [cancel_hook] with the waiter for removing the cancellation hook.
|
||||
This is the reason that [cancel_hook] is mutable.
|
||||
|
||||
When the job completes, we get the cancellation hook from the io_job and
|
||||
ensure it is removed from the switch, as it's no longer needed. The hook
|
||||
must have been set by this point because we don't poll for completions until
|
||||
the above steps have all finished.
|
||||
ensure it is removed, as it's no longer needed. The hook must have been set
|
||||
by this point because we don't poll for completions until the above steps
|
||||
have all finished.
|
||||
|
||||
If the switch is turned off while the operation is running, the switch will start calling
|
||||
the hooks. If it gets to ours before it's removed, we will submit a cancellation request to uring.
|
||||
If the context is cancelled while the operation is running, the hooks will start being called.
|
||||
If it gets to ours before it's removed, we will submit a cancellation request to uring.
|
||||
If the operation completes before Linux processes the cancellation, we get [ENOENT], which we ignore.
|
||||
|
||||
If the switch is turned off before starting then we discontinue the fibre. *)
|
||||
If the context is cancelled before starting then we discontinue the fibre. *)
|
||||
|
||||
(* [with_cancel_hook ~sw ~action st fn] calls [fn] with a fresh cancel hook.
|
||||
When [fn cancel_hook] returns, it registers a cancellation callback with [sw] and stores its handle in [cancel_hook].
|
||||
If [sw] is already off, it schedules [action] to be discontinued.
|
||||
(* [with_cancel_hook ~action st fn] calls [fn] with a fresh cancel hook.
|
||||
When [fn cancel_hook] returns, it registers a cancellation callback with [action] and stores its handle in [cancel_hook].
|
||||
If [action] is already cancelled, it schedules [action] to be discontinued.
|
||||
@return Whether to retry the operation later, once there is space. *)
|
||||
let with_cancel_hook ~action st fn =
|
||||
let release = ref Switch.null_hook in
|
||||
let sw = action.Suspended.fibre.switch in
|
||||
match Switch.get_error sw with
|
||||
let release = ref Eio.Hook.null in
|
||||
let ctx = action.Suspended.fibre.cancel in
|
||||
match Eio.Cancel.get_error ctx with
|
||||
| Some ex -> enqueue_failed_thread st action ex; false
|
||||
| None ->
|
||||
match fn release with
|
||||
| None -> true
|
||||
| Some job ->
|
||||
release := Switch.add_cancel_hook sw (fun _ -> cancel job);
|
||||
release := Eio.Cancel.add_hook ctx (fun _ -> cancel job);
|
||||
false
|
||||
|
||||
let rec submit_rw_req st ({op; file_offset; fd; buf; len; cur_off; action} as req) =
|
||||
@ -344,11 +343,6 @@ let rec schedule ({run_q; sleep_q; mem_q; uring; _} as st) : [`Exit_scheduler] =
|
||||
(* Wakeup any paused fibres *)
|
||||
match Queue.take run_q with
|
||||
| Thread (k, v) -> Suspended.continue k v (* We already have a runnable task *)
|
||||
| Thread_checked k ->
|
||||
begin match Switch.get_error k.fibre.switch with
|
||||
| Some e -> Suspended.discontinue k e
|
||||
| None -> Suspended.continue k ()
|
||||
end
|
||||
| Failed_thread (k, ex) -> Suspended.discontinue k ex
|
||||
| exception Queue.Empty ->
|
||||
let now = Unix.gettimeofday () in
|
||||
@ -392,17 +386,21 @@ and handle_complete st ~runnable result =
|
||||
match runnable with
|
||||
| Read (req, cancel) ->
|
||||
Log.debug (fun l -> l "read returned");
|
||||
Switch.remove_hook !cancel;
|
||||
Eio.Hook.remove !cancel;
|
||||
complete_rw_req st req result
|
||||
| Write (req, cancel) ->
|
||||
Log.debug (fun l -> l "write returned");
|
||||
Switch.remove_hook !cancel;
|
||||
Eio.Hook.remove !cancel;
|
||||
complete_rw_req st req result
|
||||
| Job (k, cancel) ->
|
||||
Switch.remove_hook !cancel;
|
||||
begin match Switch.get_error k.fibre.switch with
|
||||
| Some e -> Suspended.discontinue k e (* If cancelled, report that instead. *)
|
||||
Eio.Hook.remove !cancel;
|
||||
begin match Eio.Cancel.get_error k.fibre.cancel with
|
||||
| None -> Suspended.continue k result
|
||||
| Some e ->
|
||||
(* If cancelled, report that instead.
|
||||
Should we only do this on error, to avoid losing the return value?
|
||||
We already do that with rw jobs. *)
|
||||
Suspended.discontinue k e
|
||||
end
|
||||
| Job_no_cancel k ->
|
||||
Suspended.continue k result
|
||||
@ -410,7 +408,7 @@ and complete_rw_req st ({len; cur_off; action; _} as req) res =
|
||||
match res, len with
|
||||
| 0, _ -> Suspended.discontinue action End_of_file
|
||||
| e, _ when e < 0 ->
|
||||
begin match Switch.get_error action.fibre.switch with
|
||||
begin match Eio.Cancel.get_error action.fibre.cancel with
|
||||
| Some e -> Suspended.discontinue action e (* If cancelled, report that instead. *)
|
||||
| None ->
|
||||
if errno_is_retry e then (
|
||||
@ -584,7 +582,7 @@ let mkdir_beneath ~perm ?dir path =
|
||||
let dir_path = Filename.dirname path in
|
||||
let leaf = Filename.basename path in
|
||||
(* [mkdir] is really an operation on [path]'s parent. Get a reference to that first: *)
|
||||
Switch.top (fun sw ->
|
||||
Switch.run (fun sw ->
|
||||
let parent =
|
||||
wrap_errors path @@ fun () ->
|
||||
openat2 ~sw ~seekable:false ?dir dir_path
|
||||
@ -738,6 +736,7 @@ module Objects = struct
|
||||
method close = FD.close fd
|
||||
|
||||
method accept_sub ~sw ~on_error fn =
|
||||
Switch.check sw;
|
||||
let client, client_addr = accept_loose_fd fd in
|
||||
Fibre.fork_sub_ignore ~sw ~on_error
|
||||
(fun sw ->
|
||||
@ -925,140 +924,115 @@ let run ?(queue_depth=64) ?(block_size=4096) main =
|
||||
let mem_q = Queue.create () in
|
||||
let st = { mem; uring; run_q; io_q; mem_q; sleep_q; io_jobs = 0 } in
|
||||
Log.debug (fun l -> l "starting main thread");
|
||||
let rec fork ~tid ~switch:initial_switch fn =
|
||||
let rec fork ~tid ~cancel:initial_cancel fn =
|
||||
Ctf.note_switch tid;
|
||||
let fibre = { Suspended.tid; switch = initial_switch } in
|
||||
let fibre = { Eio.Private.tid; cancel = initial_cancel } in
|
||||
match_with fn ()
|
||||
{ retc = (fun () -> schedule st);
|
||||
exnc = (fun e -> raise e);
|
||||
effc = fun (type a) (e : a eff) ->
|
||||
match e with
|
||||
| Enter fn ->
|
||||
Some (fun k ->
|
||||
begin match Switch.get_error fibre.switch with
|
||||
| Some e -> discontinue k e
|
||||
| None ->
|
||||
let k = { Suspended.k; fibre } in
|
||||
fn st k;
|
||||
schedule st
|
||||
end)
|
||||
| Enter_unchecked fn ->
|
||||
Some (fun k ->
|
||||
{ retc = (fun () -> schedule st);
|
||||
exnc = raise;
|
||||
effc = fun (type a) (e : a eff) ->
|
||||
match e with
|
||||
| Enter fn -> Some (fun k ->
|
||||
match Eio.Cancel.get_error fibre.cancel with
|
||||
| Some e -> discontinue k e
|
||||
| None ->
|
||||
let k = { Suspended.k; fibre } in
|
||||
fn st k;
|
||||
schedule st
|
||||
)
|
||||
| Enter_unchecked fn -> Some (fun k ->
|
||||
let k = { Suspended.k; fibre } in
|
||||
fn st k;
|
||||
schedule st
|
||||
)
|
||||
| ERead args ->
|
||||
Some (fun k ->
|
||||
let k = { Suspended.k; fibre } in
|
||||
enqueue_read st k args;
|
||||
schedule st)
|
||||
| Close fd ->
|
||||
Some (fun k ->
|
||||
let k = { Suspended.k; fibre } in
|
||||
enqueue_close st k fd;
|
||||
schedule st)
|
||||
| EWrite args ->
|
||||
Some (fun k ->
|
||||
let k = { Suspended.k; fibre } in
|
||||
enqueue_write st k args;
|
||||
schedule st)
|
||||
| Sleep_until time ->
|
||||
Some (fun k ->
|
||||
let k = { Suspended.k; fibre } in
|
||||
let cancel_hook = ref Switch.null_hook in
|
||||
let sw = fibre.switch in
|
||||
begin match Switch.get_error sw with
|
||||
| ERead args -> Some (fun k ->
|
||||
let k = { Suspended.k; fibre } in
|
||||
enqueue_read st k args;
|
||||
schedule st)
|
||||
| Close fd -> Some (fun k ->
|
||||
let k = { Suspended.k; fibre } in
|
||||
enqueue_close st k fd;
|
||||
schedule st
|
||||
)
|
||||
| EWrite args -> Some (fun k ->
|
||||
let k = { Suspended.k; fibre } in
|
||||
enqueue_write st k args;
|
||||
schedule st
|
||||
)
|
||||
| Sleep_until time -> Some (fun k ->
|
||||
let k = { Suspended.k; fibre } in
|
||||
match Eio.Cancel.get_error fibre.cancel with
|
||||
| Some ex -> Suspended.discontinue k ex
|
||||
| None ->
|
||||
let cancel_hook = ref Eio.Hook.null in
|
||||
let job = Zzz.add ~cancel_hook sleep_q time k in
|
||||
cancel_hook := Switch.add_cancel_hook sw (fun ex ->
|
||||
cancel_hook := Eio.Cancel.add_hook fibre.cancel (fun ex ->
|
||||
Zzz.remove sleep_q job;
|
||||
enqueue_failed_thread st k ex
|
||||
);
|
||||
schedule st
|
||||
end)
|
||||
| Eio.Private.Effects.Set_switch switch ->
|
||||
Some (fun k ->
|
||||
let old = fibre.switch in
|
||||
fibre.switch <- switch;
|
||||
)
|
||||
| Eio.Private.Effects.Set_cancel cancel -> Some (fun k ->
|
||||
let old = fibre.cancel in
|
||||
fibre.cancel <- cancel;
|
||||
continue k old
|
||||
)
|
||||
| Eio.Private.Effects.Yield ->
|
||||
Some (fun k ->
|
||||
| Eio.Private.Effects.Suspend f -> Some (fun k ->
|
||||
let k = { Suspended.k; fibre } in
|
||||
Queue.push (Thread_checked k) st.run_q;
|
||||
schedule st
|
||||
)
|
||||
| Eio.Private.Effects.Suspend_unchecked f ->
|
||||
Some (fun k ->
|
||||
let k = { Suspended.k; fibre } in
|
||||
f tid (function
|
||||
f fibre (function
|
||||
| Ok v -> enqueue_thread st k v
|
||||
| Error ex -> enqueue_failed_thread st k ex
|
||||
);
|
||||
schedule st
|
||||
)
|
||||
| Eio.Private.Effects.Suspend f ->
|
||||
Some (fun k ->
|
||||
match Switch.get_error fibre.switch with
|
||||
| Some e -> discontinue k e
|
||||
| None ->
|
||||
let k = { Suspended.k; fibre } in
|
||||
f tid (function
|
||||
| Ok v -> enqueue_thread st k v
|
||||
| Error ex -> enqueue_failed_thread st k ex
|
||||
);
|
||||
schedule st
|
||||
| Eio.Private.Effects.Fork f -> Some (fun k ->
|
||||
let k = { Suspended.k; fibre } in
|
||||
let id = Ctf.mint_id () in
|
||||
Ctf.note_created id Ctf.Task;
|
||||
let promise, resolver = Promise.create_with_id id in
|
||||
enqueue_thread st k promise;
|
||||
fork
|
||||
~tid:id
|
||||
~cancel:fibre.cancel
|
||||
(fun () ->
|
||||
match f () with
|
||||
| x -> Promise.fulfill resolver x
|
||||
| exception ex ->
|
||||
Log.debug (fun f -> f "Forked fibre failed: %a" Fmt.exn ex);
|
||||
Promise.break resolver ex
|
||||
)
|
||||
)
|
||||
| Eio.Private.Effects.Fork f ->
|
||||
Some (fun k ->
|
||||
let k = { Suspended.k; fibre } in
|
||||
let id = Ctf.mint_id () in
|
||||
Ctf.note_created id Ctf.Task;
|
||||
let promise, resolver = Promise.create_with_id id in
|
||||
enqueue_thread st k promise;
|
||||
fork
|
||||
~tid:id
|
||||
~switch:fibre.switch
|
||||
(fun () ->
|
||||
match f () with
|
||||
| x -> Promise.fulfill resolver x
|
||||
| exception ex ->
|
||||
Log.debug (fun f -> f "Forked fibre failed: %a" Fmt.exn ex);
|
||||
Promise.break resolver ex
|
||||
))
|
||||
| Eio.Private.Effects.Fork_ignore f ->
|
||||
Some (fun k ->
|
||||
let k = { Suspended.k; fibre } in
|
||||
enqueue_thread st k ();
|
||||
let child = Ctf.note_fork () in
|
||||
Ctf.note_switch child;
|
||||
fork ~tid:child ~switch:fibre.switch (fun () ->
|
||||
match f () with
|
||||
| () ->
|
||||
Ctf.note_resolved child ~ex:None
|
||||
| exception ex ->
|
||||
Ctf.note_resolved child ~ex:(Some ex)
|
||||
))
|
||||
| Eio.Private.Effects.Trace -> Some (fun k -> continue k Eunix.Trace.default_traceln)
|
||||
| Alloc ->
|
||||
Some (fun k ->
|
||||
let k = { Suspended.k; fibre } in
|
||||
alloc_buf st k)
|
||||
| Free buf ->
|
||||
Some (fun k ->
|
||||
free_buf st buf;
|
||||
continue k ())
|
||||
| _ -> None
|
||||
}
|
||||
| Eio.Private.Effects.Fork_ignore f -> Some (fun k ->
|
||||
let k = { Suspended.k; fibre } in
|
||||
enqueue_thread st k ();
|
||||
let child = Ctf.note_fork () in
|
||||
Ctf.note_switch child;
|
||||
fork ~tid:child ~cancel:fibre.cancel (fun () ->
|
||||
match f () with
|
||||
| () ->
|
||||
Ctf.note_resolved child ~ex:None
|
||||
| exception ex ->
|
||||
Ctf.note_resolved child ~ex:(Some ex)
|
||||
)
|
||||
)
|
||||
| Eio.Private.Effects.Trace -> Some (fun k -> continue k Eunix.Trace.default_traceln)
|
||||
| Alloc -> Some (fun k ->
|
||||
let k = { Suspended.k; fibre } in
|
||||
alloc_buf st k
|
||||
)
|
||||
| Free buf -> Some (fun k ->
|
||||
free_buf st buf;
|
||||
continue k ()
|
||||
)
|
||||
| _ -> None
|
||||
}
|
||||
in
|
||||
let main_done = ref false in
|
||||
let `Exit_scheduler =
|
||||
fork ~tid:(Ctf.mint_id ()) ~switch:Eio.Private.boot_switch (fun () ->
|
||||
Fun.protect (fun () -> Switch.top (fun _sw -> main stdenv))
|
||||
fork ~tid:(Ctf.mint_id ()) ~cancel:Eio.Private.boot_cancel (fun () ->
|
||||
Fun.protect (fun () -> Eio.Cancel.protect (fun () -> main stdenv))
|
||||
~finally:(fun () -> main_done := true)
|
||||
) in
|
||||
) in
|
||||
if not !main_done then
|
||||
failwith "Deadlock detected: no events scheduled but main function hasn't returned";
|
||||
Log.debug (fun l -> l "exit")
|
||||
|
@ -48,8 +48,7 @@ val noop : unit -> unit
|
||||
(** {1 Time functions} *)
|
||||
|
||||
val sleep_until : float -> unit
|
||||
(** [sleep_until time] blocks until the current time is [time].
|
||||
@param sw Cancel the sleep if [sw] is turned off. *)
|
||||
(** [sleep_until time] blocks until the current time is [time]. *)
|
||||
|
||||
(** {1 Memory allocation functions} *)
|
||||
|
||||
|
@ -12,7 +12,7 @@ let setup_log level =
|
||||
let () =
|
||||
setup_log (Some Logs.Debug);
|
||||
run @@ fun _stdenv ->
|
||||
Switch.top @@ fun sw ->
|
||||
Switch.run @@ fun sw ->
|
||||
let fd = Unix.handle_unix_error (openfile ~sw "test.txt" Unix.[O_RDONLY]) 0 in
|
||||
let buf = alloc () in
|
||||
let _ = read_exactly fd buf 5 in
|
||||
|
@ -14,7 +14,7 @@ let read_then_write_chunk infd outfd file_offset len =
|
||||
U.free buf
|
||||
|
||||
let copy_file infd outfd insize block_size =
|
||||
Switch.top @@ fun sw ->
|
||||
Switch.run @@ fun sw ->
|
||||
let rec copy_block file_offset =
|
||||
let remaining = Int63.(sub insize file_offset) in
|
||||
if remaining <> Int63.zero then (
|
||||
@ -27,7 +27,7 @@ let copy_file infd outfd insize block_size =
|
||||
|
||||
let run_cp block_size queue_depth infile outfile () =
|
||||
U.run ~queue_depth ~block_size @@ fun _stdenv ->
|
||||
Switch.top @@ fun sw ->
|
||||
Switch.run @@ fun sw ->
|
||||
let open Unix in
|
||||
let infd = Eio_linux.openfile ~sw infile [O_RDONLY] 0 in
|
||||
let outfd = Eio_linux.openfile ~sw outfile [O_WRONLY; O_CREAT; O_TRUNC] 0o644 in
|
||||
|
@ -17,7 +17,7 @@ let read_one_byte ~sw r =
|
||||
|
||||
let test_poll_add () =
|
||||
Eio_linux.run @@ fun _stdenv ->
|
||||
Switch.top @@ fun sw ->
|
||||
Switch.run @@ fun sw ->
|
||||
let r, w = Eio_linux.pipe sw in
|
||||
let thread = read_one_byte ~sw r in
|
||||
Fibre.yield ();
|
||||
@ -30,7 +30,7 @@ let test_poll_add () =
|
||||
|
||||
let test_poll_add_busy () =
|
||||
Eio_linux.run ~queue_depth:1 @@ fun _stdenv ->
|
||||
Switch.top @@ fun sw ->
|
||||
Switch.run @@ fun sw ->
|
||||
let r, w = Eio_linux.pipe sw in
|
||||
let a = read_one_byte ~sw r in
|
||||
let b = read_one_byte ~sw r in
|
||||
@ -46,11 +46,11 @@ let test_poll_add_busy () =
|
||||
(* Write a string to a pipe and read it out again. *)
|
||||
let test_copy () =
|
||||
Eio_linux.run ~queue_depth:2 @@ fun _stdenv ->
|
||||
Switch.top @@ fun sw ->
|
||||
Switch.run @@ fun sw ->
|
||||
let msg = "Hello!" in
|
||||
let from_pipe, to_pipe = Eio_linux.pipe sw in
|
||||
let buffer = Buffer.create 20 in
|
||||
Fibre.both ~sw
|
||||
Fibre.both
|
||||
(fun () -> Eio.Flow.copy from_pipe (Eio.Flow.buffer_sink buffer))
|
||||
(fun () ->
|
||||
Eio.Flow.copy (Eio.Flow.string_source msg) to_pipe;
|
||||
@ -63,13 +63,13 @@ let test_copy () =
|
||||
(* Write a string via 2 pipes. The copy from the 1st to 2nd pipe will be optimised and so tests a different code-path. *)
|
||||
let test_direct_copy () =
|
||||
Eio_linux.run ~queue_depth:4 @@ fun _stdenv ->
|
||||
Switch.top @@ fun sw ->
|
||||
Switch.run @@ fun sw ->
|
||||
let msg = "Hello!" in
|
||||
let from_pipe1, to_pipe1 = Eio_linux.pipe sw in
|
||||
let from_pipe2, to_pipe2 = Eio_linux.pipe sw in
|
||||
let buffer = Buffer.create 20 in
|
||||
let to_output = Eio.Flow.buffer_sink buffer in
|
||||
Switch.top (fun sw ->
|
||||
Switch.run (fun sw ->
|
||||
Fibre.fork_ignore ~sw (fun () -> Ctf.label "copy1"; Eio.Flow.copy from_pipe1 to_pipe2; Eio.Flow.close to_pipe2);
|
||||
Fibre.fork_ignore ~sw (fun () -> Ctf.label "copy2"; Eio.Flow.copy from_pipe2 to_output);
|
||||
Eio.Flow.copy (Eio.Flow.string_source msg) to_pipe1;
|
||||
@ -82,7 +82,7 @@ let test_direct_copy () =
|
||||
(* Read and write using IO vectors rather than the fixed buffers. *)
|
||||
let test_iovec () =
|
||||
Eio_linux.run ~queue_depth:4 @@ fun _stdenv ->
|
||||
Switch.top @@ fun sw ->
|
||||
Switch.run @@ fun sw ->
|
||||
let from_pipe, to_pipe = Eio_linux.pipe sw in
|
||||
let from_pipe = Eio_linux.Objects.get_fd from_pipe in
|
||||
let to_pipe = Eio_linux.Objects.get_fd to_pipe in
|
||||
@ -93,7 +93,7 @@ let test_iovec () =
|
||||
let got = Eio_linux.readv from_pipe cs in
|
||||
recv (Cstruct.shiftv cs got)
|
||||
in
|
||||
Fibre.both ~sw
|
||||
Fibre.both
|
||||
(fun () -> recv [Cstruct.sub message 5 3; Cstruct.sub message 15 3])
|
||||
(fun () ->
|
||||
let b = Cstruct.of_string "barfoo" in
|
||||
|
@ -50,7 +50,7 @@ let or_raise_path path = function
|
||||
|
||||
module Suspended = struct
|
||||
type 'a t = {
|
||||
fibre : Eunix.Suspended.state;
|
||||
fibre : Eio.Private.context;
|
||||
k : ('a, unit) continuation;
|
||||
}
|
||||
|
||||
@ -67,7 +67,7 @@ module Suspended = struct
|
||||
| Error x -> discontinue t x
|
||||
end
|
||||
|
||||
type _ eff += Await : (Eunix.Suspended.state -> ('a -> unit) -> unit) -> 'a eff
|
||||
type _ eff += Await : (Eio.Private.context -> ('a -> unit) -> unit) -> 'a eff
|
||||
let await fn = perform (Await fn)
|
||||
|
||||
type _ eff += Enter : ('a Suspended.t -> unit) -> 'a eff
|
||||
@ -92,16 +92,16 @@ let enqueue_failed_thread k ex =
|
||||
Luv.Timer.start yield 0 (fun () -> Suspended.discontinue k ex) |> or_raise
|
||||
|
||||
let with_cancel fibre ~request fn =
|
||||
let cancel = Switch.add_cancel_hook fibre.Eunix.Suspended.switch (fun _ ->
|
||||
let cancel = Eio.Cancel.add_hook fibre.Eio.Private.cancel (fun _ ->
|
||||
match Luv.Request.cancel request with
|
||||
| Ok () -> ()
|
||||
| Error e -> Log.debug (fun f -> f "Cancel failed: %s" (Luv.Error.strerror e))
|
||||
) in
|
||||
Fun.protect fn ~finally:(fun () -> Switch.remove_hook cancel)
|
||||
Fun.protect fn ~finally:(fun () -> Eio.Hook.remove cancel)
|
||||
|
||||
module Handle = struct
|
||||
type 'a t = {
|
||||
mutable release_hook : Switch.hook; (* Use this on close to remove switch's [on_release] hook. *)
|
||||
mutable release_hook : Eio.Hook.t; (* Use this on close to remove switch's [on_release] hook. *)
|
||||
mutable fd : [`Open of 'a Luv.Handle.t | `Closed]
|
||||
}
|
||||
|
||||
@ -117,7 +117,7 @@ module Handle = struct
|
||||
Ctf.label "close";
|
||||
let fd = get "close" t in
|
||||
t.fd <- `Closed;
|
||||
Switch.remove_hook t.release_hook;
|
||||
Eio.Hook.remove t.release_hook;
|
||||
enter_unchecked @@ fun k ->
|
||||
Luv.Handle.close fd (Suspended.continue k)
|
||||
|
||||
@ -127,7 +127,7 @@ module Handle = struct
|
||||
let to_luv x = get "to_luv" x
|
||||
|
||||
let of_luv_no_hook fd =
|
||||
{ fd = `Open fd; release_hook = Switch.null_hook }
|
||||
{ fd = `Open fd; release_hook = Eio.Hook.null }
|
||||
|
||||
let of_luv ~sw fd =
|
||||
let t = of_luv_no_hook fd in
|
||||
@ -137,7 +137,7 @@ end
|
||||
|
||||
module File = struct
|
||||
type t = {
|
||||
mutable release_hook : Switch.hook; (* Use this on close to remove switch's [on_release] hook. *)
|
||||
mutable release_hook : Eio.Hook.t; (* Use this on close to remove switch's [on_release] hook. *)
|
||||
mutable fd : [`Open of Luv.File.t | `Closed]
|
||||
}
|
||||
|
||||
@ -153,7 +153,7 @@ module File = struct
|
||||
Ctf.label "close";
|
||||
let fd = get "close" t in
|
||||
t.fd <- `Closed;
|
||||
Switch.remove_hook t.release_hook;
|
||||
Eio.Hook.remove t.release_hook;
|
||||
await_exn (fun _fibre -> Luv.File.close fd)
|
||||
|
||||
let ensure_closed t =
|
||||
@ -162,7 +162,7 @@ module File = struct
|
||||
let to_luv = get "to_luv"
|
||||
|
||||
let of_luv_no_hook fd =
|
||||
{ fd = `Open fd; release_hook = Switch.null_hook }
|
||||
{ fd = `Open fd; release_hook = Eio.Hook.null }
|
||||
|
||||
let of_luv ~sw fd =
|
||||
let t = of_luv_no_hook fd in
|
||||
@ -207,12 +207,12 @@ module Stream = struct
|
||||
|
||||
let rec read_into (sock:'a t) buf =
|
||||
let r = enter (fun k ->
|
||||
let cancel = Switch.add_cancel_hook k.fibre.switch (fun ex ->
|
||||
let cancel = Eio.Cancel.add_hook k.fibre.cancel (fun ex ->
|
||||
Luv.Stream.read_stop (Handle.get "read_into:cancel" sock) |> or_raise;
|
||||
enqueue_failed_thread k (Switch.Cancelled ex)
|
||||
enqueue_failed_thread k (Eio.Cancel.Cancelled ex)
|
||||
) in
|
||||
Luv.Stream.read_start (Handle.get "read_start" sock) ~allocate:(fun _ -> buf) (fun r ->
|
||||
Switch.remove_hook cancel;
|
||||
Eio.Hook.remove cancel;
|
||||
Luv.Stream.read_stop (Handle.get "read_stop" sock) |> or_raise;
|
||||
Suspended.continue k r
|
||||
)
|
||||
@ -248,13 +248,13 @@ let sleep_until due =
|
||||
let delay = 1000. *. (due -. Unix.gettimeofday ()) |> ceil |> truncate |> max 0 in
|
||||
let timer = Luv.Timer.init () |> or_raise in
|
||||
enter @@ fun k ->
|
||||
let cancel = Switch.add_cancel_hook k.fibre.switch (fun ex ->
|
||||
let cancel = Eio.Cancel.add_hook k.fibre.cancel (fun ex ->
|
||||
Luv.Timer.stop timer |> or_raise;
|
||||
Luv.Handle.close timer (fun () -> ());
|
||||
enqueue_failed_thread k ex
|
||||
) in
|
||||
Luv.Timer.start timer delay (fun () ->
|
||||
Switch.remove_hook cancel;
|
||||
Eio.Hook.remove cancel;
|
||||
Suspended.continue k ()
|
||||
) |> or_raise
|
||||
|
||||
@ -351,7 +351,7 @@ module Objects = struct
|
||||
method close = Handle.close sock
|
||||
|
||||
method accept_sub ~sw ~on_error fn =
|
||||
Eio.Semaphore.acquire ~sw ready;
|
||||
Eio.Semaphore.acquire ready;
|
||||
let client = self#make_client |> Handle.of_luv_no_hook in
|
||||
match Luv.Stream.accept ~server:(Handle.get "accept" sock) ~client:(Handle.get "accept" client) with
|
||||
| Error e ->
|
||||
@ -583,9 +583,9 @@ end
|
||||
let run main =
|
||||
Log.debug (fun l -> l "starting run");
|
||||
let stdenv = Objects.stdenv () in
|
||||
let rec fork ~tid ~switch:initial_switch fn =
|
||||
let rec fork ~tid ~cancel:initial_cancel fn =
|
||||
Ctf.note_switch tid;
|
||||
let fibre = { Eunix.Suspended.tid; switch = initial_switch } in
|
||||
let fibre = { Eio.Private.tid; cancel = initial_cancel } in
|
||||
match_with fn ()
|
||||
{ retc = (fun () -> ());
|
||||
exnc = (fun e -> raise e);
|
||||
@ -606,7 +606,7 @@ let run main =
|
||||
enqueue_thread k promise;
|
||||
fork
|
||||
~tid:id
|
||||
~switch:fibre.switch
|
||||
~cancel:fibre.cancel
|
||||
(fun () ->
|
||||
match f () with
|
||||
| x -> Promise.fulfill resolver x
|
||||
@ -620,56 +620,38 @@ let run main =
|
||||
enqueue_thread k ();
|
||||
let child = Ctf.note_fork () in
|
||||
Ctf.note_switch child;
|
||||
fork ~tid:child ~switch:fibre.switch (fun () ->
|
||||
fork ~tid:child ~cancel:fibre.cancel (fun () ->
|
||||
match f () with
|
||||
| () ->
|
||||
Ctf.note_resolved child ~ex:None
|
||||
| exception ex ->
|
||||
Ctf.note_resolved child ~ex:(Some ex)
|
||||
))
|
||||
| Eio.Private.Effects.Set_switch switch ->
|
||||
| Eio.Private.Effects.Set_cancel cancel ->
|
||||
Some (fun k ->
|
||||
let old = fibre.switch in
|
||||
fibre.switch <- switch;
|
||||
let old = fibre.cancel in
|
||||
fibre.cancel <- cancel;
|
||||
continue k old
|
||||
)
|
||||
| Enter_unchecked fn -> Some (fun k ->
|
||||
fn { Suspended.k; fibre }
|
||||
)
|
||||
| Enter fn -> Some (fun k ->
|
||||
match Switch.get_error fibre.switch with
|
||||
match Eio.Cancel.get_error fibre.cancel with
|
||||
| Some e -> discontinue k e
|
||||
| None -> fn { Suspended.k; fibre }
|
||||
)
|
||||
| Eio.Private.Effects.Yield ->
|
||||
Some (fun k ->
|
||||
let yield = Luv.Timer.init () |> or_raise in
|
||||
Luv.Timer.start yield 0 (fun () ->
|
||||
match Switch.get_error fibre.switch with
|
||||
| Some e -> discontinue k e
|
||||
| None -> continue k ()
|
||||
)
|
||||
|> or_raise
|
||||
)
|
||||
| Eio.Private.Effects.Suspend_unchecked fn ->
|
||||
Some (fun k ->
|
||||
let k = { Suspended.k; fibre } in
|
||||
fn tid (enqueue_result_thread k)
|
||||
)
|
||||
| Eio.Private.Effects.Suspend fn ->
|
||||
Some (fun k ->
|
||||
begin match Switch.get_error fibre.switch with
|
||||
| Some e -> discontinue k e
|
||||
| None ->
|
||||
let k = { Suspended.k; fibre } in
|
||||
fn tid (enqueue_result_thread k)
|
||||
end)
|
||||
let k = { Suspended.k; fibre } in
|
||||
fn fibre (enqueue_result_thread k)
|
||||
)
|
||||
| _ -> None
|
||||
}
|
||||
in
|
||||
let main_status = ref `Running in
|
||||
fork ~tid:(Ctf.mint_id ()) ~switch:Eio.Private.boot_switch (fun () ->
|
||||
match Switch.top (fun _sw -> main stdenv) with
|
||||
fork ~tid:(Ctf.mint_id ()) ~cancel:Eio.Private.boot_cancel (fun () ->
|
||||
match Eio.Cancel.protect (fun () -> main stdenv) with
|
||||
| () -> main_status := `Done
|
||||
| exception ex -> main_status := `Ex (ex, Printexc.get_raw_backtrace ())
|
||||
);
|
||||
|
@ -23,7 +23,7 @@ exception Luv_error of Luv.Error.t
|
||||
val or_raise : 'a or_error -> 'a
|
||||
(** [or_error (Error e)] raises [Luv_error e]. *)
|
||||
|
||||
val await : (Eunix.Suspended.state -> ('a -> unit) -> unit) -> 'a
|
||||
val await : (Eio.Private.context -> ('a -> unit) -> unit) -> 'a
|
||||
(** [await fn] converts a function using a luv-style callback to one using effects.
|
||||
Use it as e.g. [await (fun fibre -> Luv.File.realpath path)].
|
||||
Use [fibre] to implement cancellation. *)
|
||||
|
@ -32,7 +32,7 @@ Hello, world!
|
||||
|
||||
```ocaml
|
||||
let main _stdenv =
|
||||
Switch.top @@ fun sw ->
|
||||
Switch.run @@ fun sw ->
|
||||
let fd = Eio_luv.File.open_ ~sw "/dev/zero" [] |> Eio_luv.or_raise in
|
||||
let buf = Luv.Buffer.create 4 in
|
||||
read_exactly fd buf;
|
||||
|
@ -1,12 +1,7 @@
|
||||
open EffectHandlers.Deep
|
||||
|
||||
type state = {
|
||||
tid : Ctf.id;
|
||||
mutable switch : Eio.Std.Switch.t;
|
||||
}
|
||||
|
||||
type 'a t = {
|
||||
fibre : state;
|
||||
fibre : Eio.Private.context;
|
||||
k : ('a, [`Exit_scheduler]) continuation;
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,5 @@
|
||||
(** Keep track of scheduled alarms. *)
|
||||
|
||||
open Eio.Std
|
||||
|
||||
module Key = struct
|
||||
type t = Optint.Int63.t
|
||||
let compare = Optint.Int63.compare
|
||||
@ -11,7 +9,7 @@ module Job = struct
|
||||
type t = {
|
||||
time : float;
|
||||
thread : unit Suspended.t;
|
||||
cancel_hook : Switch.hook ref;
|
||||
cancel_hook : Eio.Hook.t ref;
|
||||
}
|
||||
|
||||
let compare a b = Float.compare a.time b.time
|
||||
@ -39,7 +37,7 @@ let remove t id =
|
||||
let pop t ~now =
|
||||
match Q.min t.sleep_queue with
|
||||
| Some (_, { Job.time; thread; cancel_hook }) when time <= now ->
|
||||
Switch.remove_hook !cancel_hook;
|
||||
Eio.Hook.remove !cancel_hook;
|
||||
t.sleep_queue <- Option.get (Q.rest t.sleep_queue);
|
||||
`Due thread
|
||||
| Some (_, { Job.time; _ }) -> `Wait_until time
|
||||
|
@ -1,5 +1,3 @@
|
||||
open Eio.Std
|
||||
|
||||
module Key : sig
|
||||
type t
|
||||
end
|
||||
@ -10,7 +8,7 @@ type t
|
||||
val create : unit -> t
|
||||
(** [create ()] is a fresh empty queue. *)
|
||||
|
||||
val add : cancel_hook:Switch.hook ref -> t -> float -> unit Suspended.t -> Key.t
|
||||
val add : cancel_hook:Eio.Hook.t ref -> t -> float -> unit Suspended.t -> Key.t
|
||||
(** [add ~cancel_hook t time thread] adds a new event, due at [time], and returns its ID.
|
||||
[cancel_hook] will be released when the event is later returned by {!pop}. *)
|
||||
|
||||
|
@ -38,10 +38,9 @@ Here, we use a mutex to check that the parent domain really did run while waitin
|
||||
|
||||
```ocaml
|
||||
# run @@ fun mgr ->
|
||||
Switch.top @@ fun sw ->
|
||||
let mutex = Stdlib.Mutex.create () in
|
||||
Mutex.lock mutex;
|
||||
Fibre.both ~sw
|
||||
Fibre.both
|
||||
(fun () ->
|
||||
traceln "Spawning new domain...";
|
||||
let response = Eio.Domain_manager.run_compute_unsafe mgr (fun () ->
|
||||
|
@ -17,10 +17,9 @@ let () =
|
||||
|
||||
open Eio.Std
|
||||
|
||||
let run (fn : sw:Switch.t -> Eio.Stdenv.t -> unit) =
|
||||
let run (fn : Eio.Stdenv.t -> unit) =
|
||||
Eio_main.run @@ fun env ->
|
||||
Switch.top @@ fun sw ->
|
||||
fn ~sw env
|
||||
fn env
|
||||
|
||||
let read_all flow =
|
||||
let b = Buffer.create 100 in
|
||||
@ -53,7 +52,7 @@ let chdir path =
|
||||
|
||||
Creating a file and reading it back:
|
||||
```ocaml
|
||||
# run @@ fun ~sw env ->
|
||||
# run @@ fun env ->
|
||||
let cwd = Eio.Stdenv.cwd env in
|
||||
write_file ~create:(`Exclusive 0o666) cwd "test-file" "my-data";
|
||||
traceln "Got %S" @@ read_file cwd "test-file";;
|
||||
@ -72,7 +71,7 @@ Perm = 644
|
||||
|
||||
Trying to use cwd to access a file outside of that subtree fails:
|
||||
```ocaml
|
||||
# run @@ fun ~sw env ->
|
||||
# run @@ fun env ->
|
||||
let cwd = Eio.Stdenv.cwd env in
|
||||
write_file ~create:(`Exclusive 0o666) cwd "../test-file" "my-data";
|
||||
failwith "Should have failed";;
|
||||
@ -81,7 +80,7 @@ Exception: Eio.Dir.Permission_denied ("../test-file", _)
|
||||
|
||||
Trying to use cwd to access an absolute path fails:
|
||||
```ocaml
|
||||
# run @@ fun ~sw env ->
|
||||
# run @@ fun env ->
|
||||
let cwd = Eio.Stdenv.cwd env in
|
||||
write_file ~create:(`Exclusive 0o666) cwd "/tmp/test-file" "my-data";
|
||||
failwith "Should have failed";;
|
||||
@ -92,7 +91,7 @@ Exception: Eio.Dir.Permission_denied ("/tmp/test-file", _)
|
||||
|
||||
Exclusive create fails if already exists:
|
||||
```ocaml
|
||||
# run @@ fun ~sw env ->
|
||||
# run @@ fun env ->
|
||||
let cwd = Eio.Stdenv.cwd env in
|
||||
write_file ~create:(`Exclusive 0o666) cwd "test-file" "first-write";
|
||||
write_file ~create:(`Exclusive 0o666) cwd "test-file" "first-write";
|
||||
@ -102,7 +101,7 @@ Exception: Eio.Dir.Already_exists ("test-file", _)
|
||||
|
||||
If-missing create succeeds if already exists:
|
||||
```ocaml
|
||||
# run @@ fun ~sw env ->
|
||||
# run @@ fun env ->
|
||||
let cwd = Eio.Stdenv.cwd env in
|
||||
write_file ~create:(`If_missing 0o666) cwd "test-file" "1st-write-original";
|
||||
write_file ~create:(`If_missing 0o666) cwd "test-file" "2nd-write";
|
||||
@ -113,7 +112,7 @@ If-missing create succeeds if already exists:
|
||||
|
||||
Truncate create succeeds if already exists, and truncates:
|
||||
```ocaml
|
||||
# run @@ fun ~sw env ->
|
||||
# run @@ fun env ->
|
||||
let cwd = Eio.Stdenv.cwd env in
|
||||
write_file ~create:(`Or_truncate 0o666) cwd "test-file" "1st-write-original";
|
||||
write_file ~create:(`Or_truncate 0o666) cwd "test-file" "2nd-write";
|
||||
@ -126,7 +125,7 @@ Truncate create succeeds if already exists, and truncates:
|
||||
|
||||
Error if no create and doesn't exist:
|
||||
```ocaml
|
||||
# run @@ fun ~sw env ->
|
||||
# run @@ fun env ->
|
||||
let cwd = Eio.Stdenv.cwd env in
|
||||
write_file ~create:`Never cwd "test-file" "1st-write-original";
|
||||
traceln "Got %S" @@ read_file cwd "test-file";;
|
||||
@ -135,7 +134,7 @@ Exception: Eio.Dir.Not_found ("test-file", _)
|
||||
|
||||
Appending to an existing file:
|
||||
```ocaml
|
||||
# run @@ fun ~sw env ->
|
||||
# run @@ fun env ->
|
||||
let cwd = Eio.Stdenv.cwd env in
|
||||
write_file ~create:(`Or_truncate 0o666) cwd "test-file" "1st-write-original";
|
||||
write_file ~create:`Never ~append:true cwd "test-file" "2nd-write";
|
||||
@ -149,7 +148,7 @@ Appending to an existing file:
|
||||
# Mkdir
|
||||
|
||||
```ocaml
|
||||
# run @@ fun ~sw env ->
|
||||
# run @@ fun env ->
|
||||
let cwd = Eio.Stdenv.cwd env in
|
||||
try_mkdir cwd "subdir";
|
||||
try_mkdir cwd "subdir/nested";
|
||||
@ -170,7 +169,7 @@ Creating directories with nesting, symlinks, etc:
|
||||
- : unit = ()
|
||||
# Unix.symlink "foo" "dangle";;
|
||||
- : unit = ()
|
||||
# run @@ fun ~sw env ->
|
||||
# run @@ fun env ->
|
||||
let cwd = Eio.Stdenv.cwd env in
|
||||
try_mkdir cwd "subdir";
|
||||
try_mkdir cwd "to-subdir/nested";
|
||||
@ -192,7 +191,8 @@ Creating directories with nesting, symlinks, etc:
|
||||
|
||||
Create a sandbox, write a file with it, then read it from outside:
|
||||
```ocaml
|
||||
# run @@ fun ~sw env ->
|
||||
# run @@ fun env ->
|
||||
Switch.run @@ fun sw ->
|
||||
let cwd = Eio.Stdenv.cwd env in
|
||||
try_mkdir cwd "sandbox";
|
||||
let subdir = Eio.Dir.open_dir ~sw cwd "sandbox" in
|
||||
@ -210,7 +210,7 @@ Create a sandbox, write a file with it, then read it from outside:
|
||||
We create a directory and chdir into it.
|
||||
Using `cwd` we can't access the parent, but using `fs` we can:
|
||||
```ocaml
|
||||
# run @@ fun ~sw env ->
|
||||
# run @@ fun env ->
|
||||
let cwd = Eio.Stdenv.cwd env in
|
||||
let fs = Eio.Stdenv.fs env in
|
||||
try_mkdir cwd "fs-test";
|
||||
|
@ -10,7 +10,7 @@ open Eio.Std
|
||||
let run (fn : net:Eio.Net.t -> Switch.t -> unit) =
|
||||
Eio_main.run @@ fun env ->
|
||||
let net = Eio.Stdenv.net env in
|
||||
Switch.top (fn ~net)
|
||||
Switch.run (fn ~net)
|
||||
|
||||
let addr = `Tcp (Unix.inet_addr_loopback, 8081)
|
||||
|
||||
@ -56,7 +56,7 @@ let run_server ~sw socket =
|
||||
|
||||
let test_address addr ~net sw =
|
||||
let server = Eio.Net.listen net ~sw ~reuse_addr:true ~backlog:5 addr in
|
||||
Fibre.both ~sw
|
||||
Fibre.both
|
||||
(fun () -> run_server ~sw server)
|
||||
(fun () ->
|
||||
run_client ~sw ~net ~addr;
|
||||
@ -108,19 +108,18 @@ Cancelling the read:
|
||||
# run @@ fun ~net sw ->
|
||||
let shutdown, set_shutdown = Promise.create () in
|
||||
let server = Eio.Net.listen net ~sw ~reuse_addr:true ~backlog:5 addr in
|
||||
Fibre.both ~sw
|
||||
Fibre.both
|
||||
(fun () ->
|
||||
Eio.Net.accept_sub server ~sw (fun ~sw flow _addr ->
|
||||
try
|
||||
Fibre.both ~sw
|
||||
Fibre.both
|
||||
(fun () -> raise (Promise.await shutdown))
|
||||
(fun () ->
|
||||
let msg = read_all flow in
|
||||
traceln "Server received: %S" msg
|
||||
)
|
||||
with Graceful_shutdown ->
|
||||
Switch.top @@ fun _sw ->
|
||||
Eio.Flow.copy_string "Request cancelled" flow;
|
||||
Eio.Flow.copy_string "Request cancelled" flow
|
||||
) ~on_error:raise
|
||||
)
|
||||
(fun () ->
|
||||
@ -135,7 +134,7 @@ Cancelling the read:
|
||||
+Connecting to server...
|
||||
+Connection opened - cancelling server's read
|
||||
+Client received: "Request cancelled"
|
||||
Exception: Graceful_shutdown.
|
||||
- : unit = ()
|
||||
```
|
||||
|
||||
Calling accept when the switch is already off:
|
||||
|
@ -15,14 +15,14 @@ let run fn =
|
||||
Eio_main.run @@ fun _ ->
|
||||
fn ()
|
||||
|
||||
let add ?sw t v =
|
||||
let add t v =
|
||||
traceln "Adding %d to stream" v;
|
||||
S.add ?sw t v;
|
||||
S.add t v;
|
||||
traceln "Added %d to stream" v
|
||||
|
||||
let take ?sw t =
|
||||
let take t =
|
||||
traceln "Reading from stream";
|
||||
traceln "Got %d from stream" (S.take ?sw t)
|
||||
traceln "Got %d from stream" (S.take t)
|
||||
```
|
||||
|
||||
# Test cases
|
||||
@ -51,10 +51,9 @@ Readers have to wait when the stream is empty:
|
||||
|
||||
```ocaml
|
||||
# run @@ fun () ->
|
||||
Switch.top @@ fun sw ->
|
||||
let t = S.create 2 in
|
||||
add t 1;
|
||||
Fibre.both ~sw
|
||||
Fibre.both
|
||||
(fun () -> take t; take t)
|
||||
(fun () -> add t 2);;
|
||||
+Adding 1 to stream
|
||||
@ -72,10 +71,9 @@ Writers have to wait when the stream is full:
|
||||
|
||||
```ocaml
|
||||
# run @@ fun () ->
|
||||
Switch.top @@ fun sw ->
|
||||
let t = S.create 3 in
|
||||
add t 1;
|
||||
Fibre.both ~sw
|
||||
Fibre.both
|
||||
(fun () ->
|
||||
add t 2;
|
||||
add t 3;
|
||||
@ -110,9 +108,8 @@ A zero-length queue is synchronous:
|
||||
|
||||
```ocaml
|
||||
# run @@ fun () ->
|
||||
Switch.top @@ fun sw ->
|
||||
let t = S.create 0 in
|
||||
Fibre.both ~sw
|
||||
Fibre.both
|
||||
(fun () ->
|
||||
add t 1;
|
||||
add t 2;
|
||||
@ -138,10 +135,9 @@ Cancel reading from a stream:
|
||||
# run @@ fun () ->
|
||||
let t = S.create 1 in
|
||||
try
|
||||
Switch.top @@ fun sw ->
|
||||
Fibre.both ~sw
|
||||
(fun () -> take ~sw t)
|
||||
(fun () -> Switch.turn_off sw Cancel);
|
||||
Fibre.both
|
||||
(fun () -> take t)
|
||||
(fun () -> raise Cancel);
|
||||
assert false;
|
||||
with Cancel ->
|
||||
traceln "Cancelled";
|
||||
@ -162,10 +158,9 @@ Cancel writing to a stream:
|
||||
# run @@ fun () ->
|
||||
let t = S.create 1 in
|
||||
try
|
||||
Switch.top @@ fun sw ->
|
||||
Fibre.both ~sw
|
||||
(fun () -> add ~sw t 1; add ~sw t 2)
|
||||
(fun () -> Switch.turn_off sw Cancel);
|
||||
Fibre.both
|
||||
(fun () -> add t 1; add t 2)
|
||||
(fun () -> raise Cancel);
|
||||
assert false;
|
||||
with Cancel ->
|
||||
traceln "Cancelled";
|
||||
@ -191,17 +186,15 @@ Cancel writing to a zero-length stream:
|
||||
# run @@ fun () ->
|
||||
let t = S.create 0 in
|
||||
try
|
||||
Switch.top @@ fun sw ->
|
||||
Fibre.both ~sw
|
||||
(fun () -> add ~sw t 1)
|
||||
(fun () -> Switch.turn_off sw Cancel);
|
||||
Fibre.both
|
||||
(fun () -> add t 1)
|
||||
(fun () -> raise Cancel);
|
||||
assert false;
|
||||
with Cancel ->
|
||||
traceln "Cancelled";
|
||||
Switch.top @@ fun sw ->
|
||||
Fibre.both ~sw
|
||||
(fun () -> add ~sw t 2)
|
||||
(fun () -> take ~sw t);;
|
||||
Fibre.both
|
||||
(fun () -> add t 2)
|
||||
(fun () -> take t);;
|
||||
+Adding 1 to stream
|
||||
+Cancelled
|
||||
+Adding 2 to stream
|
||||
@ -211,20 +204,20 @@ Cancel writing to a zero-length stream:
|
||||
- : unit = ()
|
||||
```
|
||||
|
||||
Trying to use a stream with a turned-off switch:
|
||||
Trying to use a stream with a cancelled context:
|
||||
|
||||
```ocaml
|
||||
# run @@ fun () ->
|
||||
let t = S.create 0 in
|
||||
Switch.top @@ fun sw ->
|
||||
Switch.turn_off sw Cancel;
|
||||
begin try add ~sw t 1 with ex -> traceln "%a" Fmt.exn ex end;
|
||||
begin try take ~sw t with ex -> traceln "%a" Fmt.exn ex end;;
|
||||
Eio.Cancel.sub @@ fun c ->
|
||||
Eio.Cancel.cancel c Cancel;
|
||||
begin try add t 1 with ex -> traceln "%a" Fmt.exn ex end;
|
||||
begin try take t with ex -> traceln "%a" Fmt.exn ex end;;
|
||||
+Adding 1 to stream
|
||||
+Cancelled: Cancel
|
||||
+Reading from stream
|
||||
+Cancelled: Cancel
|
||||
Exception: Cancel.
|
||||
Exception: Cancelled: Cancel
|
||||
```
|
||||
|
||||
Readers queue up:
|
||||
@ -232,7 +225,7 @@ Readers queue up:
|
||||
```ocaml
|
||||
# run @@ fun () ->
|
||||
let t = S.create 0 in
|
||||
Switch.top @@ fun sw ->
|
||||
Switch.run @@ fun sw ->
|
||||
Fibre.fork_ignore ~sw (fun () -> take t; traceln "a done");
|
||||
Fibre.fork_ignore ~sw (fun () -> take t; traceln "b done");
|
||||
Fibre.fork_ignore ~sw (fun () -> take t; traceln "c done");
|
||||
@ -262,7 +255,7 @@ Writers queue up:
|
||||
```ocaml
|
||||
# run @@ fun () ->
|
||||
let t = S.create 0 in
|
||||
Switch.top @@ fun sw ->
|
||||
Switch.run @@ fun sw ->
|
||||
Fibre.fork_ignore ~sw (fun () -> add t 1);
|
||||
Fibre.fork_ignore ~sw (fun () -> add t 2);
|
||||
Fibre.fork_ignore ~sw (fun () -> add t 3);
|
||||
|
@ -9,7 +9,7 @@ open Eio.Std
|
||||
|
||||
let run (fn : Switch.t -> unit) =
|
||||
Eio_main.run @@ fun _e ->
|
||||
Switch.top fn
|
||||
Switch.run fn
|
||||
```
|
||||
|
||||
# Test cases
|
||||
@ -40,8 +40,8 @@ Exception: Failure "Cancel".
|
||||
`Fibre.both`, both fibres pass:
|
||||
|
||||
```ocaml
|
||||
# run (fun sw ->
|
||||
Fibre.both ~sw
|
||||
# run (fun _sw ->
|
||||
Fibre.both
|
||||
(fun () -> for i = 1 to 2 do traceln "i = %d" i; Fibre.yield () done)
|
||||
(fun () -> for j = 1 to 2 do traceln "j = %d" j; Fibre.yield () done)
|
||||
);;
|
||||
@ -56,7 +56,7 @@ Exception: Failure "Cancel".
|
||||
|
||||
```ocaml
|
||||
# run (fun sw ->
|
||||
Fibre.both ~sw
|
||||
Fibre.both
|
||||
(fun () -> for i = 1 to 5 do traceln "i = %d" i; Fibre.yield () done)
|
||||
(fun () -> failwith "Failed")
|
||||
);;
|
||||
@ -68,7 +68,7 @@ Exception: Failure "Failed".
|
||||
|
||||
```ocaml
|
||||
# run (fun sw ->
|
||||
Fibre.both ~sw
|
||||
Fibre.both
|
||||
(fun () -> Fibre.yield (); failwith "Failed")
|
||||
(fun () -> for i = 1 to 5 do traceln "i = %d" i; Fibre.yield () done)
|
||||
);;
|
||||
@ -80,7 +80,7 @@ Exception: Failure "Failed".
|
||||
|
||||
```ocaml
|
||||
# run (fun sw ->
|
||||
Fibre.both ~sw (fun () -> failwith "Failed") ignore;
|
||||
Fibre.both (fun () -> failwith "Failed") ignore;
|
||||
traceln "Not reached"
|
||||
);;
|
||||
Exception: Failure "Failed".
|
||||
@ -90,7 +90,7 @@ Exception: Failure "Failed".
|
||||
|
||||
```ocaml
|
||||
# run (fun sw ->
|
||||
Fibre.both ~sw ignore (fun () -> failwith "Failed");
|
||||
Fibre.both ignore (fun () -> failwith "Failed");
|
||||
traceln "not reached"
|
||||
);;
|
||||
Exception: Failure "Failed".
|
||||
@ -100,7 +100,7 @@ Exception: Failure "Failed".
|
||||
|
||||
```ocaml
|
||||
# run (fun sw ->
|
||||
Fibre.both ~sw
|
||||
Fibre.both
|
||||
(fun () -> failwith "Failed 1")
|
||||
(fun () -> failwith "Failed 2")
|
||||
);;
|
||||
@ -141,12 +141,12 @@ Turning off a switch runs the cancel callbacks, unless they've been removed by t
|
||||
let h1 = Switch.add_cancel_hook sw (fun _ -> traceln "Cancel 1") in
|
||||
let h2 = Switch.add_cancel_hook sw (fun _ -> traceln "Cancel 2") in
|
||||
let h3 = Switch.add_cancel_hook sw (fun _ -> traceln "Cancel 3") in
|
||||
Switch.remove_hook h2;
|
||||
Eio.Hook.remove h2;
|
||||
Switch.turn_off sw (Failure "Cancelled");
|
||||
let h4 = Switch.add_cancel_hook sw (fun _ -> traceln "Cancel 4") in
|
||||
Switch.remove_hook h1;
|
||||
Switch.remove_hook h3;
|
||||
Switch.remove_hook h4
|
||||
Eio.Hook.remove h1;
|
||||
Eio.Hook.remove h3;
|
||||
Eio.Hook.remove h4
|
||||
);;
|
||||
+Cancel 3
|
||||
+Cancel 1
|
||||
@ -154,15 +154,31 @@ Turning off a switch runs the cancel callbacks, unless they've been removed by t
|
||||
Exception: Failure "Cancelled".
|
||||
```
|
||||
|
||||
Wait for either a promise or a switch; switch cancelled first:
|
||||
Cancellation callbacks do not run on success, but release ones do:
|
||||
|
||||
```ocaml
|
||||
# run @@ fun sw ->
|
||||
Switch.add_cancel_hook sw (fun _ -> traceln "Cance hook") |> ignore;
|
||||
Switch.on_release sw (fun _ -> traceln "Release hook");;
|
||||
+Release hook
|
||||
- : unit = ()
|
||||
```
|
||||
|
||||
Wait for either a promise or a cancellation; cancellation first:
|
||||
```ocaml
|
||||
# run (fun sw ->
|
||||
let p, r = Promise.create () in
|
||||
Fibre.fork_ignore ~sw (fun () -> traceln "Waiting"; Promise.await ~sw p; traceln "Resolved");
|
||||
Switch.turn_off sw (Failure "Cancelled");
|
||||
Promise.fulfill r ()
|
||||
Fibre.fork_ignore ~sw (fun () ->
|
||||
Fibre.both
|
||||
(fun () -> traceln "Waiting"; Promise.await p; traceln "Resolved")
|
||||
(fun () -> failwith "Cancelled")
|
||||
);
|
||||
Fibre.yield ();
|
||||
Promise.fulfill r ();
|
||||
traceln "Main thread done";
|
||||
);;
|
||||
+Waiting
|
||||
+Main thread done
|
||||
Exception: Failure "Cancelled".
|
||||
```
|
||||
|
||||
@ -171,7 +187,7 @@ Wait for either a promise or a switch; promise resolves first:
|
||||
```ocaml
|
||||
# run (fun sw ->
|
||||
let p, r = Promise.create () in
|
||||
Fibre.fork_ignore ~sw (fun () -> traceln "Waiting"; Promise.await ~sw p; traceln "Resolved");
|
||||
Fibre.fork_ignore ~sw (fun () -> traceln "Waiting"; Promise.await p; traceln "Resolved");
|
||||
Promise.fulfill r ();
|
||||
Fibre.yield ();
|
||||
traceln "Now cancelling...";
|
||||
@ -188,7 +204,7 @@ Wait for either a promise or a switch; switch cancelled first. Result version.
|
||||
```ocaml
|
||||
# run (fun sw ->
|
||||
let p, r = Promise.create () in
|
||||
Fibre.fork_ignore ~sw (fun () -> traceln "Waiting"; ignore (Promise.await_result ~sw p); traceln "Resolved");
|
||||
Fibre.fork_ignore ~sw (fun () -> traceln "Waiting"; ignore (Promise.await_result p); traceln "Resolved");
|
||||
Switch.turn_off sw (Failure "Cancelled");
|
||||
Promise.fulfill r ()
|
||||
);;
|
||||
@ -201,13 +217,14 @@ Wait for either a promise or a switch; promise resolves first but switch off wit
|
||||
```ocaml
|
||||
# run (fun sw ->
|
||||
let p, r = Promise.create () in
|
||||
Fibre.fork_ignore ~sw (fun () -> traceln "Waiting"; ignore (Promise.await_result ~sw p); traceln "Resolved");
|
||||
Fibre.fork_ignore ~sw (fun () -> traceln "Waiting"; ignore (Promise.await_result p); traceln "Resolved");
|
||||
Promise.fulfill r ();
|
||||
traceln "Now cancelling...";
|
||||
Switch.turn_off sw (Failure "Cancelled")
|
||||
);;
|
||||
+Waiting
|
||||
+Now cancelling...
|
||||
+Resolved
|
||||
Exception: Failure "Cancelled".
|
||||
```
|
||||
|
||||
@ -217,14 +234,14 @@ Child switches are cancelled when the parent is cancelled:
|
||||
# run (fun sw ->
|
||||
let p, _ = Promise.create () in
|
||||
let on_error ex = traceln "child: %s" (Printexc.to_string ex) in
|
||||
Fibre.fork_sub_ignore ~sw ~on_error (fun sw -> traceln "Child 1"; Promise.await ~sw p);
|
||||
Fibre.fork_sub_ignore ~sw ~on_error (fun sw -> traceln "Child 2"; Promise.await ~sw p);
|
||||
Fibre.fork_sub_ignore ~sw ~on_error (fun sw -> traceln "Child 1"; Promise.await p);
|
||||
Fibre.fork_sub_ignore ~sw ~on_error (fun sw -> traceln "Child 2"; Promise.await p);
|
||||
Switch.turn_off sw (Failure "Cancel parent")
|
||||
);;
|
||||
+Child 1
|
||||
+Child 2
|
||||
+child: Failure("Cancel parent")
|
||||
+child: Failure("Cancel parent")
|
||||
+child: Cancelled: Failure("Cancel parent")
|
||||
+child: Cancelled: Failure("Cancel parent")
|
||||
Exception: Failure "Cancel parent".
|
||||
```
|
||||
|
||||
@ -235,8 +252,8 @@ A child can fail independently of the parent:
|
||||
let p1, r1 = Promise.create () in
|
||||
let p2, r2 = Promise.create () in
|
||||
let on_error ex = traceln "child: %s" (Printexc.to_string ex) in
|
||||
Fibre.fork_sub_ignore ~sw ~on_error (fun sw -> traceln "Child 1"; Promise.await ~sw p1);
|
||||
Fibre.fork_sub_ignore ~sw ~on_error (fun sw -> traceln "Child 2"; Promise.await ~sw p2);
|
||||
Fibre.fork_sub_ignore ~sw ~on_error (fun sw -> traceln "Child 1"; Promise.await p1);
|
||||
Fibre.fork_sub_ignore ~sw ~on_error (fun sw -> traceln "Child 2"; Promise.await p2);
|
||||
Promise.break r1 (Failure "Child error");
|
||||
Promise.fulfill r2 ();
|
||||
Fibre.yield ();
|
||||
@ -271,13 +288,13 @@ A child can be cancelled independently of the parent:
|
||||
- : unit = ()
|
||||
```
|
||||
|
||||
A child error handle raises:
|
||||
A child error handler raises:
|
||||
|
||||
```ocaml
|
||||
# run (fun sw ->
|
||||
let p, r = Promise.create () in
|
||||
let on_error = raise in
|
||||
Fibre.fork_sub_ignore ~sw ~on_error (fun sw -> traceln "Child"; Promise.await ~sw p);
|
||||
Fibre.fork_sub_ignore ~sw ~on_error (fun sw -> traceln "Child"; Promise.await p);
|
||||
Promise.break r (Failure "Child error escapes");
|
||||
Fibre.yield ();
|
||||
traceln "Not reached"
|
||||
@ -290,12 +307,16 @@ A child error handler deals with the exception:
|
||||
|
||||
```ocaml
|
||||
# run (fun sw ->
|
||||
let print ex = traceln "%s" (Printexc.to_string ex); 0 in
|
||||
let x = Switch.sub sw ~on_error:print (fun _sw -> failwith "Child error") in
|
||||
traceln "x = %d" x
|
||||
let p, r = Promise.create () in
|
||||
let on_error = traceln "caught: %a" Fmt.exn in
|
||||
Fibre.fork_sub_ignore ~sw ~on_error (fun sw -> traceln "Child"; Promise.await p);
|
||||
Promise.break r (Failure "Child error is caught");
|
||||
Fibre.yield ();
|
||||
traceln "Still running"
|
||||
);;
|
||||
+Failure("Child error")
|
||||
+x = 0
|
||||
+Child
|
||||
+caught: Failure("Child error is caught")
|
||||
+Still running
|
||||
- : unit = ()
|
||||
```
|
||||
|
||||
@ -313,12 +334,16 @@ Failure
|
||||
|
||||
# Release handlers
|
||||
|
||||
```ocaml
|
||||
let release label = Fibre.yield (); traceln "release %s" label
|
||||
```
|
||||
|
||||
Release on success:
|
||||
|
||||
```ocaml
|
||||
# run (fun sw ->
|
||||
Switch.on_release sw (fun () -> traceln "release 1");
|
||||
Switch.on_release sw (fun () -> traceln "release 2");
|
||||
Switch.on_release sw (fun () -> release "1");
|
||||
Switch.on_release sw (fun () -> release "2");
|
||||
);;
|
||||
+release 2
|
||||
+release 1
|
||||
@ -329,8 +354,8 @@ Release on error:
|
||||
|
||||
```ocaml
|
||||
# run (fun sw ->
|
||||
Switch.on_release sw (fun () -> traceln "release 1");
|
||||
Switch.on_release sw (fun () -> traceln "release 2");
|
||||
Switch.on_release sw (fun () -> release "1");
|
||||
Switch.on_release sw (fun () -> release "2");
|
||||
failwith "Test error"
|
||||
);;
|
||||
+release 2
|
||||
@ -342,9 +367,9 @@ A release operation itself fails:
|
||||
|
||||
```ocaml
|
||||
# run (fun sw ->
|
||||
Switch.on_release sw (fun () -> traceln "release 1"; failwith "failure 1");
|
||||
Switch.on_release sw (fun () -> traceln "release 2");
|
||||
Switch.on_release sw (fun () -> traceln "release 3"; failwith "failure 3");
|
||||
Switch.on_release sw (fun () -> release "1"; failwith "failure 1");
|
||||
Switch.on_release sw (fun () -> release "2");
|
||||
Switch.on_release sw (fun () -> release "3"; failwith "failure 3");
|
||||
);;
|
||||
+release 3
|
||||
+release 2
|
||||
@ -355,6 +380,21 @@ and
|
||||
Failure("failure 1")
|
||||
```
|
||||
|
||||
Attaching a release handler to a finished switch from a cancelled context:
|
||||
|
||||
```ocaml
|
||||
# run @@ fun sw ->
|
||||
let sub = Switch.run Fun.id in (* A finished switch *)
|
||||
Switch.turn_off sw (Failure "Parent cancelled too!");
|
||||
Switch.on_release sub (fun () -> release "1");;
|
||||
+release 1
|
||||
Exception:
|
||||
Multiple exceptions:
|
||||
Failure("Parent cancelled too!")
|
||||
and
|
||||
Invalid_argument("Switch finished!")
|
||||
```
|
||||
|
||||
Using switch from inside release handler:
|
||||
|
||||
```ocaml
|
||||
@ -389,9 +429,9 @@ Using switch from inside release handler:
|
||||
|
||||
```ocaml
|
||||
let fork_sub_ignore_resource sw =
|
||||
traceln "Allocate resource";
|
||||
traceln "allocate resource";
|
||||
Fibre.fork_sub_ignore ~sw ~on_error:raise
|
||||
~on_release:(fun () -> traceln "Free resource")
|
||||
~on_release:(fun () -> release "resource")
|
||||
(fun _sw -> traceln "Child fibre running")
|
||||
```
|
||||
|
||||
@ -401,9 +441,9 @@ We release when `fork_sub_ignore` returns:
|
||||
# run (fun sw ->
|
||||
fork_sub_ignore_resource sw
|
||||
);;
|
||||
+Allocate resource
|
||||
+allocate resource
|
||||
+Child fibre running
|
||||
+Free resource
|
||||
+release resource
|
||||
- : unit = ()
|
||||
```
|
||||
|
||||
@ -414,8 +454,8 @@ We release when `fork_sub_ignore` fails due to parent switch being already off:
|
||||
Switch.turn_off sw (Failure "Switch already off");
|
||||
fork_sub_ignore_resource sw
|
||||
);;
|
||||
+Allocate resource
|
||||
+Free resource
|
||||
+allocate resource
|
||||
+release resource
|
||||
Exception: Failure "Switch already off".
|
||||
```
|
||||
|
||||
@ -423,12 +463,11 @@ We release when `fork_sub_ignore` fails due to parent switch being invalid:
|
||||
|
||||
```ocaml
|
||||
# run (fun sw ->
|
||||
let copy = ref sw in
|
||||
Switch.sub sw ~on_error:raise (fun sub -> copy := sub);
|
||||
fork_sub_ignore_resource !copy
|
||||
let sub = Switch.run Fun.id in
|
||||
fork_sub_ignore_resource sub
|
||||
);;
|
||||
+Allocate resource
|
||||
+Free resource
|
||||
+allocate resource
|
||||
+release resource
|
||||
Exception: Invalid_argument "Switch finished!".
|
||||
```
|
||||
|
||||
@ -445,3 +484,23 @@ We release when `fork_sub_ignore`'s switch is turned off while running:
|
||||
+Free resource
|
||||
Exception: Failure "Simulated error".
|
||||
```
|
||||
|
||||
# Error reporting
|
||||
|
||||
All cancel hooks run, even if some fail, and all errors are reported:
|
||||
|
||||
```ocaml
|
||||
# run (fun sw ->
|
||||
Switch.add_cancel_hook sw (fun _ -> failwith "cancel1 failed") |> ignore;
|
||||
Switch.add_cancel_hook sw (fun _ -> failwith "cancel2 failed") |> ignore;
|
||||
raise Exit
|
||||
);;
|
||||
Exception:
|
||||
Multiple exceptions:
|
||||
Stdlib.Exit
|
||||
and
|
||||
During cancellation:
|
||||
Failure("cancel2 failed")
|
||||
and
|
||||
Failure("cancel1 failed")
|
||||
```
|
||||
|
@ -21,7 +21,7 @@ Create a promise, fork a thread waiting for it, then fulfull it:
|
||||
```ocaml
|
||||
# let () =
|
||||
Eio_main.run @@ fun _stdenv ->
|
||||
Switch.top @@ fun sw ->
|
||||
Switch.run @@ fun sw ->
|
||||
let p, r = Promise.create () in
|
||||
traceln "Initial state: %a" (pp_promise Fmt.string) p;
|
||||
let thread = Fibre.fork ~sw ~exn_turn_off:false (fun () -> Promise.await p) in
|
||||
@ -42,7 +42,7 @@ Create a promise, fork a thread waiting for it, then break it:
|
||||
```ocaml
|
||||
# let () =
|
||||
Eio_main.run @@ fun _stdenv ->
|
||||
Switch.top @@ fun sw ->
|
||||
Switch.run @@ fun sw ->
|
||||
let p, r = Promise.create () in
|
||||
traceln "Initial state: %a" (pp_promise Fmt.string) p;
|
||||
let thread = Fibre.fork ~sw ~exn_turn_off:false (fun () -> Promise.await p) in
|
||||
@ -66,13 +66,13 @@ Some simple tests of `fork_ignore`:
|
||||
# let () =
|
||||
Eio_main.run @@ fun _stdenv ->
|
||||
let i = ref 0 in
|
||||
Switch.top (fun sw ->
|
||||
Switch.run (fun sw ->
|
||||
Fibre.fork_ignore ~sw (fun () -> incr i);
|
||||
);
|
||||
traceln "Forked code ran; i is now %d" !i;
|
||||
let p1, r1 = Promise.create () in
|
||||
try
|
||||
Switch.top (fun sw ->
|
||||
Switch.run (fun sw ->
|
||||
Fibre.fork_ignore ~sw (fun () -> Promise.await p1; incr i; raise Exit);
|
||||
traceln "Forked code waiting; i is still %d" !i;
|
||||
Promise.fulfill r1 ()
|
||||
@ -90,7 +90,7 @@ Basic semaphore tests:
|
||||
# let () =
|
||||
let module Semaphore = Eio.Semaphore in
|
||||
Eio_main.run @@ fun _stdenv ->
|
||||
Switch.top @@ fun sw ->
|
||||
Switch.run @@ fun sw ->
|
||||
let running = ref 0 in
|
||||
let sem = Semaphore.make 2 in
|
||||
let fork = Fibre.fork ~sw ~exn_turn_off:false in
|
||||
@ -126,7 +126,7 @@ Releasing a semaphore when no-one is waiting for it:
|
||||
# let () =
|
||||
let module Semaphore = Eio.Semaphore in
|
||||
Eio_main.run @@ fun _stdenv ->
|
||||
Switch.top @@ fun sw ->
|
||||
Switch.run @@ fun sw ->
|
||||
let sem = Semaphore.make 0 in
|
||||
Semaphore.release sem; (* Release with free-counter *)
|
||||
traceln "Initial config: %d" (Semaphore.get_value sem);
|
||||
|
@ -41,8 +41,7 @@ Cancelling sleep:
|
||||
|
||||
```ocaml
|
||||
# run @@ fun ~clock ->
|
||||
Switch.top @@ fun sw ->
|
||||
Fibre.both ~sw
|
||||
Fibre.both
|
||||
(fun () -> Eio.Time.sleep clock 1200.; assert false)
|
||||
(fun () -> failwith "Simulated cancel");;
|
||||
Exception: Failure "Simulated cancel".
|
||||
@ -52,7 +51,7 @@ Switch is already off:
|
||||
|
||||
```ocaml
|
||||
# run @@ fun ~clock ->
|
||||
Switch.top @@ fun sw ->
|
||||
Switch.run @@ fun sw ->
|
||||
Switch.turn_off sw (Failure "Simulated failure");
|
||||
Eio.Time.sleep clock 1200.0;
|
||||
assert false;;
|
||||
@ -63,8 +62,8 @@ Scheduling a timer that's already due:
|
||||
|
||||
```ocaml
|
||||
# run @@ fun ~clock ->
|
||||
Switch.top @@ fun sw ->
|
||||
Fibre.both ~sw
|
||||
Switch.run @@ fun sw ->
|
||||
Fibre.both
|
||||
(fun () -> traceln "First fibre runs"; Eio.Time.sleep clock (-1.0); traceln "Sleep done")
|
||||
(fun () -> traceln "Second fibre runs");;
|
||||
+First fibre runs
|
||||
@ -77,8 +76,8 @@ Check ordering works:
|
||||
|
||||
```ocaml
|
||||
# run @@ fun ~clock ->
|
||||
Switch.top @@ fun sw ->
|
||||
Fibre.both ~sw
|
||||
Switch.run @@ fun sw ->
|
||||
Fibre.both
|
||||
(fun () ->
|
||||
Eio.Time.sleep clock 1200.0;
|
||||
assert false
|
||||
|
Loading…
x
Reference in New Issue
Block a user