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:
- Use
mapItfromstd/sequtilsto create a list ofFutures for our requests. - Await all
Futures at once withallFutures. - 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:
- 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.
- 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:
- We use
mapItto create a list ofFutures, one for each URI. Each call tosession.check(it)returns aFuture[void]and starts the request in the background. - We use
allFuturesto await all thoseFutures at once. - We add a
try..except CancelledErrorblock aroundallFutures. This is important: ifcheck(uris)itself is cancelled, we want to make sure all the pending requests we started are also cancelled and cleaned up properly. UsingcancelAndWait(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!