Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Oak leaking async ops on simple server test #454

Open
acrodrig opened this issue Jan 24, 2022 · 2 comments
Open

Oak leaking async ops on simple server test #454

acrodrig opened this issue Jan 24, 2022 · 2 comments

Comments

@acrodrig
Copy link

When trying to use oak to unit test some REST services, the server seems to leak async ops. Simplest code to reproduce below. Would love to know if I am doing something wrong (maybe explicit close connection?) or if this is a bug. Thanks much.

I run the test below using:

deno test -A server.test.ts 

Sample code:

import { assertEquals } from "https://deno.land/std/testing/asserts.ts";
import { Application, Router } from "https://deno.land/x/oak/mod.ts";

const PORT = 8080;

let listenPromise: Promise<void>;
const controller = new AbortController();

// Starts server and saves the returned promise into `listenPromise`
export const startServer = function(port: number): void {
    const app = new Application(), router = new Router();

    // Ping route returns "PONG""
    router.get("/ping", (ctx) => {
        ctx.response.body = "PONG";
        ctx.response.status = 200;
    });

    app.use(router.routes());
    app.use(router.allowedMethods());
    listenPromise = app.listen({ port: port, signal: controller.signal });
};

export const stopServer = async function(): Promise<void> {
    controller.abort();
    await listenPromise;
};

// starts small server defined above
startServer(PORT);

// BUG: THIS TEST CASE IS LEAKING ASYNC OPS, WHY?
Deno.test("test that I can connect to server", async function() {
    const url = "http://localhost:8080/ping";
    const response = await fetch(url);
    const text = await response.text();
    assertEquals(text, "PONG");
});

// Close the server on a test so that it runs at the end
Deno.test("close server", { sanitizeOps: false, sanitizeResources: false }, async function() {
    await stopServer();
});

The console output looks like this:

running 2 tests from file:///Users/andres/Code/RC/gateways/test/bug.test.ts
test test that I can connect to server ... FAILED (7ms)
test close server ... ok (1ms)

failures:

test that I can connect to server
AssertionError: Test case is leaking async ops.
Before:
  - dispatched: 2
  - completed: 1
After:
  - dispatched: 10
  - completed: 8
Ops:
  op_http_accept:
    Before:
      - dispatched: 0
      - completed: 0
    After:
      - dispatched: 2
      - completed: 1

Make sure to await all promises returned from Deno APIs before
finishing test case.
    at assert (deno:runtime/js/06_util.js:41:13)
    at asyncOpSanitizer (deno:runtime/js/40_testing.js:121:7)
    at async resourceSanitizer (deno:runtime/js/40_testing.js:137:7)
    at async Object.exitSanitizer [as fn] (deno:runtime/js/40_testing.js:169:9)
    at async runTest (deno:runtime/js/40_testing.js:427:7)
    at async Object.runTests (deno:runtime/js/40_testing.js:540:22)

failures:

	test that I can connect to server

test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out (29ms)

error: Test failed
@acrodrig
Copy link
Author

acrodrig commented Jan 24, 2022

I should add that if I replace URL http://localhost:8080/ping in the test case for an external URL such as https://filesamples.com/samples/code/json/sample2.json then the test no longer leaks, so presumably the leak is happening at the router.get "ping" function.

@cmorten
Copy link
Contributor

cmorten commented Aug 29, 2022

In this instance got a hunch it is your setup that's leaking based on the error.

  1. You have op_http_accept as 0 dispatched prior to the test. This indicates that although you have called startServer() prior to the test, Oak still hasn't managed to make it's first listener.accept() call yet.
  2. After the test we can see that 2 accept ops have been dispatched but only 1 has been completed. The completed one will be for the very first listener.accept() promise which will have completed when you made the fetch() call to the Oak server. The second accept op which we can see is dispatched but not completed is because under the hood Oak automatically looks to accept another connection but with no further requests (or stopping the server) this promise will never resolve.
  3. The simplest solution is likely to move the start and stop server functionality into the single test.

E.g. the following adjustment to your test file doesn't throw:

import { assertEquals } from "https://deno.land/std/testing/asserts.ts";
import { Application, Router } from "https://deno.land/x/oak/mod.ts";

const PORT = 8080;

let listenPromise: Promise<void>;
const controller = new AbortController();

// Starts server and saves the returned promise into `listenPromise`
export const startServer = function (port: number): void {
  const app = new Application(),
    router = new Router();

  // Ping route returns "PONG""
  router.get("/ping", (ctx) => {
    ctx.response.body = "PONG";
    ctx.response.status = 200;
  });

  app.use(router.routes());
  app.use(router.allowedMethods());
  listenPromise = app.listen({ port: port, signal: controller.signal });
};

export const stopServer = async function (): Promise<void> {
  controller.abort();
  await listenPromise;
};

Deno.test("test that I can connect to server", async function () {
  // starts small server defined above
  startServer(PORT);

  const url = "http://localhost:8080/ping";
  const response = await fetch(url);
  const text = await response.text();
  assertEquals(text, "PONG");

  await stopServer();
});

There may be a way to setup the tests to avoid having the startServer() and stopServer() calls in each of your tests. For this would likely need to make use of the "listen" event so you can await the first accept() call being made reliably.

See https://github.com/oakserver/oak#opening-the-server

E.g. in addition to your listenPromise have it's "listening" counterpart...

const listeningPromise = new Promise<void>((resolve) => {
  app.addEventListener(
    "listen",
    () => {
      resolve();
    },
  );
});

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants