• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Flags: --expose-gc --expose-internals
2'use strict';
3const common = require('../common');
4const http = require('http');
5const async_hooks = require('async_hooks');
6const makeDuplexPair = require('../common/duplexpair');
7
8// Regression test for https://github.com/nodejs/node/issues/30122
9// When a domain is attached to an http Agent’s ReusedHandle object, that
10// domain should be kept alive through the ReusedHandle and that in turn
11// through the actual underlying handle.
12
13// Consistency check: There is a ReusedHandle being used, and it emits events.
14// We also use this async hook to manually trigger GC just before the domain’s
15// own `before` hook runs, in order to reproduce the bug above (the ReusedHandle
16// being collected and the domain with it while the handle is still alive).
17const checkInitCalled = common.mustCall();
18const checkBeforeCalled = common.mustCallAtLeast();
19let reusedHandleId;
20async_hooks.createHook({
21  init(id, type, triggerId, resource) {
22    if (resource.constructor.name === 'ReusedHandle') {
23      reusedHandleId = id;
24      checkInitCalled();
25    }
26  },
27  before(id) {
28    if (id === reusedHandleId) {
29      global.gc();
30      checkBeforeCalled();
31    }
32  }
33}).enable();
34
35// We use a DuplexPair rather than TLS sockets to keep the domain from being
36// attached to too many objects that use strong references (timers, the network
37// socket handle, etc.) and wrap the client side in a JSStreamSocket so we don’t
38// have to implement the whole _handle API ourselves.
39const { serverSide, clientSide } = makeDuplexPair();
40const JSStreamSocket = require('internal/js_stream_socket');
41const wrappedClientSide = new JSStreamSocket(clientSide);
42
43// Consistency check: We use asyncReset exactly once.
44wrappedClientSide._handle.asyncReset =
45  common.mustCall(wrappedClientSide._handle.asyncReset);
46
47// Dummy server implementation, could be any server for this test...
48const server = http.createServer(common.mustCall((req, res) => {
49  res.writeHead(200, {
50    'Content-Type': 'text/plain'
51  });
52  res.end('Hello, world!');
53}, 2));
54server.emit('connection', serverSide);
55
56// HTTP Agent that only returns the fake connection.
57class TestAgent extends http.Agent {
58  createConnection = common.mustCall(() => wrappedClientSide)
59}
60const agent = new TestAgent({ keepAlive: true, maxSockets: 1 });
61
62function makeRequest(cb) {
63  const req = http.request({ agent }, common.mustCall((res) => {
64    res.resume();
65    res.on('end', cb);
66  }));
67  req.end('');
68}
69
70// The actual test starts here:
71
72const domain = require('domain');
73// Create the domain in question and a dummy “noDomain” domain that we use to
74// avoid attaching new async resources to the original domain.
75const d = domain.create();
76const noDomain = domain.create();
77
78d.run(common.mustCall(() => {
79  // Create a first request only so that we can get a “re-used” socket later.
80  makeRequest(common.mustCall(() => {
81    // Schedule the second request.
82    setImmediate(common.mustCall(() => {
83      makeRequest(common.mustCall(() => {
84        // The `setImmediate()` is run inside of `noDomain` so that it doesn’t
85        // keep the actual target domain alive unnecessarily.
86        noDomain.run(common.mustCall(() => {
87          setImmediate(common.mustCall(() => {
88            // This emits an async event on the reused socket, so it should
89            // run the domain’s `before` hooks.
90            // This should *not* throw an error because the domain was garbage
91            // collected too early.
92            serverSide.end();
93          }));
94        }));
95      }));
96    }));
97  }));
98}));
99