package async_extended

  1. Overview
  2. Docs

A resource that can be acquired to obtain Handles that should be released by the user later. Most resources can be acquired multiple times by multiple concurrent async jobs. What exactly happens when you acquire depends on the specific resource.

Possible examples of resources:

  • File (acquire opens a file)
  • Tcp endpoints (acquire creates a connection)
  • Entitlements (the motivating use case)
  • Semaphore (acquire is blocking and takes a lock)
  • Slot (like semaphore, but acquire is non-blocking and fails)

Definitions:

It may be useful think to think about resources and dependencies between them in terms of *activation*s. Activation of resource x is a pair of events (and the corresponding period of time): (i) x has been successfully acquired (to obtain a handle h) and (ii) Handle.release h has been called.

We say that activation a is enclosed into activation b whenever a begins before b and a ends after b

module Raw : sig ... end
type ('a, 'e) t = ('a, 'e) Raw.t
val create : acquire:(unit -> ('a, 'e) Core.Result.t Async_kernel.Deferred.t) -> release:('a -> unit Async_kernel.Deferred.t) -> ('a, 'e) t
val with_ : ('a, 'e) t -> f:('a -> 'b Async_kernel.Deferred.t) -> ('b, 'e) Core.Result.t Async_kernel.Deferred.t

Access a resource without having to deal with Handle.t explicitly. The resource is acquired before f is called and released after f returns a result or raises an error to the enclosing monitor.

f should not use 'a after it raises or returns, whichever happens first.

Bind corresponds to resource dependency: when acquiring x >>= f, resource x will be acquired, then f is going to be evaluated, then the result of f is going to be acquired. Releases will be done in the opposite order.

include Core.Monad.S2 with type ('a, 'e) t := ('a, 'e) t
val (>>=) : ('a, 'e) t -> ('a -> ('b, 'e) t) -> ('b, 'e) t
val (>>|) : ('a, 'e) t -> ('a -> 'b) -> ('b, 'e) t
module Let_syntax : sig ... end
module Monad_infix : sig ... end
val bind : ('a, 'e) t -> f:('a -> ('b, 'e) t) -> ('b, 'e) t
val return : 'a -> ('a, _) t
val map : ('a, 'e) t -> f:('a -> 'b) -> ('b, 'e) t
val join : (('a, 'e) t, 'e) t -> ('a, 'e) t
val ignore_m : (_, 'e) t -> (unit, 'e) t
val all : ('a, 'e) t list -> ('a list, 'e) t
val all_ignore : (unit, 'e) t list -> (unit, 'e) t
val map_error : ('a, 'e1) t -> f:('e1 -> 'e2) -> ('a, 'e2) t
val fail : 'e -> ('a, 'e) t
val shared : ('a, 'e) t -> ('a, 'e) t

The idea is the following: If you try to acquire a shared resource that's already been acquired, but not yet released then, instead of acquiring it again, you use the value acquired earlier. You only release the underlying resource when all handles to the shared resource get released.

More precisely, if y = shared x and x is exclusively used here then: (i) every activation of y is enclosed into an activation of x; (ii) at any time there is at most one activation of x; (iii) activations of x are as short as possible otherwise.

Beware shared is not referentially transparent in that acquire (shared x) followed by acquire (shared x) will acquire x twice, so you always want to bind the result to a variable: let y = shared x in ... (* acquire y multiple times *)

As an example of what you can do with this, in dart library shared lets us:

  • Coalesce multiple requests for the same entitlement by the same user into one.
  • Only establish up to one connection to dart server per user.

In general, it might be helpful when:

  • If acquiring the original resource is costly;
  • If acquiring x multiple times concurrently is not safe;
  • If multiple acquirings of x would block each other;
  • <your idea>.
module Memo (Key : Core.Hashable) : sig ... end
val delayed_release : ('a, 'e) t -> delay:Core.Time.Span.t -> ('a, 'e) t

Delay all releases by the given amount and don't wait for them