488 lines
16 KiB
Markdown
488 lines
16 KiB
Markdown
# Chronos - An efficient library for asynchronous programming
|
|
|
|
[![Github action](https://github.com/status-im/nim-chronos/workflows/CI/badge.svg)](https://github.com/status-im/nim-chronos/actions/workflows/ci.yml)
|
|
[![License: Apache](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
|
|
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
|
|
![Stability: experimental](https://img.shields.io/badge/stability-experimental-orange.svg)
|
|
|
|
## Introduction
|
|
|
|
Chronos is an efficient [async/await](https://en.wikipedia.org/wiki/Async/await) framework for Nim. Features include:
|
|
|
|
* Asynchronous socket and process I/O
|
|
* HTTP server with SSL/TLS support out of the box (no OpenSSL needed)
|
|
* Synchronization primitivies like queues, events and locks
|
|
* Cancellation
|
|
* Efficient dispatch pipeline with excellent multi-platform support
|
|
* Exception effect support (see [exception effects](#exception-effects))
|
|
|
|
## Installation
|
|
|
|
You can use Nim's official package manager Nimble to install Chronos:
|
|
|
|
```text
|
|
nimble install chronos
|
|
```
|
|
|
|
or add a dependency to your `.nimble` file:
|
|
|
|
```text
|
|
requires "chronos"
|
|
```
|
|
|
|
## Projects using `chronos`
|
|
|
|
* [libp2p](https://github.com/status-im/nim-libp2p) - Peer-to-Peer networking stack implemented in many languages
|
|
* [presto](https://github.com/status-im/nim-presto) - REST API framework
|
|
* [Scorper](https://github.com/bung87/scorper) - Web framework
|
|
* [2DeFi](https://github.com/gogolxdong/2DeFi) - Decentralised file system
|
|
* [websock](https://github.com/status-im/nim-websock/) - WebSocket library with lots of features
|
|
|
|
`chronos` is available in the [Nim Playground](https://play.nim-lang.org/#ix=2TpS)
|
|
|
|
Submit a PR to add yours!
|
|
|
|
## Documentation
|
|
|
|
### Concepts
|
|
|
|
Chronos implements the async/await paradigm in a self-contained library using
|
|
the macro and closure iterator transformation features provided by Nim.
|
|
|
|
The event loop is called a "dispatcher" and a single instance per thread is
|
|
created, as soon as one is needed.
|
|
|
|
To trigger a dispatcher's processing step, we need to call `poll()` - either
|
|
directly or through a wrapper like `runForever()` or `waitFor()`. Each step
|
|
handles any file descriptors, timers and callbacks that are ready to be
|
|
processed.
|
|
|
|
`Future` objects encapsulate the result of an `async` procedure upon successful
|
|
completion, and a list of callbacks to be scheduled after any type of
|
|
completion - be that success, failure or cancellation.
|
|
|
|
(These explicit callbacks are rarely used outside Chronos, being replaced by
|
|
implicit ones generated by async procedure execution and `await` chaining.)
|
|
|
|
Async procedures (those using the `{.async.}` pragma) return `Future` objects.
|
|
|
|
Inside an async procedure, you can `await` the future returned by another async
|
|
procedure. At this point, control will be handled to the event loop until that
|
|
future is completed.
|
|
|
|
Future completion is tested with `Future.finished()` and is defined as success,
|
|
failure or cancellation. This means that a future is either pending or completed.
|
|
|
|
To differentiate between completion states, we have `Future.failed()` and
|
|
`Future.cancelled()`.
|
|
|
|
### Dispatcher
|
|
|
|
You can run the "dispatcher" event loop forever, with `runForever()` which is defined as:
|
|
|
|
```nim
|
|
proc runForever*() =
|
|
while true:
|
|
poll()
|
|
```
|
|
|
|
You can also run it until a certain future is completed, with `waitFor()` which
|
|
will also call `Future.read()` on it:
|
|
|
|
```nim
|
|
proc p(): Future[int] {.async.} =
|
|
await sleepAsync(100.milliseconds)
|
|
return 1
|
|
|
|
echo waitFor p() # prints "1"
|
|
```
|
|
|
|
`waitFor()` is defined like this:
|
|
|
|
```nim
|
|
proc waitFor*[T](fut: Future[T]): T =
|
|
while not(fut.finished()):
|
|
poll()
|
|
return fut.read()
|
|
```
|
|
|
|
### Async procedures and methods
|
|
|
|
The `{.async.}` pragma will transform a procedure (or a method) returning a
|
|
specialised `Future` type into a closure iterator. If there is no return type
|
|
specified, a `Future[void]` is returned.
|
|
|
|
```nim
|
|
proc p() {.async.} =
|
|
await sleepAsync(100.milliseconds)
|
|
|
|
echo p().type # prints "Future[system.void]"
|
|
```
|
|
|
|
Whenever `await` is encountered inside an async procedure, control is passed
|
|
back to the dispatcher for as many steps as it's necessary for the awaited
|
|
future to complete successfully, fail or be cancelled. `await` calls the
|
|
equivalent of `Future.read()` on the completed future and returns the
|
|
encapsulated value.
|
|
|
|
```nim
|
|
proc p1() {.async.} =
|
|
await sleepAsync(1.seconds)
|
|
|
|
proc p2() {.async.} =
|
|
await sleepAsync(1.seconds)
|
|
|
|
proc p3() {.async.} =
|
|
let
|
|
fut1 = p1()
|
|
fut2 = p2()
|
|
# Just by executing the async procs, both resulting futures entered the
|
|
# dispatcher's queue and their "clocks" started ticking.
|
|
await fut1
|
|
await fut2
|
|
# Only one second passed while awaiting them both, not two.
|
|
|
|
waitFor p3()
|
|
```
|
|
|
|
Don't let `await`'s behaviour of giving back control to the dispatcher surprise
|
|
you. If an async procedure modifies global state, and you can't predict when it
|
|
will start executing, the only way to avoid that state changing underneath your
|
|
feet, in a certain section, is to not use `await` in it.
|
|
|
|
### Error handling
|
|
|
|
Exceptions inheriting from [`CatchableError`](https://nim-lang.org/docs/system.html#CatchableError)
|
|
interrupt execution of the `async` procedure. The exception is placed in the
|
|
`Future.error` field while changing the status of the `Future` to `Failed`
|
|
and callbacks are scheduled.
|
|
|
|
When a future is awaited, the exception is re-raised, traversing the `async`
|
|
execution chain until handled.
|
|
|
|
```nim
|
|
proc p1() {.async.} =
|
|
await sleepAsync(1.seconds)
|
|
raise newException(ValueError, "ValueError inherits from CatchableError")
|
|
|
|
proc p2() {.async.} =
|
|
await sleepAsync(1.seconds)
|
|
|
|
proc p3() {.async.} =
|
|
let
|
|
fut1 = p1()
|
|
fut2 = p2()
|
|
await fut1
|
|
echo "unreachable code here"
|
|
await fut2
|
|
|
|
# `waitFor()` would call `Future.read()` unconditionally, which would raise the
|
|
# exception in `Future.error`.
|
|
let fut3 = p3()
|
|
while not(fut3.finished()):
|
|
poll()
|
|
|
|
echo "fut3.state = ", fut3.state # "Failed"
|
|
if fut3.failed():
|
|
echo "p3() failed: ", fut3.error.name, ": ", fut3.error.msg
|
|
# prints "p3() failed: ValueError: ValueError inherits from CatchableError"
|
|
```
|
|
|
|
You can put the `await` in a `try` block, to deal with that exception sooner:
|
|
|
|
```nim
|
|
proc p3() {.async.} =
|
|
let
|
|
fut1 = p1()
|
|
fut2 = p2()
|
|
try:
|
|
await fut1
|
|
except CachableError:
|
|
echo "p1() failed: ", fut1.error.name, ": ", fut1.error.msg
|
|
echo "reachable code here"
|
|
await fut2
|
|
```
|
|
|
|
Because `chronos` ensures that all exceptions are re-routed to the `Future`,
|
|
`poll` will not itself raise exceptions.
|
|
|
|
`poll` may still panic / raise `Defect` if such are raised in user code due to
|
|
undefined behavior.
|
|
|
|
#### Checked exceptions
|
|
|
|
By specifying a `raises` list to an async procedure, you can check which
|
|
exceptions can be raised by it:
|
|
|
|
```nim
|
|
proc p1(): Future[void] {.async: (raises: [IOError]).} =
|
|
assert not (compiles do: raise newException(ValueError, "uh-uh"))
|
|
raise newException(IOError, "works") # Or any child of IOError
|
|
|
|
proc p2(): Future[void] {.async, (raises: [IOError]).} =
|
|
await p1() # Works, because await knows that p1
|
|
# can only raise IOError
|
|
```
|
|
|
|
Under the hood, the return type of `p1` will be rewritten to an internal type
|
|
which will convey raises informations to `await`.
|
|
|
|
#### The `Exception` type
|
|
|
|
Exceptions deriving from `Exception` are not caught by default as these may
|
|
include `Defect` and other forms undefined or uncatchable behavior.
|
|
|
|
Because exception effect tracking is turned on for `async` functions, this may
|
|
sometimes lead to compile errors around forward declarations, methods and
|
|
closures as Nim conservatively asssumes that any `Exception` might be raised
|
|
from those.
|
|
|
|
Make sure to excplicitly annotate these with `{.raises.}`:
|
|
|
|
```nim
|
|
# Forward declarations need to explicitly include a raises list:
|
|
proc myfunction() {.raises: [ValueError].}
|
|
|
|
# ... as do `proc` types
|
|
type MyClosure = proc() {.raises: [ValueError].}
|
|
|
|
proc myfunction() =
|
|
raise (ref ValueError)(msg: "Implementation here")
|
|
|
|
let closure: MyClosure = myfunction
|
|
```
|
|
|
|
For compatibility, `async` functions can be instructed to handle `Exception` as
|
|
well, specifying `handleException: true`. `Exception` that is not a `Defect` and
|
|
not a `CatchableError` will then be caught and remapped to
|
|
`AsyncExceptionError`:
|
|
|
|
```nim
|
|
proc raiseException() {.async: (handleException: true, raises: [AsyncExceptionError]).} =
|
|
raise (ref Exception)(msg: "Raising Exception is UB")
|
|
|
|
proc callRaiseException() {.async: (raises: []).} =
|
|
try:
|
|
raiseException()
|
|
except AsyncExceptionError as exc:
|
|
# The original Exception is available from the `parent` field
|
|
echo exc.parent.msg
|
|
```
|
|
|
|
This mode can be enabled globally with `-d:chronosHandleException` as a help
|
|
when porting code to `chronos` but should generally be avoided as global
|
|
configuration settings may interfere with libraries that use `chronos` leading
|
|
to unexpected behavior.
|
|
|
|
### Raw functions
|
|
|
|
Raw functions are those that interact with `chronos` via the `Future` type but
|
|
whose body does not go through the async transformation.
|
|
|
|
Such functions are created by adding `raw: true` to the `async` parameters:
|
|
|
|
```nim
|
|
proc rawAsync(): Future[void] {.async: (raw: true).} =
|
|
let future = newFuture[void]("rawAsync")
|
|
future.complete()
|
|
return future
|
|
```
|
|
|
|
Raw functions must not raise exceptions directly - they are implicitly declared
|
|
as `raises: []` - instead they should store exceptions in the returned `Future`:
|
|
|
|
```nim
|
|
proc rawFailure(): Future[void] {.async: (raw: true).} =
|
|
let future = newFuture[void]("rawAsync")
|
|
future.fail((ref ValueError)(msg: "Oh no!"))
|
|
return future
|
|
```
|
|
|
|
Raw functions can also use checked exceptions:
|
|
|
|
```nim
|
|
proc rawAsyncRaises(): Future[void] {.async: (raw: true, raises: [IOError]).} =
|
|
let fut = newFuture[void]()
|
|
assert not (compiles do: fut.fail((ref ValueError)(msg: "uh-uh")))
|
|
fut.fail((ref IOError)(msg: "IO"))
|
|
return fut
|
|
```
|
|
|
|
### Callbacks and closures
|
|
|
|
Callback/closure types are declared using the `async` annotation as usual:
|
|
|
|
```nim
|
|
type MyCallback = proc(): Future[void] {.async.}
|
|
|
|
proc runCallback(cb: MyCallback) {.async: (raises: []).} =
|
|
try:
|
|
await cb()
|
|
except CatchableError:
|
|
discard # handle errors as usual
|
|
```
|
|
|
|
When calling a callback, it is important to remember that the given function
|
|
may raise and exceptions need to be handled.
|
|
|
|
Checked exceptions can be used to limit the exceptions that a callback can
|
|
raise:
|
|
|
|
```nim
|
|
type MyEasyCallback = proc: Future[void] {.async: (raises: []).}
|
|
|
|
proc runCallback(cb: MyEasyCallback) {.async: (raises: [])} =
|
|
await cb()
|
|
```
|
|
|
|
### Platform independence
|
|
|
|
Several functions in `chronos` are backed by the operating system, such as
|
|
waiting for network events, creating files and sockets etc. The specific
|
|
exceptions that are raised by the OS is platform-dependent, thus such functions
|
|
are declared as raising `CatchableError` but will in general raise something
|
|
more specific. In particular, it's possible that some functions that are
|
|
annotated as raising `CatchableError` only raise on _some_ platforms - in order
|
|
to work on all platforms, calling code must assume that they will raise even
|
|
when they don't seem to do so on one platform.
|
|
|
|
### Cancellation support
|
|
|
|
Any running `Future` can be cancelled. This can be used for timeouts,
|
|
to let a user cancel a running task, to start multiple futures in parallel
|
|
and cancel them as soon as one finishes, etc.
|
|
|
|
```nim
|
|
import chronos/apps/http/httpclient
|
|
|
|
proc cancellationExample() {.async.} =
|
|
# Simple cancellation
|
|
let future = sleepAsync(10.minutes)
|
|
future.cancelSoon()
|
|
# `cancelSoon` will not wait for the cancellation
|
|
# to be finished, so the Future could still be
|
|
# pending at this point.
|
|
|
|
# Wait for cancellation
|
|
let future2 = sleepAsync(10.minutes)
|
|
await future2.cancelAndWait()
|
|
# Using `cancelAndWait`, we know that future2 isn't
|
|
# pending anymore. However, it could have completed
|
|
# before cancellation happened (in which case, it
|
|
# will hold a value)
|
|
|
|
# Race between futures
|
|
proc retrievePage(uri: string): Future[string] {.async.} =
|
|
let httpSession = HttpSessionRef.new()
|
|
try:
|
|
let resp = await httpSession.fetch(parseUri(uri))
|
|
return bytesToString(resp.data)
|
|
finally:
|
|
# be sure to always close the session
|
|
# `finally` will run also during cancellation -
|
|
# `noCancel` ensures that `closeWait` doesn't get cancelled
|
|
await noCancel(httpSession.closeWait())
|
|
|
|
let
|
|
futs =
|
|
@[
|
|
retrievePage("https://duckduckgo.com/?q=chronos"),
|
|
retrievePage("https://www.google.fr/search?q=chronos")
|
|
]
|
|
|
|
let finishedFut = await one(futs)
|
|
for fut in futs:
|
|
if not fut.finished:
|
|
fut.cancelSoon()
|
|
echo "Result: ", await finishedFut
|
|
|
|
waitFor(cancellationExample())
|
|
```
|
|
|
|
Even if cancellation is initiated, it is not guaranteed that
|
|
the operation gets cancelled - the future might still be completed
|
|
or fail depending on the ordering of events and the specifics of
|
|
the operation.
|
|
|
|
If the future indeed gets cancelled, `await` will raise a
|
|
`CancelledError` as is likely to happen in the following example:
|
|
```nim
|
|
proc c1 {.async.} =
|
|
echo "Before sleep"
|
|
try:
|
|
await sleepAsync(10.minutes)
|
|
echo "After sleep" # not reach due to cancellation
|
|
except CancelledError as exc:
|
|
echo "We got cancelled!"
|
|
raise exc
|
|
|
|
proc c2 {.async.} =
|
|
await c1()
|
|
echo "Never reached, since the CancelledError got re-raised"
|
|
|
|
let work = c2()
|
|
waitFor(work.cancelAndWait())
|
|
```
|
|
|
|
The `CancelledError` will now travel up the stack like any other exception.
|
|
It can be caught and handled (for instance, freeing some resources)
|
|
|
|
### Multiple async backend support
|
|
|
|
Thanks to its powerful macro support, Nim allows `async`/`await` to be
|
|
implemented in libraries with only minimal support from the language - as such,
|
|
multiple `async` libraries exist, including `chronos` and `asyncdispatch`, and
|
|
more may come to be developed in the futures.
|
|
|
|
Libraries built on top of `async`/`await` may wish to support multiple async
|
|
backends - the best way to do so is to create separate modules for each backend
|
|
that may be imported side-by-side - see [nim-metrics](https://github.com/status-im/nim-metrics/blob/master/metrics/)
|
|
for an example.
|
|
|
|
An alternative way is to select backend using a global compile flag - this
|
|
method makes it diffucult to compose applications that use both backends as may
|
|
happen with transitive dependencies, but may be appropriate in some cases -
|
|
libraries choosing this path should call the flag `asyncBackend`, allowing
|
|
applications to choose the backend with `-d:asyncBackend=<backend_name>`.
|
|
|
|
Known `async` backends include:
|
|
|
|
* `chronos` - this library (`-d:asyncBackend=chronos`)
|
|
* `asyncdispatch` the standard library `asyncdispatch` [module](https://nim-lang.org/docs/asyncdispatch.html) (`-d:asyncBackend=asyncdispatch`)
|
|
* `none` - ``-d:asyncBackend=none`` - disable ``async`` support completely
|
|
|
|
``none`` can be used when a library supports both a synchronous and
|
|
asynchronous API, to disable the latter.
|
|
|
|
### Compile-time configuration
|
|
|
|
`chronos` contains several compile-time [configuration options](./chronos/config.nim) enabling stricter compile-time checks and debugging helpers whose runtime cost may be significant.
|
|
|
|
Strictness options generally will become default in future chronos releases and allow adapting existing code without changing the new version - see the [`config.nim`](./chronos/config.nim) module for more information.
|
|
|
|
## TODO
|
|
* Pipe/Subprocess Transports.
|
|
* Multithreading Stream/Datagram servers
|
|
|
|
## Contributing
|
|
|
|
When submitting pull requests, please add test cases for any new features or fixes and make sure `nimble test` is still able to execute the entire test suite successfully.
|
|
|
|
`chronos` follows the [Status Nim Style Guide](https://status-im.github.io/nim-style-guide/).
|
|
|
|
## Other resources
|
|
|
|
* [Historical differences with asyncdispatch](https://github.com/status-im/nim-chronos/wiki/AsyncDispatch-comparison)
|
|
|
|
## License
|
|
|
|
Licensed and distributed under either of
|
|
|
|
* MIT license: [LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT
|
|
|
|
or
|
|
|
|
* Apache License, Version 2.0, ([LICENSE-APACHEv2](LICENSE-APACHEv2) or http://www.apache.org/licenses/LICENSE-2.0)
|
|
|
|
at your option. These files may not be copied, modified, or distributed except according to those terms.
|