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