DuppyAdvanced scheduler and monad for server-oriented programming.
*
* The bars could not hold me; * Force could not control me now. * They try to keep me down, yeah! * But Jah put I around. * (...) * Let me tell you this - * I'm a duppy conqueror ! *
}
}
*
Lee "Scratch" Perry & Bob Marley - Duppy conqueror
* *
* * Duppy is a task scheduler for ocaml. It implements a wrapper * around the platform's readiness call: epoll or kqueue, and * poll elsewhere. SPEC.md states what it guarantees. * * Using Duppy.Task, the programmer can easily submit tasks that need to wait * on a socket even, or for a given timeout (possibly zero). * * With Duppy.Async, one can use a scheduler to submit asynchronous tasks. * * Duppy.Io implements recursive easy reading and writing to a Unix.file_descr * * Finally, Duppy.run lets a computation park on its events in direct * style instead of splitting into tasks. * * The scheduler runs a pool of domains, one per core: a task is dispatched * onto whichever domain is free when it becomes ready. It can also run a * pool of threads instead, for a program that must not run tasks in * parallel.
A scheduler is a device for processing tasks. * * 'a is the type of objects used for priorities.
How a task is run. A worker is one domain of the pool; on a thread pool, where a worker is a thread, `Threaded tasks run in place.
`Immediate: a worker takes every ready one as a single batch and runs them in sequence on its own thread. No hand-off, no auxiliary thread, no slot of max_blocking. Nothing else is dispatched by that worker until the batch ends, so one task that blocks holds up the rest of the batch and the worker. A worker alternates a batch with one task of another class, so a ready list that refills as fast as it drains does not starve the others.
`Direct: a worker takes one and runs it on its own thread. No hand-off, no auxiliary thread, no slot of max_blocking, so it is taken even when every slot is in use. The worker dispatches nothing else until it returns. Ready ones are taken one per worker and so spread over the pool. The worker's auxiliary threads share its domain lock, and may be given it while the task runs.
`Threaded: a worker hands one to an auxiliary thread of its domain and goes back to dispatching. When the task parks in a syscall the runtime lock is released and the domain keeps running. Each holds a slot of max_blocking until it returns; with none left the workers decline them, and they wait for a slot.
Among ready `Direct and `Threaded tasks a worker takes one of the lowest rank, and among those the one that became ready first.
Choosing a class
Which work goes where belongs to the application. As a starting point:
`Immediate for reads and writes on descriptors known not to block, and for bookkeeping that only wakes something else.`Direct for work that is time-sensitive and has to run ahead of the rest, such as a clock, and for work whose duration is predictable and large enough to be worth a scheduling step of its own.`Threaded for everything that may block or run long for any reason: disk and network calls, loops sized by their data. It is the default because a task wrongly placed here costs a hand-off, while one wrongly placed in the other two stalls a worker.Wraps every task body. Effect handlers do not cross the thread a task is dispatched to, so a caller whose tasks need one installs it here rather than at each of its own entry points.
val create :
?on_error:(exn -> Stdlib.Printexc.raw_backtrace -> unit) ->
?on_fatal:(exn -> Stdlib.Printexc.raw_backtrace -> unit) ->
?rank:('a -> int) ->
?classify:('a -> execution_class) ->
?wrapper:wrapper ->
unit ->
'a schedulerInitiate a new scheduler. It has no domains until start is called. *
val start :
?pool:
[ `Domains of int
| `Growing of int * int
| `Selective_domains of ('a -> bool) list
| `Threads of ('a -> bool) list ] ->
?max_blocking:int ->
?grow_after:float ->
?log:(string -> unit) ->
'a scheduler ->
unitstart s spawns the scheduler's pool, and the thread that waits for * events, which is not a domain and runs no OCaml code. Raises Failure if * s is already started. * * With `Domains n, the pool is n workers on n domains, the calling * domain being the first of them: n - 1 domains are spawned. Tasks run in * parallel. Spawning a domain makes Unix.fork fail from then on, so with * n > 1 this must be called after any daemonization. * * With `Selective_domains accepts, the pool is likewise one worker and one * domain per predicate, each taking only the tasks whose priority it accepts. * The first predicate is the calling domain's: one that refuses everything * keeps that domain out of the pool. * * With `Threads accepts, the pool is one systhread per predicate, each * taking only the tasks whose priority it accepts, and a `Threaded task runs * in place on the thread that took it. Nothing runs in parallel and * Unix.fork stays usable. * With `Growing (initial, n), the pool starts as `Domains initial and * adds a domain, up to n, each time ready work has found every worker * busy for grow_after seconds. It never shrinks. * *
val started : 'a scheduler -> boolWhether start has been called.
val stop : 'a scheduler -> unitStop the scheduler, let the tasks already running finish, and wait for its domains to return.
val reserve_blocking : 'a scheduler -> unit -> unitreserve_blocking s adds one slot to the `Threaded budget set by max_blocking and returns the function giving it back. A task that may park for as long as a resource it owns lives takes one, so that it cannot starve the others. Auxiliary threads above a lowered budget exit once idle.
module Task : sig ... endCore task registration. * * A task will be a set of events to watch, and a corresponding function to * execute when one of the events is triggered. * * The executed function may then return a list of new tasks to schedule.
Direct-style computations
run f executes f so that it can park on await instead of splitting into tasks. It returns as soon as f finishes or parks; a parked computation resumes on whichever domain picks up its task, which may not be the one it started on.
val await :
priority:'a ->
'a scheduler ->
[< Task.event ] list ->
Task.event listawait ~priority s events parks the calling computation until one of events occurs and returns those that did.
Only a computation running under run can park: calling this from a plain task handler raises Effect.Unhandled.
val suspend :
?delay:float ->
priority:'a ->
'a scheduler ->
((unit -> unit) -> unit) ->
unitsuspend ?delay ~priority s register parks the calling computation and calls register with a function that resumes it. The resumer may be called from any thread or domain; the computation then continues as a task of priority.
With delay, the computation also resumes once delay seconds have elapsed. It resumes once, on whichever comes first, and the resumer may then be called any number of times.
val thread : priority:'a -> 'a scheduler -> (unit -> unit) -> unitthread ~priority s fn runs fn on a thread of its own, on the domain of the pool worker that accepts priority and has the fewest such threads, so that they spread over the cores.
On a thread pool, and before the pool has started, the thread is created in place.
blocking fn runs fn and returns its result. A computation that parks inside fn blocks the calling thread until what it waits for occurs, and continues on that same thread.
Use it around code that has to finish on the thread it started on, such as a section that holds a mutex.
module Condition : sig ... endA condition a computation waits for without holding a worker.
val reschedule : ?delay:float -> priority:'a -> 'a scheduler -> unitreschedule ?delay ~priority s parks and resumes the computation under priority, to leave a priority a computation should no longer hold.
module Async : sig ... endAsynchronous task module * * This module implements an asynchronous API to Duppy.scheduler * It allows to create a task that will run and then go to sleep.
module type Transport_t = sig ... endModule type for Io functor.
module type Io_t = sig ... endReading and writing a socket from a computation running under run: both park until the socket is ready rather than returning to the scheduler.
module MakeIo (Transport : Transport_t) : Io_t with type socket = Transport.t* Duppy is a Caribbean patois word of West African origin meaning ghost or spirit. * Much of Caribbean folklore revolves around duppies. * Duppies are generally regarded as malevolent spirits. * They are said to come out and haunt people at night mostly, * and people from the islands claim to have seen them. * The 'Rolling Calf', 'Three footed horse' or 'Old Higue' are examples of the more malicious spirits. *