Module Duppy

Advanced 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 task scheduler for OCaml.

* * 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.

type 'a scheduler

A scheduler is a device for processing tasks. * * 'a is the type of objects used for priorities.

type execution_class = [
  1. | `Immediate
  2. | `Direct
  3. | `Threaded
]

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.
type wrapper = {
  1. wrap : 'a. (unit -> 'a) -> 'a;
}

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.

exception Unknown_domain of int
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 scheduler

Initiate a new scheduler. It has no domains until start is called. *

  • parameter on_error

    called when a task raises. *

    @param on_fatal

    called when the event loop itself crashes, which should be * considered a MAJOR FAILURE: all non-ready tasks are dropped. Not called once * the program is exiting, whose exit handlers may tear down what the loop * waits on. Default: print the backtrace and exit. *

    @param rank

    the urgency of a priority, lowest first, between 0 and 63. * Default: fun _ -> 0 *

    @param classify

    how each priority is run. Default: fun _ -> `Threaded *

    @param wrapper

    wraps every task body. Default: run it as is

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 -> unit

start 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. * *

  • parameter pool

    Default: `Domains (Domain.recommended_domain_count ()) *

    @param max_blocking

    the most `Threaded tasks that may be in flight at * once over the whole pool, whichever workers took them. Unused by a thread * pool. Default: 64 *

    @param grow_after

    Seconds of saturation before a growing pool adds a * domain. Default: 1. *

    @param log

    Logging function. Default: no logging

val started : 'a scheduler -> bool

Whether start has been called.

val stop : 'a scheduler -> unit

Stop the scheduler, let the tasks already running finish, and wait for its domains to return.

val reserve_blocking : 'a scheduler -> unit -> unit

reserve_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 ... end

Core 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.

val run : (unit -> unit) -> unit

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 list

await ~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) -> unit

suspend ?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) -> unit

thread ~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.

val blocking : (unit -> 'a) -> 'a

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 ... end

A condition a computation waits for without holding a worker.

val time : unit -> float

Monotonic seconds with an arbitrary origin, the clock of every delay.

val reschedule : ?delay:float -> priority:'a -> 'a scheduler -> unit

reschedule ?delay ~priority s parks and resumes the computation under priority, to leave a priority a computation should no longer hold.

module Async : sig ... end

Asynchronous 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 ... end

Module type for Io functor.

module type Io_t = sig ... end

Reading and writing a socket from a computation running under run: both park until the socket is ready rather than returning to the scheduler.

module Io : Io_t with type socket = Unix.file_descr

Some culture..

* 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. *

http://en.wikipedia.org/wiki/Duppy