Search code examples
denooak

Does Deno's oak wait for all in-flight requests to drain before returning its listen promise?


I'm setting up a new web project using Deno and oak.

I've passed an AbortSignal into the listen call and I'm listening for a SIGTERM from the OS and calling abort, in case this is not built-in behaviour.

Similar to setups described here: Deno and Docker how to listen for the SIGTERM signal and close the server

Question: Upon abort, will the await listen(...) call return immediately or after all remaining requests have completed?

If not then I guess I will need to accurately count concurrent requests using Atomics and wait until that counter drops to zero before ending the process.


Solution

  • Rather than rely on second hand information from someone else (which might not be correct), why not just do a test and find out for yourself (or review the source code)?

    Here's a reproducible example which indicates that — when using [email protected] with [email protected] — the server gracefully shuts down: it still responds to a pending request even after the AbortSignal is aborted:

    so-74600368.ts:

    import {
      Application,
      type Context,
    } from "https://deno.land/x/[email protected]/mod.ts";
    
    import { delay } from "https://deno.land/[email protected]/async/delay.ts";
    
    async function sendRequestAndLogResponseText(): Promise<void> {
      try {
        const response = await fetch("http://localhost:8000/");
    
        if (!response.ok) {
          throw new Error(`Response not OK (Status code: ${response.status})`);
        }
    
        const text = await response.text();
        console.log(performance.now(), text);
      } catch (ex) {
        console.error(ex);
      }
    }
    
    async function sendSquentialRequsets(numOfRequests: number): Promise<void> {
      for (let i = 0; i < numOfRequests; i += 1) {
        await sendRequestAndLogResponseText();
      }
    }
    
    function printStartupMessage({ hostname, port, secure }: {
      hostname: string;
      port: number;
      secure?: boolean;
    }): void {
      if (!hostname || hostname === "0.0.0.0") hostname = "localhost";
      const address =
        new URL(`http${secure ? "s" : ""}://${hostname}:${port}/`).href;
      console.log(`Listening at ${address}`);
      console.log("Use ctrl+c to stop");
    }
    
    async function main() {
      const log = new Map<Context, boolean>();
      const controller = new AbortController();
    
      controller.signal.addEventListener("abort", () => {
        console.log(performance.now(), "Abort method invoked");
      });
    
      const app = new Application();
    
      app.use(async (ctx) => {
        log.set(ctx, false);
    
        if (log.size > 2) {
          console.log(performance.now(), "Aborting");
          controller.abort(new Error("Received third request. Aborting now."));
        }
    
        // A bit of artificial delay, to ensure that no unaccounted for latency
        // might cause a non-deterministic/unexpected result:
        await delay(300);
    
        ctx.response.body = `Response OK: (#${log.size})`;
        log.set(ctx, true);
      });
    
      app.addEventListener("listen", (ev) => {
        console.log(performance.now(), "Server starting");
        printStartupMessage(ev);
      });
    
      const listenerPromise = app.listen({
        hostname: "localhost",
        port: 8000,
        signal: controller.signal,
      })
        .then(() => {
          console.log(performance.now(), "Server stopped");
          return { type: "server", ok: true };
        })
        .catch((reason) => ({ type: "server", ok: false, reason }));
    
      const requestsPromise = sendSquentialRequsets(3)
        .then(() => {
          console.log(performance.now(), "All responses OK");
          return { type: "requests", ok: true };
        })
        .catch((reason) => ({ type: "requests", ok: false, reason }));
    
      const results = await Promise.allSettled([listenerPromise, requestsPromise]);
    
      for (const result of results) console.log(result);
    
      const allResponsesSent = [...log.values()].every(Boolean);
      console.log({ allResponsesSent });
    }
    
    if (import.meta.main) main();
    
    
    % deno --version
    deno 1.28.2 (release, x86_64-apple-darwin)
    v8 10.9.194.1
    typescript 4.8.3
    
    % deno run --allow-net=localhost so-74600368.ts
    62 Server starting
    Listening at http://127.0.0.1:8000/
    Use ctrl+c to stop
    378 Response OK: (#1)
    682 Response OK: (#2)
    682 Aborting
    682 Abort method invoked
    990 Server stopped
    992 Response OK: (#3)
    992 All responses OK
    { status: "fulfilled", value: { type: "server", ok: true } }
    { status: "fulfilled", value: { type: "requests", ok: true } }
    { allResponsesSent: true }