-
Notifications
You must be signed in to change notification settings - Fork 180
add the lwt_direct
package, for direct-style control flow
#1060
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
cd0e502
7145d61
8cab71e
0978471
5b746aa
33527db
b869daa
495852e
d776f41
dc85027
9bee283
c5aa6c5
cdf51ce
6e408a8
020ae8b
92c1d3e
61f338d
05d3233
d696674
8587e96
09fba3a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
# This file is generated by dune, edit dune-project instead | ||
opam-version: "2.0" | ||
synopsis: "Direct-style control-flow and `await` for Lwt" | ||
maintainer: [ | ||
"Raphaël Proust <[email protected]>" "Anton Bachin <[email protected]>" | ||
] | ||
authors: ["Simon Cruanes"] | ||
license: "MIT" | ||
homepage: "https://github.com/ocsigen/lwt" | ||
doc: "https://ocsigen.org/lwt" | ||
bug-reports: "https://github.com/ocsigen/lwt/issues" | ||
depends: [ | ||
"dune" {>= "2.7"} | ||
"ocaml" {>= "5.0"} | ||
"base-unix" | ||
"lwt" {>= "6"} | ||
"bisect_ppx" {with-test} | ||
"odoc" {with-doc} | ||
] | ||
build: [ | ||
["dune" "subst"] {dev} | ||
[ | ||
"dune" | ||
"build" | ||
"-p" | ||
name | ||
"-j" | ||
jobs | ||
"@install" | ||
"@runtest" {with-test} | ||
"@doc" {with-doc} | ||
] | ||
] | ||
dev-repo: "git+https://github.com/ocsigen/lwt.git" |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
(library | ||
(public_name lwt_direct) | ||
(synopsis "Direct-style control-flow and `await` for Lwt") | ||
(enabled_if (>= %{ocaml_version} "5.0")) | ||
(libraries lwt lwt.unix) | ||
(instrumentation | ||
(backend bisect_ppx))) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
(* Direct-style wrapper for Lwt code | ||
|
||
The implementation of the direct-style wrapper relies on ocaml5's effect | ||
system capturing continuations and adding them as a callback to some lwt | ||
promises. *) | ||
|
||
(* part 1: tasks, getting the scheduler to call them *) | ||
|
||
let tasks : (unit -> unit) Queue.t = Queue.create () | ||
|
||
let[@inline] push_task f : unit = Queue.push f tasks | ||
|
||
let absolute_max_number_of_steps = | ||
(* TODO 6.0: what's a good number here? should it be customisable? *) | ||
10_000 | ||
|
||
let run_all_tasks () : unit = | ||
let n_processed = ref 0 in | ||
let max_number_of_steps = min absolute_max_number_of_steps (2 * Queue.length tasks) in | ||
while (not (Queue.is_empty tasks)) && !n_processed < max_number_of_steps do | ||
let t = Queue.pop tasks in | ||
incr n_processed; | ||
try t () | ||
with exn -> | ||
(* TODO 6.0: change async_exception handler to accept a backtrace, pass it | ||
here and at the other use site. *) | ||
(* TODO 6.0: this and other try-with: respect exception-filter *) | ||
!Lwt.async_exception_hook exn | ||
done; | ||
(* In the case where there are no promises ready for wakeup, the scheduler's | ||
engine will pause until some IO completes. There might never be completed | ||
IO, depending on the program structure and the state of the world. If this | ||
happens and the queue is not empty, we add a [pause] so that the engine has | ||
something to wakeup for so that the rest of the queue can be processed. *) | ||
if not (Queue.is_empty tasks) && Lwt.paused_count () = 0 then ignore (Lwt.pause () : unit Lwt.t) | ||
|
||
let setup_hooks = | ||
let already_done = ref false in | ||
fun () -> | ||
if not !already_done then ( | ||
already_done := true; | ||
(* TODO 6.0: assess whether we should have both hooks or just one (which | ||
one). Tempted to say we should only have the enter hook. *) | ||
let _hook1 = Lwt_main.Enter_iter_hooks.add_first run_all_tasks in | ||
let _hook2 = Lwt_main.Leave_iter_hooks.add_first run_all_tasks in | ||
raphael-proust marked this conversation as resolved.
Show resolved
Hide resolved
|
||
() | ||
) | ||
|
||
(* part 2: effects, performing them *) | ||
|
||
type _ Effect.t += | ||
| Await : 'a Lwt.t -> 'a Effect.t | ||
| Yield : unit Effect.t | ||
|
||
let await (fut : 'a Lwt.t) : 'a = | ||
match Lwt.state fut with | ||
| Lwt.Return x -> x | ||
| Lwt.Fail exn -> raise exn | ||
| Lwt.Sleep -> Effect.perform (Await fut) | ||
|
||
let yield () : unit = Effect.perform Yield | ||
|
||
(* interlude: task-local storage helpers *) | ||
|
||
module Storage = struct | ||
[@@@alert "-trespassing"] | ||
module Lwt_storage= Lwt.Private.Sequence_associated_storage | ||
[@@@alert "+trespassing"] | ||
type 'a key = 'a Lwt.key | ||
let new_key = Lwt.new_key | ||
let get = Lwt.get | ||
let set k v = Lwt_storage.(current_storage := modify_storage k (Some v) !current_storage) | ||
let remove k = Lwt_storage.(current_storage := modify_storage k None !current_storage) | ||
let reset_to_empty () = Lwt_storage.(current_storage := empty_storage) | ||
let save_current () = !Lwt_storage.current_storage | ||
let restore_current saved = Lwt_storage.current_storage := saved | ||
end | ||
|
||
(* part 3: handling effects *) | ||
|
||
let handler : _ Effect.Deep.effect_handler = | ||
let effc : type b. b Effect.t -> ((b, unit) Effect.Deep.continuation -> 'a) option = | ||
function | ||
| Yield -> | ||
Some (fun k -> | ||
let storage = Storage.save_current () in | ||
push_task (fun () -> | ||
Storage.restore_current storage; | ||
Effect.Deep.continue k ())) | ||
| Await fut -> | ||
Some | ||
(fun k -> | ||
let storage = Storage.save_current () in | ||
Lwt.on_any fut | ||
(fun res -> push_task (fun () -> | ||
Storage.restore_current storage; Effect.Deep.continue k res)) | ||
(fun exn -> push_task (fun () -> | ||
Storage.restore_current storage; Effect.Deep.discontinue k exn))) | ||
| _ -> None | ||
in | ||
{ effc } | ||
|
||
(* part 4: putting it all together: running tasks *) | ||
|
||
let run_inside_effect_handler_and_resolve_ (type a) (promise : a Lwt.u) f () : unit = | ||
let run_f_and_set_res () = | ||
Storage.reset_to_empty(); | ||
match f () with | ||
| res -> Lwt.wakeup promise res | ||
| exception exc -> Lwt.wakeup_exn promise exc | ||
in | ||
Effect.Deep.try_with run_f_and_set_res () handler | ||
|
||
let spawn f : _ Lwt.t = | ||
setup_hooks (); | ||
let lwt, resolve = Lwt.wait () in | ||
push_task (run_inside_effect_handler_and_resolve_ resolve f); | ||
lwt | ||
|
||
(* part 4 (encore): running a task in the background *) | ||
|
||
let run_inside_effect_handler_in_the_background_ f () : unit = | ||
let run_f () : unit = | ||
Storage.reset_to_empty(); | ||
try | ||
f () | ||
with exn -> | ||
!Lwt.async_exception_hook exn | ||
in | ||
Effect.Deep.try_with run_f () handler | ||
|
||
let spawn_in_the_background f : unit = | ||
setup_hooks (); | ||
push_task (run_inside_effect_handler_in_the_background_ f) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
(** Direct style control flow for Lwt. | ||
|
||
This module relies on OCaml 5's | ||
{{:https://ocaml.org/manual/5.3/effects.html} effect handlers}. | ||
Instead of chaining promises using {!Lwt.bind} and {!Lwt.map} | ||
and other combinators, it becomes possible to start | ||
lightweight "tasks" using [Lwt_direct.spawn (fun () -> ...)]. | ||
The body of such a task is written in direct-style code, | ||
using OCaml's standard control flow structures such as loops, | ||
higher-order functions, exception handlers, [match], etc. | ||
|
||
Interactions with the rest of lwt can be done using [await], | ||
for example: | ||
|
||
{[ | ||
Lwt_direct.spawn (fun () -> | ||
let continue = ref true in | ||
while !continue do | ||
match Lwt_io.read_line in_channel |> Lwt_direct.await with | ||
| exception End_of_file -> continue := false | ||
| line -> | ||
let uppercase_line = String.uppercase_ascii line in | ||
Lwt_io.write_line out_channel uppercase_line |> Lwt_direct.await | ||
done) | ||
]} | ||
|
||
This code snippet contains a simple "task" that repeatedly reads | ||
a line from a [Lwt_io] channel, uppercases it, and writes the | ||
uppercase version to another channel. | ||
|
||
This task is itself a [unit Lwt.t], which is resolved when the function | ||
returns. It is possible to use | ||
{!Lwt_direct.run_in_the_background} to ignore the result and | ||
let the task run in the background instead. | ||
|
||
*) | ||
|
||
val spawn : (unit -> 'a) -> 'a Lwt.t | ||
(** [spawn f] runs the function [f ()] in a task within | ||
the [Lwt_unix] event loop. [f ()] can create [Lwt] | ||
promises and use {!await} to wait for them. Like any promise | ||
in Lwt, [f ()] can starve the event loop if it runs long computations | ||
without yielding to the event loop. | ||
|
||
When [f ()] terminates (successfully or not), the promise | ||
[spawn f] is resolved with [f ()]'s result, or the exception | ||
raised by [f ()]. *) | ||
|
||
val spawn_in_the_background : | ||
(unit -> unit) -> | ||
unit | ||
(** [spawn_in_the_background f] is similar to [ignore (spawn f)]. | ||
The computation [f()] runs in the background in the event loop | ||
and returns no result. | ||
If [f()] raises an exception, {!Lwt.async_exception_hook} is called. *) | ||
|
||
val yield : unit -> unit | ||
(** Yield to the event loop. | ||
|
||
Calling [yield] outside of {!spawn} or {!run_in_the_background} will raise an exception, | ||
crash your program, or otherwise cause errors. It is a programming error to do so. *) | ||
|
||
val await : 'a Lwt.t -> 'a | ||
(** [await prom] returns the result of [prom], or re-raises the | ||
exception with which [prom] failed if it failed. | ||
If [prom] is not resolved yet, [await prom] will suspend the | ||
current task and resume it when [prom] is resolved. | ||
|
||
Calling [await] outside of {!spawn} or {!run_in_the_background} will raise an exception, | ||
crash your program, or otherwise cause errors. It is a programming error to do so. *) | ||
|
||
(** Local storage. | ||
|
||
This storage is the same as the one described with {!Lwt.key}, | ||
except that it is usable from the inside of {!spawn} or | ||
{!run_in_the_background}. | ||
|
||
Each task has its own storage, independent from other tasks or promises. *) | ||
module Storage : sig | ||
type 'a key = 'a Lwt.key | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it's deprecated because the way that the storage flows is non-obvious and not extensible. Non-obvious: It's the fundamental original issue of Lwt: there are no threads but the API sometimes say there is. Non-extensible: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess actually you adding these storage function is because of the the lack of extensibility. So maybe the way out of it, the way to undeprecate is to
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I need to write more about this, but in a way this storage makes more sense for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes I think the stoarge makes more sense for direct-style, because of the |
||
val new_key : unit -> 'a key | ||
(** Alias to {!Lwt.new_key} *) | ||
|
||
val get : 'a key -> 'a option | ||
(** get the value associated with this key in local storage, or [None] *) | ||
|
||
val set : 'a key -> 'a -> unit | ||
(** [set k v] sets the key to the value for the rest of the task. *) | ||
|
||
val remove : 'a key -> unit | ||
(** Remove the value associated with this key, if any *) | ||
end |
Uh oh!
There was an error while loading. Please reload this page.