#Asynchronous programming library for MoonBit

    Packages under this directory provide basic asynchronous IO functionality for MoonBit, as well as useful asynchronous programming facilities. Currently, this library has full support for native backend and wasm backend. JavaScript backend has browser-compatible support for a limited set of API (most notably HTTP client, backed by fetch API), and wasm-gc backend is not supported. All operating systems supported by MoonBit (Linux/MacOS/Windows) are supported. API document is available at https://mooncakes.io/docs/moonbitlang/core/async.

    WARNING: this library is current experimental, API is subject to future change.

    #Installation

    Asynchronous programming support is part of the MoonBit standard library moonbitlang/core, so there is no need to explicitly install any library. In order to use asynchronous programming API, users do need to import specific packages, though:

    • moonbitlang/core/async: basic asynchronous operations, asynchronous control flow primitives. This package must be imported in order to use async fn main or async test
    • moonbitlang/core/async/socket: TCP and UDP socket
    • moonbitlang/core/async/stdio: operations on standard output channels
    • moonbitlang/core/async/fs: file system operations, such as file IO and directory reading
    • moonbitlang/core/async/process: spawning system process
    • moonbitlang/core/async/shell: shell-free commands, pipelines, redirection, and glob expansion
    • moonbitlang/core/async/http: HTTP client and server support. Features:
      • HTTPS client support
      • CONNECT based HTTP/HTTPS proxy support
    • moonbitlang/core/async/websocket: WebSocket client and server support. Features:
      • TLS encrypted WebSocket (wss://) support
      • CONNECT based HTTP/HTTPS proxy support
      • native integration with moonbitlang/core/async/http
    • moonbitlang/core/async/io: generic IO abstraction & utilities, such as in-memory pipe
    • moonbitlang/core/async/signal: control signal handling behavior
    • moonbitlang/core/async/raw_fd: integrate arbitrary file descriptor into moonbitlang/core/async's event loop. Only supports native backend
    • moonbitlang/core/async/gzip: wrap async stream with gzip compression/decompression
    • moonbitlang/core/async/tls: TLS support via OpenSSL/Schannel
    • moonbitlang/core/async/pipe: operations on operating system pipes
    • moonbitlang/core/async/js_async: integration with JavaScript promise and readable stream. Only supports JavaScript backend.

    To use these packages, add them to the import field of moon.pkg.

    #Runtime model

    Currently, moonbitlang/core/async features a single-threaded, cooperative multitasking model. With this single-threaded model, code without suspension point (i.e. code that does not call async functions) can always be considered atomic. This model eliminates the need for many locks and bugs. However, it also come with some caveats:

    • task scheduling can only happen when current task suspend itself, by performing some asynchronous IO operation or manually calling @async.pause. If you perform heavy computation loop without pausing from time to time, the whole program will be blocked until the loop terminates, and other task will not get executed before that. Similarly, performing blocking IO operation not provided by moonbitlang/core/async may block the whole program as well

    • in the same way, task cancellation can only happen when a task is in suspended state (blocked by IO operation or manually pause'ed)

    • although internally moonbitlang/core/async may use OS threads to perform some IO job, user code can only utilize one hardware processor

    #Task cancellation

    In moonbitlang/core/async, every suspension point (a sleep, a wait operation, asynchronous IO operations or @async.pause) are cancellable by default. Therefore, asynchronous operations built from these basic primitives are also cancellable by default, so there is no need to worry about accidentally creating uncancellable task. As a consequence, a blocking async fn is fully modular and does not lose any generality in moonbitlang/core/async, and we encourage libraries built around moonbitlang/core/async to provide public API in simple, blocking style whenever possible.

    In moonbitlang/core/async, when a task is cancelled, it will receive a special signal at where it suspended. This cancellation signal is similar to, but not the same as an error. The cancellation signal propogates like an error, and can trigger defer and errdefer blocks. However, catch can not capture the cancellation signal.

    In most cases, users don't need to handle cancellation specially. Cleanup related logic can be put in defer or errdefer, while other error handlers usually don't need to handle cancellation anyway. If special handling of cancellation is indeed necessary, @async.handle_cancellation can be used.

    Cancellation is a sticky state attached to every task in moonbitlang/core/async. The aforementioned cancellation signal is merely a notification for the cancelled task, capturing that signal via @async.handle_cancellation does not revert cancellation. The task will stay in cancelled state, and the next unprotected async operation will get cancelled immediately. To truly protect a piece of critical code from cancellation, use @async.protect_from_cancel.

    async function types are cancellable by default in MoonBit. Non-cancellable operations (i.e. all asynchronous code are wrapped in @async.protect_from_cancel) can be explicitly annotated with nocancel at the end of the signature, to indicate that the function cannot be cancelled from type level. Calling cancellable function from noconcel context is a type error.

    #Structured concurrency and error propagation

    moonbitlang/core/async features structured concurrency. In moonbitlang/core/async, every asynchronous task must be spawned in a task group. Task groups can be created with the with_task_group function

    async fn with_task_group(async (TaskGroup) -> Unit) -> Unit

    with_task_group(f) will create a new task group and run f in the new group. f will receive the new group itself as argument, and may spawn new task using that group. Spawned tasks run concurrently with other code (subjected to the single threaded execution model's restriction mentioned above).

    By default, with_task_group(f) will wait for all child tasks, including f itself to complete before returning normally. If any of the child task failed, with_task_group will fail with the same error as well.

    In any case, when with_task_group decides that it wants to exit immediately, it will automatically cancel all remaining child tasks, and wait for their cancellation cleanup logic's completion before actually returning. So When with_task_group returns, it is guaranteed that all tasks spawned in the group have terminated completely, leaving no room for orphan task and resource leak.

    For more behavior detail and useful API, consult the API document of @async.with_task_group and methods on @async.TaskGroup.

    Cond

    A condition variable that can be used for synchronization between tasks.

    Queue

    An asynchronous queue, where reader can wait for data to arrive in a non-blocking manner.

    Semaphore

    A semaphore data structure useful for synchronization between tasks

    BreakFromSpawnLoop

    pub(all) suberror BreakFromSpawnLoop

    A special error used to break away from TaskGroup::spawn_loop.

    TimeoutError

    Default error raised by @async.with_timeout when timeout expires

    TimerCancelled

    When waiting for a timer, if the timer is explicitly cancelled, the waiter will receive this error.

    WaitedTaskAlreadyCancelled

    #alias(TaskCancelled, deprecated="`TaskCancelled` is deprecated, use `WaitedTaskAlreadyCancelled` instead")
    pub suberror WaitedTaskAlreadyCancelled derive(
    Debug
    ,
    ToJson
    )

    Special error indicating a waited task is cancelled. Note that this error has nothing to do with cancellation of current task.

    Lazy

    type Lazy[X]

    A lazily initialized value that requires async operation to compute. The async initialization process will start as soon as the result is requested. the result will be cached, so the initialization process will run only once. If no one is waiting for the result anymore before initialization completes, the initialization process will be cancelled automatically.

    Lazy::Lazy

    #callsite(autofill(loc))
    #alias(new, deprecated="`new` is deprecated, use `Lazy` instead")
    #as_free_fn(lazy_init, deprecated="use `@async.Lazy(..)` instead")
    fn[X] Lazy::Lazy(f : async () -> X, loc~ : SourceLoc) -> Lazy[X]

    Create a new lazily initialized value by passing an async initialization function. The function f will be started in the background automatically when someone request for the result of the lazy value.

    If all waiters are cancelled before f completes, f will be cancelled automatically. After f is cancelled, calling .wait() on the lazy value will start f again.

    If f completes normally or due to an error, the result will be cached, and subsequent .wait() on the lazy value always complete immediately.

    Lazy::wait

    async fn[X] Lazy::wait(self : Lazy[X]) -> X

    Wait for the result of a lazily initialized value.

    If the initialization of the value has not yet started, it will be started automatically.

    If the initialization of the value has already completed, the cached result will be returned automatically.

    Mutex

    type Mutex

    A mutex for synchronization between multiple tasks. Essentially a mutex is just a semaphore of size one.

    Mutex::Mutex

    fn Mutex::Mutex() -> Mutex

    Create a new mutex. The mutex is initially in released state.

    Mutex::acquire

    async fn Mutex::acquire(self : Mutex) -> Unit noraise

    Acquire the mutex.

    If the mutex is already acquired by someone else, acquire will wait until the mutex is released.

    If several tasks are both waiting for the mutex, they will acquire the mutex in a first-come-first-serve manner

    acquire is cancellation safe. When acquire is cancelled, it will never trigger any side effect. In particular, a cancelled acquire request will never actually acquire the mutex.

    Mutex::release

    fn Mutex::release(self : Mutex) -> Unit

    Release the mutex. The mutex must have been acquired, otherwise the program will abort.

    Mutex::try_acquire

    fn Mutex::try_acquire(self : Mutex) -> Bool

    Try to acquire the mutex, but do not wait for it. If the mutex is currently available, it will be acquired and true will be returned. If the mutex is currently not available, false will be returned.

    try_acquire follow the same FIFO rule of acquire: if there are acquire call waiting for the mutex, try_acquire always return false.

    RetryMethod

    pub(all) enum RetryMethod {
    Immediate
    FixedDelay(Int)
    ExponentialDelay(initial~ : Int, factor~ : Double, maximum~ : Int)
    }

    RetryMethod describes different retry strategies used by various APIs:

    • Immediate: failed task will immediately be restarted.

    • FixedDelay(t), failed task will be restarted after sleeping for t milliseconds

    • ExponentialDelay(initial~, factor~, maximum~): failed task will be restarted with an exponentially growing delay. The initial delay is initial, and after every failure, the delay will be multiplied by factor, but will never exceed maximum.

    Task

    type Task[X]

    Task[X] represents a running task with result type X, it can be used to wait and retrieve the result value of the task.

    Task::cancel

    fn[X] Task::cancel(self : Task[X]) -> Unit

    Cancel a task. Subsequent attempt to wait for the task will receive error.

    Task::try_wait

    fn[X] Task::try_wait(self : Task[X]) -> X? raise

    Try to obtain the result of the task. If the task already terminated, its result value will be returned. If the task already failed, try_wait will fail immediately. If the task is still running, try_wait returns None. try_wait is a synchoronous function: it never blocks.

    Task::wait

    async fn[X] Task::wait(self : Task[X]) -> X

    Wait for a task and retrieve its result value. If the task fails, wait will also fail with the same error. If the target task terminated due to cancellation, wait will raise TaskCancelled.

    TaskGroup

    type TaskGroup

    A TaskGroup can be used to spawn children tasks that run in parallel. Task groups implements structured concurrency: a task group will only return after all its children task terminates.

    Task groups also handles error propagation: by default, if any child task raises error, the whole task group will also raise that error, and all other remaining child tasks will be cancelled.

    A task group produces no result value of its own: with_task_group returns Unit. To compute a value inside a group, assign it to a mutable variable declared outside the group, or start a task with spawn and retrieve its result with Task::wait.

    TaskGroup::add_defer

    fn TaskGroup::add_defer(self : TaskGroup, block : async () -> Unit nocancel) -> Unit

    Attach a defer block, represented as a cleanup function, to a task group. The clenaup function will be invoked when the group terminates, so all child task of the group (created via spawn or spawn_bg) must have terminated when any group defer code is invoked. Calling spawn or spawn_bg inside group defer is not allowed, and will abort the program immediately.

    Group scoped defer blocks are executed in FILO order, just like normal defer. with_task_group will only exit after all group defer blocks terminate.

    Group scoped defer must be nocancel. If cancellable code have to be run anyway, use protect_from_cancel. It is highly recommended to add a hard timeout to async defer block in this case, to avoid infinite hanging due to blocked operation.

    If a group defer failed with error, that error become the result of the whole with_task_group, but other remaining group defer blocks will still get executed in order. The final error raised by with_task_group would be the error raised by the last failing group defer block.

    TaskGroup::return_immediately

    async fn TaskGroup::return_immediately(self : TaskGroup) -> Unit noraise

    Request a task group to finish normally, cancelling the child tasks that are still running.

    If the group is still running, this marks the group as successfully completed and cancels every child task in the group, including the current task if it belongs to the group. If the group is no longer running (it has already failed, been cancelled, or completed), this has no effect on the group. In both cases, if the current task is being cancelled (and is not inside protect_from_cancel), the call does not return: like any other async operation in a cancelled task, it raises the cancellation signal.

    Despite the name, the group does not exit at once. As with any other cancellation, with_task_group still waits for every child task to terminate, including its defer blocks and code protected by protect_from_cancel, and then runs the group's add_defer blocks. If this call is what ended the group (the group was still running), with_task_group then returns normally, unless a group defer block, or a child task spawned without allow_failure=true, raises an error during that cleanup, in which case with_task_group fails with the last such error. If the group had already failed, been cancelled or completed, this call does not change how with_task_group ends.

    TaskGroup::spawn

    #callsite(autofill(loc))
    fn[X] TaskGroup::spawn(self : TaskGroup, f : async () -> X, no_wait? : Bool, allow_failure? : Bool, loc~ : SourceLoc) -> Task[X]

    Spawn a child task in a task group, compute a result asynchronously. A task handle will be returned, the result value of the task can be waited and retrieved using .wait(), or cancelled using .cancel().

    Unless no_wait (false by default) is true, the task group will wait for this task to complete before normal exit. No matter what the value of no_wait is, with_task_group will only return after all child tasks terminate. The task will be cancelled automatically if it is still running when the task group wishes to terminate,

    Unless allow_failure (false by default) is true, Ithe whole task group will also fail if the spawned task fails, other tasks in the group will be cancelled in this case.

    Cancelled task (either cancelled by the group or manually cancelled via Task::cancel) are not consider failing, although they raise a cancellation error. However, if the task raise some other error during cancellation handling, it will be considered failing.

    If the task group is already cancelled, but with_task_group is still running (i.e. there are still other running child task in the group), spawn will still succeed, but the child task will be cancelled immediately. In this case, the child task still get a chance to start execution (which means you can rely on defer at the start of child task being triggered), but will be in cancelled state from the very beginning (e.g. all async IO operation will be cancelled immediately before starting).

    Calling spawn after with_task_group completed or inside group.add_defer is not allowed, and will abort the program immediately.

    It is undefined whether the child task will start running immediately before spawn returns.

    TaskGroup::spawn_bg

    #callsite(autofill(loc))
    fn TaskGroup::spawn_bg(self : TaskGroup, f : async () -> Unit, no_wait? : Bool, allow_failure? : Bool, loc~ : SourceLoc) -> Unit

    Spawn a child task in a task group, and run it asynchronously in the background.

    Unless no_wait (false by default) is true, the task group will wait for this task to complete before normal exit. No matter what the value of no_wait is, with_task_group will only return after all child tasks terminate. The task will be cancelled automatically if it is still running when the task group wishes to terminate,

    Unless allow_failure (false by default) is true, the whole task group will also fail if the spawned task fails, other tasks in the group will be cancelled in this case.

    Cancelled task (either cancelled by the group or manually cancelled via Task::cancel) are not consider failing, although they raise a cancellation error. However, if the task raise some other error during cancellation handling, it will be considered failing.

    If the task group is already cancelled, but with_task_group is still running (i.e. there are still other running child task in the group), spawn_bg will still succeed, but the child task will be cancelled immediately. In this case, the child task still get a chance to start execution (which means you can rely on defer at the start of child task being triggered), but will be in cancelled state from the very beginning (e.g. all async IO operation will be cancelled immediately before starting).

    Calling spawn_bg after with_task_group completed or inside group.add_defer is not allowed, and will abort the program immediately.

    It is undefined whether the child task will start running immediately before spawn_bg returns.

    TaskGroup::spawn_loop

    fn TaskGroup::spawn_loop(self : TaskGroup, f : async () -> Unit, no_wait? : Bool, allow_failure? : Bool, retry? : RetryMethod, max_retry? : Int, fatal_error? : (Error) -> Bool) -> Unit

    Similar to spawn_bg, but the spawn a loop, i.e. the spawned task will be restarted after it terminates. The spawned task can terminate the loop by raising the special error BreakFromSpawnLoop, the loop will terminate normally instead of failing in this case.

    If retry_method is set, within each loop, the spawned task will be automatically restarted on failure. The restart strategy is determined by the value of retry_method. max_retry and fatal_error will be passed as-is to retry. See retry and RetryMethod for more details.

    The meaning of no_wait and allow_failure is the same as spawn_bg.

    Timer

    pub struct Timer {
    duration : Int
    // private fields
    }

    A timer that expires after a fixed duration with support for multiple waiters and refreshing. The timer will expire only once.

    The timer object itself has no side effect. The only way to observe the existence of a timer is by waiting for it. So if no one is waiting for a timer, the timer will not block program termination even if it is still active.

    Timer::Timer

    #alias(new, deprecated="`new` is deprecated, use `Timer` instead")
    fn Timer::Timer(duration : Int) -> Timer

    Create a new timer that expires after duration milliseconds. The timer will start immediately, and expire after the duration elapsed.

    Timer::cancel

    fn Timer::cancel(timer : Timer, err? : Error) -> Unit

    Manually cancel a timer. All waiters on the timer will fail immediately with err (TimerCancelled by default). All subsequent waiters will also fail with err immediately.

    Timer::refresh

    fn Timer::refresh(timer : Timer) -> Unit

    Refresh a timer. If the timer has already terminated, it is restarted immediately, and will be triggered again after the timer expires. If the timer is currently active, its expiration time will be delayed to current time + duration of the timer. If the timer has been cancelled, refresh has no effect.

    Timer::wait

    async fn Timer::wait(timer : Timer) -> Unit

    Wait for the expiration of a timer. If the timer has already terminated, wait will return immediately. If the timer has already been cancelled, or if the timer is cancelled during the wait, wait will fail immediately.

    all

    async fn[X] all(tasks : ArrayView[async () -> X], max_concurrent? : Int) -> Array[X]

    all(tasks, max_concurrent?) waits for all tasks to complete and returns their results.

    • All tasks are spawned and executed (optionally with max_concurrent limit).
    • If all tasks succeed, all returns an array of results in the same order as tasks.
    • If any task fails, all fails with that error and other running tasks are cancelled.

    The max_concurrent parameter limits the number of tasks that can run concurrently. If not specified, all tasks run at once.

    any

    #callsite(autofill(loc))
    async fn[X] any(tasks : ArrayView[async () -> X], max_concurrent? : Int, allow_failure? : Bool, loc~ : SourceLoc) -> X

    any(tasks, max_concurrent?, allow_failure?) waits for the first task to complete successfully.

    • All tasks are spawned and executed (optionally with max_concurrent limit).
    • When the first task completes successfully, any returns its result immediately and all other running tasks are cancelled.
    • If allow_failure is false (default), any fails as soon as any task fails.
    • If allow_failure is true, failing tasks are ignored and any waits for the first success.
    • If all tasks fail, any fails with the error from a random task.

    The max_concurrent parameter limits the number of tasks that can run concurrently. If not specified, all tasks run at once.

    any requires at least one task and will fail if the array is empty.

    handle_cancellation

    async fn[X] handle_cancellation(f : async () -> X) -> X?

    Run an asynchronous function, and return None if it is cancelled.

    is_being_cancelled

    fn is_being_cancelled() -> Bool

    Check if current coroutine is being cancelled.

    now

    fn now() -> Int64

    Get current time since Unix EPOCH in milliseconds. The event loop uses the same timer as now() for timers.

    pause

    async fn pause() -> Unit noraise

    Pause current coroutine, and give other coroutines a chance to execute. Current coroutine will resume its execution on the next scheduler slice. Useful for integrating time consuming synchronous code with asynchronous code.

    platform

    Current operating system. A single WASM binary can be run of different operating systems, so current OS information is only available at runtime.

    protect_from_cancel

    async fn[X] protect_from_cancel(f : async () -> X) -> X nocancel

    Run an asynchronous function, and make current coroutine non-cancellable while the function is running.

    retry

    async fn[X] retry(strategy : RetryMethod, max_retry? : Int, fatal_error? : (Error) -> Bool, f : async () -> X) -> X

    retry(strategy, f) will keep retrying some async operation until success. If f returns value normally, retry will immediately return value. If f fails with error, retry will restart f again. The restart strategy is determined by strategy, see the RetryMethod type for more details.

    If current task is cancelled, retry will be cancelled too.

    By default, the number of retry attempt is unbounded. However, if max_retry is set, the number of retry attempts will not exceed the value of max_retry. If retry attempts reaches limit, retry will raise the error from f's last attempt.

    If fatal_error is present, and f raises an error err such that fatal_error(err) is true, retry will fail immediately with err.

    set_deadlock_handler

    fn set_deadlock_handler(f : (Array[SourceLoc]) -> Unit) -> Unit

    Set a custom handler for dead lock condition

    set_external_event_loop

    Set an external event loop. When an external event loop is set, MoonBit will run its own async runtime in a dedicated thread, and wake the user's custom event loop when new events arrive on MoonBit's event loop. This way, the user can integrate async MoonBit code with a custom event loop, such as GUI event loop provided by the operating system.

    sleep

    async fn sleep(duration : Int) -> Unit noraise

    Sleep for duration milliseconds. This function can run in parallel with other async code without blocking the whole program.

    with_task_group

    #callsite(autofill(loc))
    async fn with_task_group(f : async (TaskGroup) -> Unit, loc~ : SourceLoc) -> Unit

    with_task_group(f) creates a new task group and run f with the new group. f itself will be run in a child task of the new group.

    with_task_group exits after the whole group terminates, which means all child tasks in the group have terminated, including f.

    When:
    • certain task in the group raises an fatal error,
    • return_immediately is invoked on the group
    • the whole with_task_group call is cancelled with_task_group will cancel all remaining children task automatically. with_task_group will still wait for all (cancelled) children to complete, though.

    with_timeout

    async fn[X] with_timeout(time : Int, f : async () -> X, error? : Error) -> X

    with_timeout(timeout, f) run the async function f.

    • If f return value before timeout, with_timeout will return value immediately.

    • If f fail before timeout, with_timeout will fail immediately.

    • If f is still running after timeout milliseconds, with_timeout will fail with TimeoutError, or the value of error, if explicitly specified.

    • If the whole with_timeout call is cancelled before f completes, f will be cancelled immediately

    Note that with_timeout will succeed as long as f completes, and the result of f is never silently discarded.

    with_timeout_opt

    async fn[X] with_timeout_opt(time : Int, f : async () -> X) -> X?

    with_timeout_opt(timeout, f) run the async function f.

    • If f return value before timeout, with_timeout_opt will return Some(value) immediately.

    • If f fail before timeout, with_timeout_opt will fail immediately.

    • If f is still running after timeout milliseconds, f is cancelled. with_timeout_opt does not return until f has terminated, including its defer blocks and any code protected by protect_from_cancel, so it can return later than timeout. It then returns None. If f returns a value anyway (for example because the remaining work was protected from cancellation), with_timeout_opt returns Some(value) instead, and if f raises an error during its cleanup, with_timeout_opt fails with that error.

    • If the whole with_timeout_opt call is cancelled before f completes, f will be cancelled immediately

    Note that with_timeout_opt will succeed as long as f completes, and the result of f is never silently discarded.