Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Making Requests Concurrently

Goal: Learn how to make arbitrarily many HTTP requests asynchronously.

Source code: chapter3/src/uptimemon.nim

In the previous chapter, we learned how to reuse a session to check multiple URIs serially. While efficient, checking URIs one by one is slow. Now, let's unlock the true power of Chronos—concurrency!

We want Chronos to start all the requests at the same time and handle each result as soon as it's available.

To achieve that, we will:

  1. Use mapIt from std/sequtils to create a list of Futures for our requests.
  2. Await all Futures at once with allFutures.
  3. Add cancellation logic to ensure that if the main check is cancelled, all individual requests are also cancelled and awaited.

Here's the code:

import std/sequtils
import chronos/apps/http/httpclient

const uris = @[
  "https://duckduckgo.com/?q=chronos", "https://mock.codes/403"
]

proc check(session: HttpSessionRef, uri: string) {.async: (raises: [CancelledError]).} =
  try:
    let response = await session.fetch(parseUri(uri))

    if response.status == 200:
      echo "[OK] " & uri
    else:
      echo "[NOK] " & uri & ": " & $response.status
  except HttpError:
    echo "[ERR] " & uri & ": " & getCurrentExceptionMsg()

proc check(uris: seq[string]) {.async: (raises: []).} =
  let
    session = HttpSessionRef.new()
    futures = uris.mapIt(session.check(it))

  try:
    await allFutures(futures)
  except CancelledError:
    await cancelAndWait(futures)
  finally:
    await session.closeWait()

when isMainModule:
  waitFor check(uris)

Run this code with nimble run. You should see something like this (the order of messages may be different):

[NOK] https://mock.codes/403: 403
[OK] https://duckduckgo.com/?q=chronos

Notice that:

  1. The order of responses is different from the order of the URIs in the source code. That's because our requests are now asynchronous and complete at different times.
  2. The execution time has improved. Now, the program runs roughly as long as its longest request, not the sum of all requests.

Let's examine the changes since the previous version.

proc check(uris: seq[string]) {.async: (raises: []).} =
  let
    session = HttpSessionRef.new()
    futures = uris.mapIt(session.check(it))

  try:
    await allFutures(futures)
  except CancelledError:
    await cancelAndWait(futures)
  finally:
    await session.closeWait()

In our check function for multiple URIs, we've replaced the loop with concurrent execution:

  1. We use mapIt to create a list of Futures, one for each URI. Each call to session.check(it) returns a Future[void] and starts the request in the background.
  2. We use allFutures to await all those Futures at once.
  3. We add a try..except CancelledError block around allFutures. This is important: if check(uris) itself is cancelled, we want to make sure all the pending requests we started are also cancelled and cleaned up properly. Using cancelAndWait(futures) ensures that all resources are freed immediately.

Note that since we handle the cancellation internally and don't re-raise the exception, the function signature is now raises: []. In async procedures, if you handle all potential exceptions, including CancelledError, the compiler sees it as not raising anything.

In the next chapter, we'll see how to prevent slow requests from freezing our application using timeouts!