Skip to content

Commit 14d99ca

Browse files
committed
fix(aerospike): preserve callback parent context
1 parent a1bdbac commit 14d99ca

2 files changed

Lines changed: 158 additions & 5 deletions

File tree

packages/datadog-instrumentations/src/aerospike.js

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const {
88
} = require('./helpers/instrument')
99

1010
const ch = tracingChannel('apm:aerospike:command')
11+
const kTracingCallbackCommand = Symbol('datadog.aerospike.tracing_callback_command')
1112

1213
function wrapCreateCommand (createCommand) {
1314
if (typeof createCommand !== 'function') return createCommand
@@ -17,27 +18,50 @@ function wrapCreateCommand (createCommand) {
1718

1819
if (!CommandClass) return CommandClass
1920

21+
if (typeof CommandClass.prototype.executeWithCallback === 'function') {
22+
shimmer.wrap(CommandClass.prototype, 'executeWithCallback', wrapExecuteWithCallback)
23+
}
2024
shimmer.wrap(CommandClass.prototype, 'process', wrapProcess)
2125

2226
return CommandClass
2327
}
2428
}
2529

30+
function wrapExecuteWithCallback (executeWithCallback) {
31+
return function (...args) {
32+
const cb = args[0]
33+
if (typeof cb !== 'function') return executeWithCallback.apply(this, args)
34+
35+
this[kTracingCallbackCommand] = true
36+
try {
37+
return ch.traceCallback(executeWithCallback, 0, getContext(this), this, ...args)
38+
} finally {
39+
this[kTracingCallbackCommand] = false
40+
}
41+
}
42+
}
43+
2644
function wrapProcess (process) {
2745
return function (...args) {
2846
const cb = args[0]
2947
if (typeof cb !== 'function') return process.apply(this, args)
3048

31-
const ctx = {
32-
commandName: this.constructor.name,
33-
commandArgs: this.args,
34-
clientConfig: this.client.config,
35-
}
49+
if (this[kTracingCallbackCommand]) return process.apply(this, args)
50+
51+
const ctx = getContext(this)
3652

3753
return ch.traceCallback(process, -1, ctx, this, ...args)
3854
}
3955
}
4056

57+
function getContext (command) {
58+
return {
59+
commandName: command.constructor.name,
60+
commandArgs: command.args,
61+
clientConfig: command.client.config,
62+
}
63+
}
64+
4165
addHook({
4266
name: 'aerospike',
4367
file: 'lib/commands/command.js',
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
'use strict'
2+
3+
const assert = require('node:assert/strict')
4+
5+
const dc = require('dc-polyfill')
6+
const { afterEach, beforeEach, describe, it } = require('mocha')
7+
8+
const { storage } = require('../../datadog-core')
9+
10+
require('../src/aerospike')
11+
12+
const HOOK = globalThis[Symbol.for('_ddtrace_instrumentations')].aerospike
13+
.find(entry => entry.file === 'lib/commands/command.js')
14+
.hook
15+
16+
const commandStorage = storage('aerospike-command-test')
17+
const commandChannel = dc.tracingChannel('apm:aerospike:command')
18+
19+
function wrapCommandFactory (commandFactory) {
20+
return HOOK(commandFactory)()
21+
}
22+
23+
describe('packages/datadog-instrumentations/src/aerospike.js', () => {
24+
let starts
25+
let asyncStarts
26+
27+
beforeEach(() => {
28+
starts = 0
29+
asyncStarts = 0
30+
31+
commandChannel.start.bindStore(commandStorage, ctx => {
32+
starts++
33+
const parentStore = commandStorage.getStore()
34+
ctx.parentStore = parentStore
35+
ctx.currentStore = { ...parentStore, span: { name: 'aerospike-command' } }
36+
return ctx.currentStore
37+
})
38+
39+
commandChannel.asyncStart.bindStore(commandStorage, ctx => {
40+
asyncStarts++
41+
return ctx.parentStore
42+
})
43+
})
44+
45+
afterEach(() => {
46+
commandChannel.start.unbindStore(commandStorage)
47+
commandChannel.asyncStart.unbindStore(commandStorage)
48+
})
49+
50+
it('runs callbacks in the parent context after Aerospike defers a synchronous result', async () => {
51+
const Command = wrapCommandFactory(() => class FakeCommand {
52+
constructor () {
53+
this.args = ['arg']
54+
this.client = { config: { hosts: '127.0.0.1:3000' } }
55+
}
56+
57+
process (callback) {
58+
callback(null, 'ok')
59+
}
60+
61+
executeWithCallback (callback) {
62+
let sync = true
63+
this.process((error, result) => {
64+
if (sync) {
65+
process.nextTick(callback, error, result)
66+
} else {
67+
callback(error, result)
68+
}
69+
})
70+
sync = false
71+
}
72+
})
73+
74+
const parentSpan = { name: 'parent' }
75+
const command = new Command()
76+
77+
const result = await new Promise((resolve, reject) => {
78+
commandStorage.run({ span: parentSpan }, () => {
79+
command.executeWithCallback((error, value) => {
80+
try {
81+
assert.ifError(error)
82+
assert.equal(commandStorage.getStore()?.span, parentSpan)
83+
resolve(value)
84+
} catch (err) {
85+
reject(err)
86+
}
87+
})
88+
})
89+
})
90+
91+
assert.equal(result, 'ok')
92+
assert.equal(starts, 1)
93+
assert.equal(asyncStarts, 1)
94+
})
95+
96+
it('still traces commands through process when no callback helper exists', async () => {
97+
const Command = wrapCommandFactory(() => class FakeCommand {
98+
constructor () {
99+
this.args = ['arg']
100+
this.client = { config: { hosts: '127.0.0.1:3000' } }
101+
}
102+
103+
process (callback) {
104+
process.nextTick(callback, null, 'ok')
105+
}
106+
107+
executeAndReturnPromise () {
108+
return new Promise((resolve, reject) => {
109+
this.process((error, result) => {
110+
if (error) {
111+
reject(error)
112+
} else {
113+
resolve(result)
114+
}
115+
})
116+
})
117+
}
118+
})
119+
120+
const parentSpan = { name: 'parent' }
121+
const command = new Command()
122+
123+
const result = await commandStorage.run({ span: parentSpan }, () => command.executeAndReturnPromise())
124+
125+
assert.equal(result, 'ok')
126+
assert.equal(starts, 1)
127+
assert.equal(asyncStarts, 1)
128+
})
129+
})

0 commit comments

Comments
 (0)