newrelic
Version:
New Relic agent
182 lines (159 loc) • 5.5 kB
JavaScript
/*
* Copyright 2022 New Relic Corporation. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/
const symbols = require('../../symbols')
function Context(segment, transaction) {
this.segments = [segment]
this.transaction = transaction
}
Context.prototype = Object.create(null)
Context.prototype.branch = function branch() {
return this.segments.push(null) - 1
}
function Contextualizer(idx, context) {
this.parentIdx = -1
this.idx = idx
this.context = context
this.child = null
}
module.exports = Contextualizer
/**
* Manually bind the async context to every
* child within the promise chain
*
* @param {object} ctxlzr current promise
* @param {object} next the next link in promise chain
* @returns {void}
*/
function bindChild(ctxlzr, next) {
// If prev has one child already, branch the context and update the child.
if (ctxlzr.child) {
// When the branch-point is the 2nd through nth link in the chain, it is
// necessary to track its segment separately so the branches can parent
// their segments on the branch-point.
if (ctxlzr.parentIdx !== -1) {
ctxlzr.idx = ctxlzr.context.branch()
}
// The first child needs to be updated to have its own branch as well. And
// each of that child's children must be updated with the new parent index.
// This is the only non-constant-time action for linking, but it only
// happens with branching promise chains specifically when the 2nd branch
// is added.
//
// Note: This does not account for branches of branches. That may result
// in improperly parented segments.
let parent = ctxlzr
let child = ctxlzr.child
const branchIdx = ctxlzr.context.branch()
do {
child.parentIdx = parent.idx
child.idx = branchIdx
parent = child
child = child.child
} while (child)
// We set the child to something falsey that isn't `null` so we can
// distinguish between having no child, having one child, and having
// multiple children.
ctxlzr.child = false
}
// If this is a branching link then create a new branch for the next promise.
// Otherwise, we can just piggy-back on the previous link's spot.
const idx = ctxlzr.child === false ? ctxlzr.context.branch() : ctxlzr.idx
// Create a new context for this next promise.
next[symbols.context] = new Contextualizer(idx, ctxlzr.context)
next[symbols.context].parentIdx = ctxlzr.idx
// If this was our first child, remember it in case we have a 2nd.
if (ctxlzr.child === null) {
ctxlzr.child = next[symbols.context]
}
}
/**
* Binds segment to the entire promise chain
*
* @param {Function} prev previous function in chain
* @param {Function} next next function in chain
* @param {TraceSegment} segment proper segment to bind
* @param {Transaction} transaction proper transaction to bind
* @returns {void}
*/
Contextualizer.link = function link(prev, next, segment, transaction) {
let ctxlzr = prev && prev[symbols.context]
if (ctxlzr && !ctxlzr.isActive()) {
ctxlzr = prev[symbols.context] = null
}
if (ctxlzr) {
bindChild(ctxlzr, next)
} else if (segment) {
// This next promise is the root of a chain. Either there was no previous
// promise or the promise was created out of context.
next[symbols.context] = new Contextualizer(0, new Context(segment, transaction))
}
}
Contextualizer.prototype = Object.create(null)
/**
* Checks if segment is currently active
*
* @returns {boolean} if segment is active
*/
Contextualizer.prototype.isActive = function isActive() {
const segments = this.context.segments
const segment = segments[this.idx] || segments[this.parentIdx] || segments[0]
const transaction = this.getTransaction()
return segment && transaction?.isActive()
}
/**
* Gets the segment at the appropriate index.
* If there is none it will get the segment at the parent index or the first one
* then assign to current index.
*
* @returns {TraceSegment} segment by idx or parentIdx or the first one
*/
Contextualizer.prototype.getSegment = function getSegment() {
const segments = this.context.segments
let segment = segments[this.idx]
if (segment == null) {
segment = segments[this.idx] = segments[this.parentIdx] || segments[0]
}
return segment
}
/**
* Gets the transaction from the context.
*
* @returns {Transaction} transaction from context
*/
Contextualizer.prototype.getTransaction = function getTransaction() {
return this.context.transaction
}
/**
* Sets the set to the appropriate index
*
* @param {TraceSegment} segment segment to assign to index
* @returns {TraceSegment} same segment passed in
*/
Contextualizer.prototype.setSegment = function setSegment(segment) {
this.context.segments[this.idx] = segment
return this.context.segments[this.idx]
}
/**
* Propagates the segment to the finally function
*
* @param {Function} prom current promise
* @returns {Function} current promise or wrapped finally that will propagate the segment
*/
Contextualizer.prototype.continue = function continueContext(prom) {
const self = this
const nextContext = prom[symbols.context]
if (!nextContext) {
return prom
}
// If we have `finally`, use that to sneak our context update.
if (typeof prom.finally === 'function') {
return prom.finally(__NR_continueContext)
}
// eslint-disable-next-line camelcase
function __NR_continueContext() {
self.setSegment(nextContext.getSegment())
}
}