elastic-apm-node
Version:
The official Elastic APM agent for Node.js
160 lines (147 loc) • 4.5 kB
JavaScript
/*
* Copyright Elasticsearch B.V. and other contributors where applicable.
* Licensed under the BSD 2-Clause License; you may not use this file except in
* compliance with the BSD 2-Clause License.
*/
'use strict';
const { EventEmitter } = require('events');
/**
* Coordinates fetching of metadata from multiple providers
*
* Implements event based coordination for fetching metadata
* from multiple providers. The first provider to return
* a non-error result "wins". When this happens the CallbackCoordination
* object will emit a `result` event.
*
* If all the metadata providers fail to return a result, then the
* object will emit an error event indicating failure to collect metadata
* from any event.
*
* Since a scheduled callback may (accidently) fail to call its own
* callback function, the CallbackCoordination object includes its
* own timeout timer to avoid deadlock situation.
*/
class CallbackCoordination extends EventEmitter {
constructor(maxWaitMS = -1, logger) {
super();
this.logger = logger;
// how many results have we seen
this.resultCount = 0;
this.expectedResults = 0;
this.errors = [];
this.scheduled = [];
this.done = false;
this.started = false;
this.timeout = null;
if (maxWaitMS !== -1) {
this.timeout = setTimeout(() => {
if (!this.done) {
this.complete();
this.logger.warn(
'cloud metadata requests timed out, using default values instead',
);
const error = new CallbackCoordinationError(
'callback coordination reached timeout',
this.errors,
);
this.emit('error', error);
}
}, maxWaitMS);
}
}
/**
* Finishes coordination
*
* Marks as `done`, cleans up timeout (if necessary)
*/
complete() {
this.done = true;
if (this.timeout) {
clearTimeout(this.timeout);
}
}
/**
* Accepts and schedules a callback function
*
* Callback will be in the form
* function(fetcher) {
* //... work to fetch data ...
*
* // this callback calls the recordResult method
* // method of the fetcher
* fetcher.recordResult(error, result)
* }
*
* Method also increments expectedResults counter to keep track
* of how many callbacks we've scheduled
*/
schedule(fetcherCallback) {
if (this.started) {
this.logger.error('Can not schedule callback, already started');
return;
}
this.expectedResults++;
this.scheduled.push(fetcherCallback);
}
/**
* Starts processing of the callbacks scheduled by the `schedule` method
*/
start() {
this.started = true;
// if called with nothing, send an error through so we don't hang
if (this.scheduled.length === 0) {
const error = new CallbackCoordinationError('no callbacks to run');
this.recordResult(error);
}
for (const cb of this.scheduled) {
process.nextTick(cb.bind(null, this));
}
}
/**
* Receives calls from scheduled callbacks.
*
* If called with a non-error, the method will emit a `result` event
* and include the results as an argument. Only a single result
* is emitted -- if other callbacks respond with a result this method
* will ignore them.
*
* If called by _all_ scheduled callbacks without a non-error, this method
* will issue the error event.
*/
recordResult(error, result) {
// console.log('.')
this.resultCount++;
if (error) {
this.errors.push(error);
if (this.resultCount >= this.expectedResults && !this.done) {
this.complete();
// we've made every request without success, signal an error
const error = new CallbackCoordinationError(
'no response from any callback, no cloud metadata will be set (normal outside of cloud env.)',
this.errors,
);
this.logger.debug('no cloud metadata servers responded');
this.emit('error', error);
}
}
if (!error && result && !this.done) {
this.complete();
this.emit('result', result);
}
}
}
/**
* Error for CallbackCoordination class
*
* Includes the individual errors from each callback
* of the CallbackCoordination object
*/
class CallbackCoordinationError extends Error {
constructor(message, allErrors = []) {
super(message);
this.allErrors = allErrors;
}
}
module.exports = {
CallbackCoordination,
};