Chronos

repository·master·Indexed 19 days ago

https://github.com/status-im/nim-chronos

An efficient asynchronous programming framework for Nim providing async/await capabilities, socket/process I/O, and an HTTP server with built-in SSL/TLS support. It utilizes a cooperative multitasking model with a dispatcher-based event loop to handle thousands of concurrent requests per thread.

Tokens
16.4K
Snippets
42
Records
78
Agent score
62%

What's inside nim-chronos

  1. Overview of Chronos features

    master

    Chronos is an asynchronous programming library for Nim that implements the async/await paradigm using macro and closure iterator transformations. Key features include:

    • Asynchronous I/O: Support for sockets and process I/O.
    • HTTP Support: Built-in HTTP client and server with SSL/TLS support (no external OpenSSL dependency required).
    • Synchronization: Primitives such as queues, events, and locks.
    • Cancellation: Support for cancelling asynchronous operations.
    • Efficient Dispatch: A multi-platform dispatch pipeline.
    • Error Handling: Support for exception effects.
  2. How exceptions work in Chronos

    master

    Exceptions that inherit from CatchableError interrupt the execution of an async procedure. When an exception occurs, the Future status changes to Failed, the exception is stored in the Future.error field, and callbacks are scheduled. When the Future is later awaited or read, the exception is re-raised, propagating up the async execution chain.

    To handle exceptions immediately, wrap the await call in a try...except block.

    poll() is designed to be safe; chronos ensures all exceptions are re-routed to the Future, so poll() itself will not raise exceptions (unless a Defect or undefined behavior occurs in user code).

    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()
      try:
        await fut1
      except CatchableError:
        echo "p1() failed: ", fut1.error.name, ": ", fut1.error.msg
      echo "reachable code here"
      await fut2
  3. Control concurrency with AsyncSemaphore

    master

    To prevent resource exhaustion (like running out of file descriptors or overwhelming a DNS resolver) when performing many asynchronous tasks, use an AsyncSemaphore.

    A semaphore limits the number of concurrent operations by requiring a function to acquire a permit before running and release it when finished. If the semaphore reaches its capacity, subsequent calls to acquire will wait until a permit is released.

    Implementation Pattern

    1. Initialize: Create an AsyncSemaphore with a fixed capacity.
    2. Acquire: Call await semaphore.acquire() at the start of the task.
    3. Release: Call semaphore.release() when the task is complete. It is recommended to use defer to ensure the semaphore is released even if the task fails.
    4. Error Handling: Since release can raise an AsyncSemaphoreError, wrap the release call in a try..except block to prevent errors from bubbling up and crashing the application.
    import chronos, chronos/asyncsync
    
    const maxConcurrency = 5
    let semaphore = newAsyncSemaphore(maxConcurrency)
    
    proc check(uri: string, semaphore: AsyncSemaphore) {.async.} = 
      await semaphore.acquire()
      try:
        # Perform asynchronous work here
        echo "Checking ", uri
      finally:
        try:
          semaphore.release()
        except AsyncSemaphoreError:
          # Handle release error gracefully
          pass
  4. Use the `await` keyword to suspend execution

    master

    The await keyword is used inside async procedures to operate on Future instances. When await is encountered, control is yielded back to the Chronos dispatcher. The procedure resumes once the awaited future completes, fails, or is cancelled. await effectively calls Future.read() to retrieve the encapsulated value upon completion.

    Concurrency Note: Executing multiple async procedures without awaiting them immediately allows them to run concurrently. For example, if you call two async procedures and then await them sequentially, the total elapsed time will be the duration of the longest task, not the sum of both.

    proc p1() {.async.} =
      await sleepAsync(1.seconds)
    
    proc p2() {.async.} =
      await sleepAsync(1.seconds)
    
    proc p3() {.async.} =
      let
        fut1 = p1()
        fut2 = p2()
      # Both futures are now in the dispatcher queue and running concurrently
      await fut1
      await fut2
      # Total time elapsed is ~1 second, not 2
    
    waitFor p3()
  5. Pass state to HTTP handlers using closures

    master

    Because HTTP handlers in Chronos typically follow the HttpProcessCallback2 signature, you cannot directly pass custom state (like a database connection or an in-memory table) as an argument to the handler function itself.

    To provide access to external state, wrap your handler logic in a function that accepts the state as an input parameter and returns the actual handler (a closure). This allows the handler to capture and use the state from its surrounding scope.

  6. Implement TCP servers and clients

    master

    Chronos includes primitives for TCP/IP (v4 and v6) networking:

    • Echo Server: A simple server that echoes received data back to the client.
    • Graceful Shutdown Server: A TCP server designed to handle shutdown procedures cleanly without abruptly dropping connections.
    • Multi-connection Client: A client capable of handling multiple concurrent connections to an echo server.
  7. Managing Future ownership

    master

    Ownership of a Future is shared between the producer (the callee that created it) and the consumer (the caller waiting for it).

    • Producer responsibility: Responsible for completing or failing the Future.
    • Consumer responsibility: Responsible for waiting for completion and potentially calling cancel.

    Critical Rule: Callers must not call complete or fail on futures, and callees/observers must not cancel them. Violating these ownership rules can lead to panics or unexpected shutdowns (e.g., if a future is completed twice).

  8. Perform HTTP requests and server middleware

    master

    Chronos provides high-level HTTP capabilities:

    • HTTP GET: Download web pages using an HTTP client.
    • Concurrent HTTP Requests: Execute multiple HTTP GET requests concurrently to improve performance.
    • HTTP Middleware: Deploy multiple middlewares to an HTTP server to intercept and process requests/responses.
  9. Support multiple async backends

    master

    To allow your library to work with different asynchronous backends, you can use one of two strategies:

    Create separate modules for each backend (e.g., my_lib_chronos.nim and my_lib_asyncdispatch.nim) that can be imported side-by-side. This is the most flexible approach for composition.

    2. Global Compile Flag

    Use a global compile flag to select the backend at compile time. This is easier to implement but can make it difficult to compose applications that rely on transitive dependencies using different backends.

    If using the flag approach, use the name asyncBackend. Users can then select the backend using -d:asyncBackend=<backend_name>.

  10. Use threads for parallel computation and event loop management

    master

    While Chronos uses a cooperative async model optimized for I/O-bound tasks, long-running computations can block the event loop and prevent other tasks from progressing. To handle heavy CPU-bound work without stalling the async loop, you should use multithreading to offload computations to parallel threads. This also allows you to manage multiple event loops in parallel if a single loop becomes overloaded.

    To facilitate communication between threads (e.g., notifying an async procedure of progress or waiting for a signal from another thread), use the ThreadSignalPtr type provided by the chronos/threadsync module.