Eugene Kabanov 02fda01bf2
Add facility to emulate signals on Windows. (#357)
* Initial Windows asyncproc part.
Deprecate usage of net/nativesockets in handles, asyncloop.
Introduce primitives with inheritable flag.

* Some posix fixes.

* More duplicate fixes.

* Fix AsyncProcessOption.EchoCommand option.
Remove isMainModule code.
Fix execCommand to use AsyncProcessOption.EvalCommand.

* Initial asyncproc tests commit.

* Some Posix fixes.

* Fix Posix crash.

* Add testsuite.
Fix osnet.nim to use proper declarations.
Fix timer.nim to use proper declarations.
Add Windows environment handling procedures.
Fix createAsyncPipe.
Add leaks tracking for AsyncProcessRef.

* Fix O_CLOEXEC constant value.

* Add file descriptors leak test.

* Remove commented code.
Refactor exceptions.
Fix compilation warnings.

* No exception ioselectors_kqueue initial commit.

* Some BSD fixes.
Linux refactoring of selectors.nim.

* Some fixes to move further.

* Last Linux fixes.

* Fixes for asyncloop to use 2nd version of selectors api.

* Add osutils.nim.

* Some fixes.

* Hardening resumeRead(), resumeWrite() and consumers.
Add ESRCH handling.
Introduce no-exception fromPipe2.

* Make Windows part exception-free and fix zombie race issue.

* createStreamServer() fixes.

* Upgrade asyncproc to use non-exception primitives.
Fix ioselectors_kqueue to use closeFd().

* Refactor accept() and acceptLoop() to be exception free.

* Deprecated some `result` usage.
Last fixes to make stream.nim exception free.
Use closeFd().
Refactor some loops to use handleEintr().

* Fix connect() forgot to unregister socket on error.

* All createAsyncSocket() sockets should be closed with unregisterAndCloseFd().

* Attempt to fix posix bug with incomplete output.

* Change number of runs in big test.

* Refactoring pipes creation. Attempt to fix "write error: Resource temporarily unavailable".

* Fix waitForExit(duration) code.
Fix test exit code.

* Fix Windows missing SIGTERM.

* Fix mistype.

* Fix compilation error.

* Attempt to fix Nim 1.6 issue.

* Eliminate Nim's WideCString usage to avoid any problems in future.

* Deprecate posix usage in osnet.

* Eliminate unused imports.

* Some debugging statements for investigation.

* Remove all the debugging statements.

* Remove testhelpers in favor of unittest2/asynctests.

* Fix flaky test.

* Make test more resilient to timings.

* Add memory size dump to CI.

* Refactor some blocks to use valueOr.
Make waitForExit to use kill() instead of terminate().

* Remove memory dumps.

* Fix peekProcessExitCode() blocks issue.

* Fix Windows issue.

* Add some debugging echoes.

* Fix compilation error.

* Add some debugging echoes.

* Add more helpers.

* Fix compilation error.

* waitForExit() is primary suspect.

* Remove fast-path for waitForExit.
Remove loop for reading signal events.

* Remove all debugging echoes.

* Return one debugging echo.

* Fix futures tests.

* Add test for multiple processes waiting to attempt stress test.

* Refactor ioselectors_epoll for better signalfd and process monitoring.
Add more race condition fixes to waitForExit.
Fix some Nim 1.6 warnings.

* Fix after rebase issues and warnings.

* Fix style issues.
Fix different Nim versions issues.
Workaround `signalfd` style issues.

* Add one more Linux signals workaround.
Add one more multiple processes test.

* Windows fixes.

* Remove unixPlatform define.
Fix WSAECONNABORTED for devel.

* Temporarily disable rate limit tests.
Fix more devel issues.

* Deprecate `hasThreadSupport` for ioselectors_kqueue.
Fix verifySelectParams issue.
Add exit codes test for multiple processes.
Fix osnet PtrToCstringConv warning.

* ioselectors_kqueue refactoring.

* Initial commit.

* Fix 1.2-1.4 compilation issue.

* Fix unused warning for testCtrlC() test.

* Post-rebase fixes.

* Restore master files.

* More fixes.

* Remove duplicates.

* Fix style mistake.

* Add more flexible pragmas.
2023-06-02 01:53:20 +03:00
2023-02-16 21:27:31 +01:00
2023-06-01 17:02:33 +03:00
2019-08-24 02:29:28 +02:00
2023-05-23 12:39:35 +02:00
2023-03-31 07:35:04 +02:00

Chronos - An efficient library for asynchronous programming

Github action License: Apache License: MIT Stability: experimental

Introduction

Chronos is an efficient async/await framework for Nim. Features include:

  • Efficient dispatch pipeline for asynchronous execution
  • HTTP server with SSL/TLS support out of the box (no OpenSSL needed)
  • Cancellation support
  • Synchronization primitivies like queues, events and locks
  • FIFO processing order of dispatch queue
  • Minimal exception effect support (see exception effects)

Installation

You can use Nim's official package manager Nimble to install Chronos:

nimble install chronos

or add a dependency to your .nimble file:

requires "chronos"

Projects using chronos

  • libp2p - Peer-to-Peer networking stack implemented in many languages
  • presto - REST API framework
  • Scorper - Web framework
  • 2DeFi - Decentralised file system
  • websock - WebSocket library with lots of features

chronos is available in the Nim Playground

Submit a PR to add yours!

Documentation

Concepts

Chronos implements the async/await paradigm in a self-contained library, using macros, with no specific helpers from the compiler.

Our 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(). This 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:

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:

proc p(): Future[int] {.async.} =
  await sleepAsync(100.milliseconds)
  return 1

echo waitFor p() # prints "1"

waitFor() is defined like this:

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.

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.

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 are caught by hidden try blocks and placed in the Future.error field, changing the future's status to Failed.

When a future is awaited, that exception is re-raised, only to be caught again by a hidden try block in the calling async procedure. That's how these exceptions move up the async chain.

A failed future's callbacks will still be scheduled, but it's not possible to resume execution from the point an exception was raised.

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:

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

Chronos does not allow that future continuations and other callbacks raise CatchableError - as such, calls to poll will never raise exceptions caused originating from tasks on the dispatcher queue. It is however possible that Defect that happen in tasks bubble up through poll as these are not caught by the transformation.

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.

Exception effects

chronos currently offers minimal support for exception effects and raises annotations. In general, during the async transformation, a generic except CatchableError handler is added around the entire function being transformed, in order to catch any exceptions and transfer them to the Future. Because of this, the effect system thinks no exceptions are "leaking" because in fact, exception handling is deferred to when the future is being read.

Effectively, this means that while code can be compiled with {.push raises: [Defect]}, the intended effect propagation and checking is disabled for async functions.

To enable checking exception effects in async code, enable strict mode with -d:chronosStrictException.

In the strict mode, async functions are checked such that they only raise CatchableError and thus must make sure to explicitly specify exception effects on forward declarations, callbacks and methods using {.raises: [CatchableError].} (or more strict) annotations.

Cancellation support

Any running Future can be cancelled. This can be used to launch multiple futures, and wait for one of them to finish, and cancel the rest of them, to add timeout, or to let the user cancel a running task.

# Simple cancellation
let future = sleepAsync(10.minutes)
future.cancel()

# Wait for cancellation
let future2 = sleepAsync(10.minutes)
await future2.cancelAndWait()

# Race between futures
proc retrievePage(uri: string): Future[string] {.async.} =
  # requires to import uri, chronos/apps/http/httpclient, stew/byteutils
  let httpSession = HttpSessionRef.new()
  try:
    resp = await httpSession.fetch(parseUri(uri))
    result = string.fromBytes(resp.data)
  finally:
    # be sure to always close the session
    await 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.cancel()
echo "Result: ", await finishedFut

When an await is cancelled, it will raise a CancelledError:

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

Other resources

License

Licensed and distributed under either of

or

at your option. These files may not be copied, modified, or distributed except according to those terms.

Description
Chronos - An efficient library for asynchronous programming
https://status-im.github.io/nim-chronos/
Readme
Languages
Nim 99.7%
C 0.2%