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

Wait for closed resources to actually close before detecting open handles #11429

Merged

Conversation

Mr0grog
Copy link
Contributor

@Mr0grog Mr0grog commented May 20, 2021

Summary

Note: this contains two major changes (waiting before detecting open handles, detecting indirectly opened handles), but they affect the same part of the code and one revealed the other as an issue, so it seemed simpler to combine them. I’ve made separate commits for each so it’s a little easier to see here, but am also happy to break them into separate PRs.

Some types of async resources in Node.js are not destroyed until after their close or end or similar callbacks/events run, leading to a situation where the --detectOpenHandles option can falsely flag resources that have been cleaned up in user code and are already scheduled for destruction. For example, if a test ends from the callback to TCPServer.close() and no other tests or lifecycle functions impose additional delays, --detectOpenHandles will flag the server as open even though it has been closed. This is also the main cause of issues people encounter with Supertest (see #8554).

Example:

const {createServer} = require('http');

test('--detectOpenHandles flags this even though the server was properly closed', done => {
  const server = createServer((_, response) => response.end('ok'));
  server.listen(0, () => {
    // Close server and return immediately on callback. During the "close"
    // callback, async hooks usually have not yet been called, leading to Jest
    // flagging the server as an open handle.
    server.close(done);
  });
});

This addresses the issue by adding a short delay before collecting open handles. It’s similar to some work @SimenB implemented (and then unimplemented) here: 74ed376


Unfortunately, this got a little bit more complex because the delay caused a test to start failing, which revealed a new issue: It turns out that some arguments to TCPServer.listen() cause the TCPSERVERWRAP async resource for the server to be created indirectly, where its stack trace has no reference to the user code that was ultimately responsible for it, which causes Jest not to track the resource.

I’ve added tracking for those indirectly created resources here. When we track an indirectly created resource, we use the stack trace from the original call that triggered the indirect created it so the user can still see the call in their code that’s causing the problem.

For example, code like:

import {createServer} from 'http';
(function userCode() {
  server.listen(0, () => {
    console.log('ok');
  });
})();

Leads to async resource creation/destruction like:

init ( asyncId: 2, type: "TCPSERVERWRAP", triggerAsyncId: 1 )
  Stack: Error: TCPSERVERWRAP
    at TCP.emitInitNative (node:internal/async_hooks:204:43)
    at createServerHandle (node:net:1230:14)
    at Server.setupListenHandle [as _listen2] (node:net:1273:14)
    at listenInCluster (node:net:1354:12)
    at Server.listen (node:net:1441:7)
    at userCode (/Users/rbrackett/Dev/async-hooks-test/index.js:52:10)

But

(function userCode () {
  const server = http.createServer((_request, response) => response.end('ok'));
  server.listen({host: 'localhost', port: 0}, () => {
    console.log('ok');
  });
})();

Leads to async resource creation/destruction like:

init ( asyncId: 2, type: "DNSCHANNEL", triggerAsyncId: 1 )
  Stack: Error: DNSCHANNEL
    at ChannelWrap.emitInitNative (node:internal/async_hooks:204:43)
    at new Resolver (node:internal/dns/utils:48:20)
    at node:internal/dns/utils:133:23
    at NativeModule.compileForInternalLoader (node:internal/bootstrap/loaders:312:7)
    at nativeModuleRequire (node:internal/bootstrap/loaders:341:14)
    at node:dns:44:5
    at NativeModule.compileForInternalLoader (node:internal/bootstrap/loaders:312:7)
    at nativeModuleRequire (node:internal/bootstrap/loaders:341:14)
    at lookupAndListen (node:net:1486:32)
    at Server.listen (node:net:1437:7)
    at userCode (/Users/rbrackett/Dev/async-hooks-test/index.js:64:10)
init ( asyncId: 3, type: "GETADDRINFOREQWRAP", triggerAsyncId: 1 )
  Stack: Error: GETADDRINFOREQWRAP
    at GetAddrInfoReqWrap.emitInitNative (node:internal/async_hooks:204:43)
    at Object.lookup (node:dns:153:21)
    at lookupAndListen (node:net:1487:7)
    at Server.listen (node:net:1437:7)
    at userCode (/Users/rbrackett/Dev/async-hooks-test/index.js:64:10)
init ( asyncId: 4, type: "TCPSERVERWRAP", triggerAsyncId: 3 )
  Stack: Error: TCPSERVERWRAP
    at TCP.emitInitNative (node:internal/async_hooks:204:43)
    at createServerHandle (node:net:1230:14)
    at Server.setupListenHandle [as _listen2] (node:net:1286:14)
    at listenInCluster (node:net:1354:12)
    at GetAddrInfoReqWrap.doListen [as callback] (node:net:1492:7)
    at GetAddrInfoReqWrap.onlookup [as oncomplete] (node:dns:71:8)
destroy ( asyncId: 3 )

Note that the TCPSERVERWRAP stack does not have userCode in it, and that the GETADDRINFOREQWRAP resource (which does have userCode in the stack) was destroyed.

The changes here allow us to detect that TCPSERVERWRAP resource and associate it with the same line of user code as the GETADDRINFOREQWRAP resource.

I also increased the size of the stack trace that we check for entrypoints to user code. The V8 default is 10, which caused us not to track resources like the DNSCHANNEL above because it has more than 10 stack frames within node internals, causing the wrapper functions that mark this as user code to get cut off the end of the stack trace.

Fixes #8554
Closes #9532

Test plan

This PR includes several unit and end-to-end tests that should cover the issues this is meant to solve.

Mr0grog added 2 commits May 20, 2021 08:54
Some types of async resources in Node.js are not destroyed until *after* their `close` or `end` or similar callbacks and events run, leading to a situation where the `--detectOpenHandles` option can falsely flag resources that have been cleaned up in user code and are already scheduled for destruction. For example, if a test ends from the callback to `TCPServer.close()` and no other tests or lifecycle functions impose additional delays, `--detectOpenHandles` will flag the server even though it has been closed. This is the main cause of issues people encounter with Supertest (see jestjs#8554).

This addresses the issue by adding a short delay before collecting open handles.

Depends on jestjs#11382.
This fixes a test that the previous commit broke. Some commands (for example, starting a TCP/HTTP server with the `host` option) can cause other async resources to be created indire
ctly (so their stack traces don't have any user code in them). Since these are still triggered by things in a user's code, we now track and show them when `--detectOpenHandles` is used.

This also increases the size of stack traces in `ErrorWithStack` because some of the Node internals in these async stack traces are deep enough that the test wrapper functions we lo
ok for fall off the bottom of the stack trace!
@codecov-commenter
Copy link

codecov-commenter commented May 20, 2021

Codecov Report

Merging #11429 (afea019) into master (27bee72) will increase coverage by 0.02%.
The diff coverage is 88.88%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master   #11429      +/-   ##
==========================================
+ Coverage   69.26%   69.29%   +0.02%     
==========================================
  Files         311      311              
  Lines       16273    16285      +12     
  Branches     4702     4707       +5     
==========================================
+ Hits        11272    11285      +13     
+ Misses       4973     4972       -1     
  Partials       28       28              
Impacted Files Coverage Δ
packages/jest-core/src/runJest.ts 68.42% <66.66%> (ø)
packages/jest-util/src/ErrorWithStack.ts 87.50% <75.00%> (-12.50%) ⬇️
packages/jest-core/src/collectHandles.ts 69.69% <100.00%> (+7.62%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 27bee72...afea019. Read the comment docs.

@Mr0grog
Copy link
Contributor Author

Mr0grog commented May 20, 2021

This gist might be useful for playing with or exploring the indirectly created async resources: https://gist.github.com/Mr0grog/f6f3d97a234087ebf247b8faf73822b1

Copy link
Member

@SimenB SimenB left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh, this is super awesome! 👍 I've tried a few approaches in the same vein as this (#9532, #11278 (see first commit)) without much success. This approach is way better, tho! 👏 Do you think you can include any of the tests from those (and thus close the linked issues)?

packages/jest-core/src/collectHandles.ts Outdated Show resolved Hide resolved
packages/jest-core/src/collectHandles.ts Outdated Show resolved Hide resolved
@SimenB SimenB added this to the Jest 27 milestone May 20, 2021
@Mr0grog
Copy link
Contributor Author

Mr0grog commented May 20, 2021

I don’t think I have time for this today, but will make the changes you suggested and try to add the relevant tests from #9532 and #11278 sometime in the next few days. 🙌

@SimenB
Copy link
Member

SimenB commented May 20, 2021

Awesome @Mr0grog, thanks! I can probably try myself if you will be busy for the next couple of days 🙂

@jeysal jeysal modified the milestones: Jest 27, High priority future May 20, 2021
@SimenB
Copy link
Member

SimenB commented May 20, 2021

The test added here is better than the stuff I was doing in #9532. I pulled in the test from #11278.

This is awesome 😀

EDIT: Hah, I ran the test on node 14 😅 OK, pulling that one out

@SimenB SimenB modified the milestones: High priority future, Jest 27 May 20, 2021
@SimenB SimenB merged commit 3b253f8 into jestjs:master May 20, 2021
@Mr0grog
Copy link
Contributor Author

Mr0grog commented May 20, 2021

Whoa, you cleaned this up and merged it in quick! Thanks. 😄 🙌

@Mr0grog Mr0grog deleted the wait-for-closed-resources-to-actually-close branch May 20, 2021 22:44
@SimenB
Copy link
Member

SimenB commented May 21, 2021

Thanks for the awesome PRs for this feature @Mr0grog! It's a really cool one that has had a couple of blemishes for a long time you've smoothed out 👍

This PR is included in 27.0.0-next.11 (stable coming next week)

@Mr0grog
Copy link
Contributor Author

Mr0grog commented May 21, 2021

Woohoo! Glad I could help improve things. 😄

@github-actions
Copy link

This pull request has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.
Please note this issue tracker is not a help forum. We recommend using StackOverflow or our discord channel for questions.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Jun 21, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Open handle when using http.Server.listen
5 participants