newrelic 7.2.1 → 7.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/NEWS.md CHANGED
@@ -1,3 +1,15 @@
1
+ ### v7.3.0 (2021-04-06)
2
+
3
+ * Added new feature-flag 'new_promise_tracking' which enables cleaning up of segment references on native promise resolve instead of destroy. Includes usage of async-await. This can be enabled via `feature_flag: { new_promise_tracking: true }` in the config file or `NEW_RELIC_FEATURE_FLAG_NEW_PROMISE_TRACKING=1` in your ENV vars.
4
+
5
+ Applications with heavy promise usage or high-throughput applications with some promise usage should see moderate to high reduction in memory usage and may see a slight reduction in CPU usage. A bump in throughput may also be noticed in some cases. Results will vary by application.
6
+
7
+ If you are experiencing high overhead levels with your promise usage and the agent attached, we recommend testing your application with 'new_promise_tracking' set to true to see if overhead is reduced. You'll also want to verify your data is still being captured correctly in case it falls into a known or unknown limitation of this approach. **NOTE: chaining of promise continuations onto an already resolved promise across an async hop (scheduled timer) will result in state-loss with this new functionality turned on. This is a less-common use-case but worth considering with your applications.**
8
+
9
+ * Fixed memory leak introduced when Infinite Tracing is enabled.
10
+
11
+ When Infinite Tracing endpoints reconnected they would instantiate a new gRPC client prior to calling `client.recordSpan()`. It appears several objects created by grpc-js (`ChannelImplementation` and child objects, promises, etc.) are held in memory indefinitely due to scheduled timers even when the client is no-longer referenced and the associated stream closed. We now avoid this situation by only creating the client once and then reusing it to establish new stream connections.
12
+
1
13
  ### v7.2.1 (2021-03-29)
2
14
 
3
15
  * Dev-only sub-dependency bump of 'y18n' to clear npm audit warnings.
@@ -15,7 +15,8 @@ exports.prerelease = {
15
15
  fastify_instrumentation: false,
16
16
  // Starts by defaulting true as introducing for deprecation/removal purposes.
17
17
  // Will eventually set false prior full removal of feature.
18
- certificate_bundle: true
18
+ certificate_bundle: true,
19
+ new_promise_tracking: false
19
20
  }
20
21
 
21
22
  // flags that are no longer used for released features
@@ -14,14 +14,14 @@ const util = require('util')
14
14
 
15
15
  const connectionStates = require('./connection/states')
16
16
 
17
- const protoOptions = {keepCase: true,
17
+ const PROTO_OPTIONS = {keepCase: true,
18
18
  longs: String,
19
19
  enums: String,
20
20
  defaults: true,
21
21
  oneofs: true
22
22
  }
23
23
 
24
- const pathProtoDefinition = __dirname +
24
+ const PROTO_DEFINITION_PATH = __dirname +
25
25
  '../../../lib/grpc/endpoints/infinite-tracing/v1.proto'
26
26
 
27
27
  const DEFAULT_RECONNECT_DELAY_MS = 15 * 1000
@@ -54,11 +54,13 @@ class GrpcConnection extends EventEmitter {
54
54
  this._metrics = metrics
55
55
  this._setState(connectionStates.disconnected)
56
56
 
57
- this._endpoint = this.getTraceObserverEndpoint(traceObserverConfig)
58
57
  this._licensekey = null
59
58
  this._runId = null
60
59
  this._requestHeadersMap = null
61
60
 
61
+ this._endpoint = this.getTraceObserverEndpoint(traceObserverConfig)
62
+
63
+ this._client = null
62
64
  this.stream = null
63
65
  }
64
66
 
@@ -320,26 +322,48 @@ class GrpcConnection extends EventEmitter {
320
322
  * a stream for further writing.
321
323
  */
322
324
  _connectSpans() {
323
- const packageDefinition = protoLoader.loadSync(pathProtoDefinition, protoOptions)
324
-
325
- const serviceDefinition = grpc.loadPackageDefinition(
326
- packageDefinition
327
- ).com.newrelic.trace.v1
328
-
329
- const credentials = this._generateCredentials(grpc)
330
- const client = new serviceDefinition.IngestService(
331
- this._endpoint,
332
- credentials
333
- )
325
+ if (!this._client) {
326
+ // Only create once to avoid potential memory leak.
327
+ // We create here (currently) for consistent error handling.
328
+ this._client = this._createClient(this._endpoint)
329
+ }
334
330
 
335
331
  const metadata =
336
332
  this._getMetadata(this._licenseKey, this._runId, this._requestHeadersMap, process.env)
337
333
 
338
- const stream = client.recordSpan(metadata)
334
+ const stream = this._client.recordSpan(metadata)
339
335
  this._setupSpanStreamObservers(stream)
340
336
 
341
337
  return stream
342
338
  }
339
+
340
+ /**
341
+ * Creates gRPC service client to use for establishing gRPC streams.
342
+ *
343
+ * WARNING: creating a client more than once can result in a memory leak.
344
+ * ChannelImplementation and related objects will stay in memory even after
345
+ * the stream is closed and we do not have a handle to the client. Currently
346
+ * impacting grpc-js@1.2.11 and several earlier versions.
347
+ */
348
+ _createClient(endpoint) {
349
+ logger.trace('Creating gRPC client for: ', endpoint)
350
+
351
+ const packageDefinition = protoLoader.loadSync(PROTO_DEFINITION_PATH, PROTO_OPTIONS)
352
+
353
+ const protoDescriptor = grpc.loadPackageDefinition(
354
+ packageDefinition
355
+ )
356
+
357
+ const traceApi = protoDescriptor.com.newrelic.trace.v1
358
+
359
+ const credentials = this._generateCredentials(grpc)
360
+ const client = new traceApi.IngestService(
361
+ endpoint,
362
+ credentials
363
+ )
364
+
365
+ return client
366
+ }
343
367
  }
344
368
 
345
369
  module.exports = GrpcConnection
@@ -40,7 +40,7 @@ function initialize(agent, shim) {
40
40
  }
41
41
 
42
42
  function tryAsyncHooks(agent, shim) {
43
- var asyncHooks = null
43
+ let asyncHooks = null
44
44
  try {
45
45
  asyncHooks = require('async_hooks')
46
46
  } catch (e) {
@@ -53,7 +53,25 @@ function tryAsyncHooks(agent, shim) {
53
53
  const segmentMap = new Map()
54
54
  module.exports.segmentMap = segmentMap
55
55
 
56
- var hook = asyncHooks.createHook({
56
+ let hookHandlers = getStandardHooks(segmentMap, agent, shim)
57
+
58
+ if (agent.config.feature_flag.new_promise_tracking) {
59
+ logger.info('Enabling new promise tracking style via new_promise_tracking feature flag.')
60
+ hookHandlers = getPromiseResolveStyleHooks(segmentMap, agent, shim)
61
+ }
62
+
63
+ const hook = asyncHooks.createHook(hookHandlers)
64
+ hook.enable()
65
+
66
+ agent.on('unload', function disableHook() {
67
+ hook.disable()
68
+ })
69
+
70
+ return true
71
+ }
72
+
73
+ function getStandardHooks(segmentMap, agent, shim) {
74
+ const hooks = {
57
75
  init: function initHook(id, type, triggerId, promiseWrap) {
58
76
  if (type !== 'PROMISE') {
59
77
  return
@@ -78,7 +96,7 @@ function tryAsyncHooks(agent, shim) {
78
96
  },
79
97
 
80
98
  before: function beforeHook(id) {
81
- var hookSegment = segmentMap.get(id)
99
+ const hookSegment = segmentMap.get(id)
82
100
 
83
101
  if (!hookSegment) {
84
102
  return
@@ -88,7 +106,7 @@ function tryAsyncHooks(agent, shim) {
88
106
  shim.setActiveSegment(hookSegment)
89
107
  },
90
108
  after: function afterHook(id) {
91
- var hookSegment = segmentMap.get(id)
109
+ const hookSegment = segmentMap.get(id)
92
110
 
93
111
  // hookSegment is the segment that was active before the promise
94
112
  // executed. If the promise is executing before a segment has been
@@ -104,11 +122,72 @@ function tryAsyncHooks(agent, shim) {
104
122
  destroy: function destHook(id) {
105
123
  segmentMap.delete(id)
106
124
  }
107
- }).enable()
125
+ }
108
126
 
109
- agent.on('unload', function disableHook() {
110
- hook.disable()
111
- })
127
+ return hooks
128
+ }
112
129
 
113
- return true
130
+ function getPromiseResolveStyleHooks(segmentMap, agent, shim) {
131
+ const hooks = {
132
+ init: function initHook(id, type, triggerId, asyncResource) {
133
+ if (type !== 'PROMISE') {
134
+ return
135
+ }
136
+
137
+ let parentSegment = segmentMap.get(triggerId)
138
+
139
+ if (parentSegment && !parentSegment.transaction.isActive()) {
140
+ // Stop propagating if the transaction was ended.
141
+ return
142
+ }
143
+
144
+ if (!parentSegment && !agent.getTransaction()) {
145
+ return
146
+ }
147
+
148
+ const activeSegment = shim.getActiveSegment() || parentSegment
149
+ if (asyncResource && asyncResource.promise) {
150
+ asyncResource.promise.__NR_id = id
151
+ }
152
+
153
+ segmentMap.set(id, activeSegment)
154
+ },
155
+
156
+ before: function beforeHook(id) {
157
+ const hookSegment = segmentMap.get(id)
158
+
159
+ if (!hookSegment) {
160
+ return
161
+ }
162
+
163
+ segmentMap.set(id, shim.getActiveSegment())
164
+ shim.setActiveSegment(hookSegment)
165
+ },
166
+ after: function afterHook(id) {
167
+ const hookSegment = segmentMap.get(id)
168
+
169
+ // hookSegment is the segment that was active before the promise
170
+ // executed. If the promise is executing before a segment has been
171
+ // restored, hookSegment will be null and should be restored. Thus
172
+ // undefined is the only invalid value here.
173
+ if (hookSegment === undefined) {
174
+ return
175
+ }
176
+
177
+ segmentMap.set(id, shim.getActiveSegment())
178
+ shim.setActiveSegment(hookSegment)
179
+ },
180
+ promiseResolve: function promiseResolveHandler(id) {
181
+ const hookSegment = segmentMap.get(id)
182
+ segmentMap.delete(id)
183
+
184
+ if (hookSegment === undefined) {
185
+ return
186
+ }
187
+
188
+ shim.setActiveSegment(hookSegment)
189
+ }
190
+ }
191
+
192
+ return hooks
114
193
  }
@@ -72,6 +72,7 @@ function createStandardAggregator(config, collector, metrics) {
72
72
  }
73
73
 
74
74
  function validateInfiniteTracing(trace_observer) {
75
+ // TODO: Remove semver check when Node 10 support dropped.
75
76
  if (!psemver.satisfies('>=10.10.0')) {
76
77
  logger.warn(
77
78
  'Infinite tracing disabled: this version of Node is not supported (must be >=10.10.0)'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "newrelic",
3
- "version": "7.2.1",
3
+ "version": "7.3.0",
4
4
  "author": "New Relic Node.js agent team <nodejs@newrelic.com>",
5
5
  "license": "Apache-2.0",
6
6
  "contributors": [