1414 * limitations under the License.
1515 */
1616
17- // This file requires continuation-local-storage in the AsyncHooksCLS
18- // constructor, rather than upon module load.
17+ // This file calls require('async_hooks') in the AsyncHooksCLS constructor,
18+ // rather than upon module load.
1919import * as asyncHooksModule from 'async_hooks' ;
2020import { EventEmitter } from 'events' ;
2121import * as shimmer from 'shimmer' ;
@@ -31,29 +31,63 @@ const EVENT_EMITTER_METHODS: Array<keyof EventEmitter> =
3131const WRAPPED = Symbol ( '@google-cloud/trace-agent:AsyncHooksCLS:WRAPPED' ) ;
3232
3333type ContextWrapped < T > = T & { [ WRAPPED ] ?: boolean } ;
34+ type Reference < T > = {
35+ value : T
36+ } ;
3437
3538/**
3639 * An implementation of continuation-local storage on top of the async_hooks
3740 * module.
3841 */
3942export class AsyncHooksCLS < Context extends { } > implements CLS < Context > {
40- private currentContext : { value : Context } ;
41- private contexts : { [ id : number ] : Context } = { } ;
43+ // instance-scope reference to avoid top-level require.
44+ private ah : AsyncHooksModule ;
45+
46+ /** A map of AsyncResource IDs to Context objects. */
47+ private contexts : { [ id : number ] : Reference < Context > } = { } ;
48+ /** The AsyncHook that proactively populates entries in this.contexts. */
4249 private hook : asyncHooksModule . AsyncHook ;
50+ /** Whether this instance is enabled. */
4351 private enabled = false ;
4452
4553 constructor ( private readonly defaultContext : Context ) {
46- this . currentContext = { value : this . defaultContext } ;
47- this . hook = ( require ( 'async_hooks' ) as AsyncHooksModule ) . createHook ( {
54+ // Store a reference to the async_hooks module, since we will need to query
55+ // the current AsyncResource ID often.
56+ this . ah = require ( 'async_hooks' ) as AsyncHooksModule ;
57+
58+ // Create the hook.
59+ this . hook = this . ah . createHook ( {
4860 init : ( id : number , type : string , triggerId : number , resource : { } ) => {
49- this . contexts [ id ] = this . currentContext . value ;
50- } ,
51- before : ( id : number ) => {
52- if ( this . contexts [ id ] ) {
53- this . currentContext . value = this . contexts [ id ] ;
61+ // init is called when a new AsyncResource is created. We want code
62+ // that runs within the scope of this new AsyncResource to see the same
63+ // context as its "parent" AsyncResource. The criteria for the parent
64+ // depends on the type of the AsyncResource.
65+ if ( type === 'PROMISE' ) {
66+ // Opt not to use the trigger ID for Promises, as this causes context
67+ // confusion in applications using async/await.
68+ // Instead, use the ID of the AsyncResource in whose scope we are
69+ // currently running.
70+ this . contexts [ id ] = this . contexts [ this . ah . executionAsyncId ( ) ] ;
71+ } else {
72+ // Use the trigger ID for any other type. In Node core, this is
73+ // usually equal the ID of the AsyncResource in whose scope we are
74+ // currently running (the "current" AsyncResource), or that of one
75+ // of its ancestors, so the behavior is not expected to be different
76+ // from using the ID of the current AsyncResource instead.
77+ // A divergence is expected only to arise through the user
78+ // AsyncResource API, because users of that API can specify their own
79+ // trigger ID. In this case, we choose to respect the user's
80+ // selection.
81+ this . contexts [ id ] = this . contexts [ triggerId ] ;
5482 }
83+ // Note that this function always assigns values in this.contexts to
84+ // values under other keys, which may or may not be undefined. Consumers
85+ // of the CLS API will get the sentinel (default) value if they query
86+ // the current context when it is stored as undefined.
5587 } ,
5688 destroy : ( id : number ) => {
89+ // destroy is called when the AsyncResource is no longer used, so also
90+ // delete its entry in the map.
5791 delete this . contexts [ id ] ;
5892 }
5993 } ) ;
@@ -64,51 +98,85 @@ export class AsyncHooksCLS<Context extends {}> implements CLS<Context> {
6498 }
6599
66100 enable ( ) : void {
67- this . currentContext . value = this . defaultContext ;
101+ this . contexts = { } ;
68102 this . hook . enable ( ) ;
69103 this . enabled = true ;
70104 }
71105
72106 disable ( ) : void {
73- this . currentContext . value = this . defaultContext ;
107+ this . contexts = { } ;
74108 this . hook . disable ( ) ;
75109 this . enabled = false ;
76110 }
77111
78112 getContext ( ) : Context {
79- return this . currentContext . value ;
113+ // We don't store this.defaultContext directly in this.contexts.
114+ // Getting undefined when looking up this.contexts means that it wasn't
115+ // set, so return the default context.
116+ const current = this . contexts [ this . ah . executionAsyncId ( ) ] ;
117+ return current ? current . value : this . defaultContext ;
80118 }
81119
82120 setContext ( value : Context ) : void {
83- this . currentContext . value = value ;
121+ const id = this . ah . executionAsyncId ( ) ;
122+ const current = this . contexts [ id ] ;
123+ if ( current ) {
124+ current . value = value ;
125+ } else {
126+ this . contexts [ id ] = { value} ;
127+ }
84128 }
85129
86130 runWithNewContext < T > ( fn : Func < T > ) : T {
87- const oldContext = this . currentContext . value ;
88- this . currentContext . value = this . defaultContext ;
131+ // Run fn() so that any AsyncResource objects that are created in
132+ // fn will have the context set by this.setContext.
133+ const id = this . ah . executionAsyncId ( ) ;
134+ const oldContext = this . contexts [ id ] ;
135+ // Reset the current context. This prevents this.getContext from returning
136+ // a stale value.
137+ this . contexts [ id ] = { value : this . defaultContext } ;
89138 try {
90139 return fn ( ) ;
91140 } finally {
92- this . currentContext . value = oldContext ;
141+ // Revert the current context to what it was before any calls to
142+ // this.setContext from within fn.
143+ this . contexts [ id ] = oldContext ;
93144 }
94145 }
95146
96147 bindWithCurrentContext < T > ( fn : Func < T > ) : Func < T > {
97- if ( ( fn as ContextWrapped < Func < T > > ) [ WRAPPED ] || ! this . currentContext ) {
148+ // Return if we have already wrapped the function.
149+ if ( ( fn as ContextWrapped < Func < T > > ) [ WRAPPED ] ) {
98150 return fn ;
99151 }
100- const current = this . currentContext ;
101- const boundContext = this . currentContext . value ;
152+ // Capture the context of the current AsyncResource.
153+ const boundContext = this . contexts [ this . ah . executionAsyncId ( ) ] ;
154+ // Return if there is no current context to bind.
155+ if ( ! boundContext ) {
156+ return fn ;
157+ }
158+ const that = this ;
159+ // TODO(kjin): This code is somewhat duplicated with runWithNewContext.
160+ // Can we merge this?
161+ // Wrap fn so that any AsyncResource objects that are created in fn will
162+ // share context with that of the AsyncResource with the given ID.
102163 const contextWrapper : ContextWrapped < Func < T > > = function ( this : { } ) {
103- const oldContext = current . value ;
104- current . value = boundContext ;
164+ const id = that . ah . executionAsyncId ( ) ;
165+ const oldContext = that . contexts [ id ] ;
166+ // Restore the captured context.
167+ that . contexts [ id ] = boundContext ;
105168 try {
106169 return fn . apply ( this , arguments ) as T ;
107170 } finally {
108- current . value = oldContext ;
171+ // Revert the current context to what it was before it was set to the
172+ // captured context.
173+ that . contexts [ id ] = oldContext ;
109174 }
110175 } ;
176+ // Prevent re-wrapping.
111177 contextWrapper [ WRAPPED ] = true ;
178+ // Explicitly inherit the original function's length, because it is
179+ // otherwise zero-ed out.
112180 Object . defineProperty ( contextWrapper , 'length' , {
113181 enumerable : false ,
114182 configurable : true ,
@@ -118,11 +186,6 @@ export class AsyncHooksCLS<Context extends {}> implements CLS<Context> {
118186 return contextWrapper ;
119187 }
120188
121- // This function is not technically needed and all tests currently pass
122- // without it (after removing call sites). While it is not a complete
123- // solution, restoring correct context before running every request/response
124- // event handler reduces the number of situations in which userspace queuing
125- // will cause us to lose context.
126189 patchEmitterToPropagateContext ( ee : EventEmitter ) : void {
127190 const that = this ;
128191 EVENT_EMITTER_METHODS . forEach ( ( method ) => {
0 commit comments