| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent f66f28e commit 6525142
2 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -40,10 +40,18 @@ const { async_id_symbol } = require('internal/async_hooks').symbols; | |||
| 40 | 40 | // ClientRequest.onSocket(). The Agent is now *strictly* | |
| 41 | 41 | // concerned with managing a connection pool. | |
| 42 | 42 | ||
| 43 | + const kReusedHandle = Symbol('kReusedHandle'); | ||
| 43 | 44 | class ReusedHandle { | |
| 44 | 45 | constructor(type, handle) { | |
| 45 | 46 | this.type = type; | |
| 46 | 47 | this.handle = handle; | |
| 48 | + // We need keep the resource object alive from this object, because | ||
| 49 | + // domains rely on GC of the resource object for lifetime tracking. | ||
| 50 | + // TODO(addaleax): This should really apply to all uses of | ||
| 51 | + // AsyncWrap::AsyncReset() when the resource is not the AsyncWrap object | ||
| 52 | + // itself. However, HTTPClientAsyncResource and HTTPServerAsyncResource | ||
| 53 | + // hold on to other objects, inhibiting GC. | ||
| 54 | + handle[kReusedHandle] = this; | ||
| 47 | 55 | } | |
| 48 | 56 | } | |
| 49 | 57 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,98 @@ | |||
| 1 | + // Flags: --expose-gc --expose-internals | ||
| 2 | + 'use strict'; | ||
| 3 | + const common = require('../common'); | ||
| 4 | + const http = require('http'); | ||
| 5 | + const async_hooks = require('async_hooks'); | ||
| 6 | + const 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). | ||
| 17 | + const checkInitCalled = common.mustCall(); | ||
| 18 | + const checkBeforeCalled = common.mustCallAtLeast(); | ||
| 19 | + let reusedHandleId; | ||
| 20 | + async_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. | ||
| 39 | + const { serverSide, clientSide } = makeDuplexPair(); | ||
| 40 | + const JSStreamSocket = require('internal/js_stream_socket'); | ||
| 41 | + const wrappedClientSide = new JSStreamSocket(clientSide); | ||
| 42 | + | ||
| 43 | + // Consistency check: We use asyncReset exactly once. | ||
| 44 | + wrappedClientSide._handle.asyncReset = | ||
| 45 | + common.mustCall(wrappedClientSide._handle.asyncReset); | ||
| 46 | + | ||
| 47 | + // Dummy server implementation, could be any server for this test... | ||
| 48 | + const server = http.createServer(common.mustCall((req, res) => { | ||
| 49 | + res.writeHead(200, { | ||
| 50 | + 'Content-Type': 'text/plain' | ||
| 51 | + }); | ||
| 52 | + res.end('Hello, world!'); | ||
| 53 | + }, 2)); | ||
| 54 | + server.emit('connection', serverSide); | ||
| 55 | + | ||
| 56 | + // HTTP Agent that only returns the fake connection. | ||
| 57 | + class TestAgent extends http.Agent { | ||
| 58 | + createConnection = common.mustCall(() => wrappedClientSide) | ||
| 59 | + } | ||
| 60 | + const agent = new TestAgent({ keepAlive: true, maxSockets: 1 }); | ||
| 61 | + | ||
| 62 | + function 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 | + | ||
| 72 | + const 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. | ||
| 75 | + const d = domain.create(); | ||
| 76 | + const noDomain = domain.create(); | ||
| 77 | + | ||
| 78 | + d.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 | + })); | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments