Skip to content

The AsyncTracker API is the JavaScript interface which allows developers to be notified about key events in the lifetime of observed objects and scheduled asynchronous events.

License

Notifications You must be signed in to change notification settings

strongloop/async-tracker

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

18 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Async Tracker Library

The AsyncTracker API is the JavaScript interface which allows developers to be notified about key events in the lifetime of observed objects and scheduled asynchronous events.

Note
Node performs a lot of asynchronous events internally, and use of this API may have a significant performance impact on your application.

Objectives

The goal of this library is to build a common API which allows creation of node application monitoring tools, exception management, long stack-traces, resource tracking and cleanup etc.

Most of the existing libraries which provide this functionality monkey-patch the node API layer and typically use closures to maintain context across async callbacks.

However when one or more of these libraries are used simultaneously, there are drawbacks such as:

  • speed degradation

  • instability

  • loss of function information such as function.length, function.toString() etc.

Ideally, there would be a common API that other libraries can build upon. The Async Tracker library attempts to do just that by providing a single interface on which other libraries can be built. The use-cases captured in Async Tracker include:

  • Associating context which is carried across async callback boundaries

  • Notification when async callback is scheduled

    • Optionally capturing significant arguments from the parent function

  • Notification of callback being triggered

    • Optionally allow running tasks before and after the callback

  • Notification when a callback is un-scheduled (timers, etc.)

  • Notification when Observable objects are created or released

  • Enable cleanup of Disposable objects

It is meant as a conversation starter and I would like to discuss any missing functionality or other feedback you have around this topic.

API Overview

Async Tracker has 4 core APIs:

  1. AsyncTracker Listeners: allows developers to register an object which will be notified of events

  2. AsyncTracker Observables: allows developers to instrument their classes so they trigger events when they are created or released.

  3. AsyncTracker Disposables: allows developers to enable cleanup of their classes (used by Zones/Domains).

  4. Deferred: allows developers to wrap callback methods so they trigger events when they are created, invoked or released.

AsyncTracker Listener interface

The AsyncTracker object is exposed as a global variable named asyncTracker.

asyncTracker.addListener(listenerObj, [name])

name: Optional. Listener name for the new listener, or an existing listener which should be replaced. listenerObj: An object which contains several optional fields. This listener will be associated with all async operations that will be scheduled and all objects that will be created.

asyncTracker.removeListener(listenerName)

Prevents the listenerObj associated with listenerName from being associated with any future async callbacks or objects. Any async callbacks or objects already associated with the listener will continue to trigger.

AsyncTracker Trigger interface

These methods are intended to be called from inside node modules and will result in corresponding functions on the listener to be triggered.

AsyncTracker.deferredCreated(fName, cbId, fData)

Called when a function is registered for deferred execution. This function may be executed one or more times in the future.

  • fName: A name of the function that scheduled the callback.

  • cbId: Unique ID of this callback

  • fData: If the listener specified interest in function arguments, fData

AsyncTracker.runDeferred(fName, cbId, next)

Called when a function needs to be invoked.

  • fName: A name of the function that scheduled the callback.

  • cbId: Unique ID of this callback

AsyncTracker.deferredReleased(fName, cbId)

Called when a function is complete and will no longer be called in the future.

  • fName: A name of the function that scheduled the callback.

  • cbId: Unique ID of this callback

Listener Object interface

Warning
Be very careful about the implementation of these functions. They will be invoked often and can result in significant performance issues. Calling function that are async from these callbacks can result in an infinite loop

Listener.deferredCreated

Map of callback functions for events that this Listener is interested in.

Callback:

  • deferredCreated(fName, cbId, fData): A function which is called when an async callback is scheduled.

    • fName: A name of the function that scheduled the callback.

    • cbId: Unique ID of this callback

    • fData: If the listener specified interest in function arguments, fData will contain a map of the significant arguments

Example:

Listener.deferredCreated[fs.open] = function callback(fName, cbId, fData){...};
Listener.deferredCreated['default'] = function defaultCallback(fName, cbId, fData){...};

Listener.invokeDeferred

Map of callback functions for events that this Listener is interested in.

Callback:

  • invokeDeferred(fName, cbId, next): A function can intercept the invocation of an async callback. This function behaves similar to Express middleware and you must call next() to continue invocation of the callback. Other libraries can use this call to run their own code before and after function execution. They may also choose to wrap the function invocation in a try/catch/finally block to catch and handle exceptions.

    • fName: A name of the function that scheduled the callback.

    • cbId: Unique ID of this callback

    • next: The callback function to execute

Example:

Listener.invokeDeferred[fs.open] = function callback(fName, cbId, next){...};
Listener.invokeDeferred['default'] = function defaultCallback(fName, cbId, next){...};

Listener.deferredReleased

Map of callback functions for events that this Listener is interested in.

Callback:

  • deferredReleased(fName, cbId): A function

    • fName: A name of the function that scheduled the callback.

    • cbId: Unique ID of this callback

Example:

Listener.deferredReleased[fs.open] = function callback(fName, cbId){...};
Listener.deferredReleased['default'] = function defaultCallback(fName, cbId){...};

Listener.trackObject(obj)

  • trackObject(obj): A function which is called when an Observable object is created

    • obj: The observable object

Example:

Listener.trackObject = function callback(obj){...};

Listener.releaseObject(obj)

  • releaseObject(obj): A function which is called when an Observable object is closed, destroyed or, explicitly released.

    • obj: The observable object

Example:

Listener.releaseObject = function callback(obj){...};

AsyncTracker Observable interface

The Observable API allows objects to trigger events so that they can be tracked by `listenerObj`s. Developers of other external libraries can also add these calls into their objects if they wish for them to be tracked.

For example, when you open a file with Node, it returns the file handle. This library maintains a list of open handles as FDTracker objects and triggers the Observable API events when a file is opened or closed. A library like Zones can then use this information to track and close file handles even if the user code has lost track of it.

asyncTracker.trackObject(obj)

Associate obj with the currently active listenerObj and trigger the trackObject function.

asyncTracker.releaseObject(obj)

Un-associate obj with the listenerObj and trigger the releaseObject function.

Disposable

This API should be implemented by tracked objects if they wish to be cleaned up by modules like Zones or Domains when they exit. This API relies on the object also registering using the Observable APIs.

Object.dispose()

This method is called by Zones or similar libraries when they are exiting and wish to cleanup a tracked object.

Deferred (Helper API)

The Deferred API provides helper functions which can be used by developers to wrap callback functions so that they trigger the appropriate functions on listenerObj and maintain context.

Deferred.wrap(fName, fArgs, fCallback)

Developers can use this function to wrap a generic callback. This function will return a closure which will trigger the appropriate listenerObj functions.

  • fName: The name of function that uses this callback. Eg: fs.open

  • fArgs: Map of argument name to values. This will be passed to listeners that are interested in function arguments

  • fCallback: The callback function to be wrapped

Deferred.wrapWithArguments(fName, fArgs, fCallback, callbackArgs)

Developers can use this function to wrap a generic callback. This function will return a closure which will trigger the appropriate listenerObj functions.

  • fName: The name of function that uses this callback. Eg: fs.open

  • fArgs: Map of argument name to values. This will be passed to listeners that are interested in function arguments

  • fCallback: The callback function to be wrapped

Deferred.wrapMethod(fMethod, argMap, callbackPos)

  • fMethod: The method to be wrapped

  • argMap: Map of argument name to argument positions. This is used to construct the argument map for listeners that are interested in function arguments. If an argument is optional, it should be prefixed with ?.

  • callbackPos: Optional position of the callback function. If not provided, it assumes the last argument is the callback function.

Deferred.wrapRequest(fMethod, argList, callbackPos)

  • fMethod: The request method to be wrapped

  • argList: List of request argument names. This is used to construct the argument map for listeners that are interested in function arguments. If an argument is optional, it should be prefixed with ?.

  • callbackPos: Optional position of the callback function. If not provided, it assumes the last argument is the callback function.

Implementation

Ideally, all the events generated from this library would happen in core Node code. However, this library has been created using monkey-patching to experiment and stabilize the API before attempting to get in include in node core.

This library provides a very small subset of the implementation in order to demonstrate the API and concept. The following Node core APIs will need to be patched for a more complete implementation:

  • Cares-wrap (DNS APIs)

  • Process-wrap (child-process APIs)

  • Stream-wrap

  • Cluster

  • Crypto (pbkdf2, randomBytes, pseudoRandomBytes)

  • fs Watch APIs: (fs.watch, fs.watchFile, fs.FSWatcher)

  • process object: process.on('SIGHUP') and other signals.

  • tls / https

  • udp

  • zlib

All the Listener calls are executed within the context of the functions creating the deferred callbacks or in the context of the callback execution. This allows libraries built using AsyncTracker to gather whatever structured data they require to operate.

Although the current code is completely written in JS, the API should be accessible from C/C++ code as well allowing for native modules to be use all AsyncTracker capabilities as long they comply with the interfaces.

Possible optimizations

Ben has written a patch which builds upon the AsyncWrap API to allow tracking calls across async boundaries without having to maintain a closure.

About

The AsyncTracker API is the JavaScript interface which allows developers to be notified about key events in the lifetime of observed objects and scheduled asynchronous events.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published