summaryrefslogtreecommitdiff
path: root/test/parallel/test-http-agent-domain-reused-gc.js
blob: 9470b23b472662e4f6d8c2f4e5f056266bcf413e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
// Flags: --expose-gc --expose-internals
'use strict';
const common = require('../common');
const http = require('http');
const async_hooks = require('async_hooks');
const makeDuplexPair = require('../common/duplexpair');

// Regression test for https://github.com/nodejs/node/issues/30122
// When a domain is attached to an http Agent’s ReusedHandle object, that
// domain should be kept alive through the ReusedHandle and that in turn
// through the actual underlying handle.

// Consistency check: There is a ReusedHandle being used, and it emits events.
// We also use this async hook to manually trigger GC just before the domain’s
// own `before` hook runs, in order to reproduce the bug above (the ReusedHandle
// being collected and the domain with it while the handle is still alive).
const checkInitCalled = common.mustCall();
const checkBeforeCalled = common.mustCallAtLeast();
let reusedHandleId;
async_hooks.createHook({
  init(id, type, triggerId, resource) {
    if (resource.constructor.name === 'ReusedHandle') {
      reusedHandleId = id;
      checkInitCalled();
    }
  },
  before(id) {
    if (id === reusedHandleId) {
      global.gc();
      checkBeforeCalled();
    }
  }
}).enable();

// We use a DuplexPair rather than TLS sockets to keep the domain from being
// attached to too many objects that use strong references (timers, the network
// socket handle, etc.) and wrap the client side in a JSStreamSocket so we don’t
// have to implement the whole _handle API ourselves.
const { serverSide, clientSide } = makeDuplexPair();
const JSStreamSocket = require('internal/js_stream_socket');
const wrappedClientSide = new JSStreamSocket(clientSide);

// Consistency check: We use asyncReset exactly once.
wrappedClientSide._handle.asyncReset =
  common.mustCall(wrappedClientSide._handle.asyncReset);

// Dummy server implementation, could be any server for this test...
const server = http.createServer(common.mustCall((req, res) => {
  res.writeHead(200, {
    'Content-Type': 'text/plain'
  });
  res.end('Hello, world!');
}, 2));
server.emit('connection', serverSide);

// HTTP Agent that only returns the fake connection.
class TestAgent extends http.Agent {
  createConnection = common.mustCall(() => wrappedClientSide)
}
const agent = new TestAgent({ keepAlive: true, maxSockets: 1 });

function makeRequest(cb) {
  const req = http.request({ agent }, common.mustCall((res) => {
    res.resume();
    res.on('end', cb);
  }));
  req.end('');
}

// The actual test starts here:

const domain = require('domain');
// Create the domain in question and a dummy “noDomain” domain that we use to
// avoid attaching new async resources to the original domain.
const d = domain.create();
const noDomain = domain.create();

d.run(common.mustCall(() => {
  // Create a first request only so that we can get a “re-used” socket later.
  makeRequest(common.mustCall(() => {
    // Schedule the second request.
    setImmediate(common.mustCall(() => {
      makeRequest(common.mustCall(() => {
        // The `setImmediate()` is run inside of `noDomain` so that it doesn’t
        // keep the actual target domain alive unnecessarily.
        noDomain.run(common.mustCall(() => {
          setImmediate(common.mustCall(() => {
            // This emits an async event on the reused socket, so it should
            // run the domain’s `before` hooks.
            // This should *not* throw an error because the domain was garbage
            // collected too early.
            serverSide.end();
          }));
        }));
      }));
    }));
  }));
}));