You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

164 lines
4.7 KiB
JavaScript

/*
* Copyright DataStax, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
const util = require('util');
const utils = require('../utils');
const promiseUtils = require('../promise-utils');
const _queueOverflowThreshold = 1000;
/**
* Debounce protocol events by acting on those events with a sliding delay.
* @ignore
* @constructor
*/
class EventDebouncer {
/**
* Creates a new instance of the event debouncer.
* @param {Number} delay
* @param {Function} logger
*/
constructor(delay, logger) {
this._delay = delay;
this._logger = logger;
this._queue = null;
this._timeout = null;
}
/**
* Adds a new event to the queue and moves the delay.
* @param {{ handler: Function, all: boolean|undefined, keyspace: String|undefined,
* cqlObject: String|null|undefined }} event
* @param {Boolean} processNow
* @returns {Promise}
*/
eventReceived(event, processNow) {
return new Promise((resolve, reject) => {
event.callback = promiseUtils.getCallback(resolve, reject);
this._queue = this._queue || { callbacks: [], keyspaces: {} };
const delay = !processNow ? this._delay : 0;
if (event.all) {
// when an event marked with all is received, it supersedes all the rest of events
// a full update (hosts + keyspaces + tokens) is going to be made
this._queue.mainEvent = event;
}
if (this._queue.callbacks.length === _queueOverflowThreshold) {
// warn once
this._logger('warn', util.format('Event debouncer queue exceeded %d events', _queueOverflowThreshold));
}
this._queue.callbacks.push(event.callback);
if (this._queue.mainEvent) {
// a full refresh is scheduled and the callback was added, nothing else to do.
return this._slideDelay(delay);
}
// Insert at keyspace level
let keyspaceEvents = this._queue.keyspaces[event.keyspace];
if (!keyspaceEvents) {
keyspaceEvents = this._queue.keyspaces[event.keyspace] = { events: [] };
}
if (event.cqlObject === undefined) {
// a full refresh of the keyspace, supersedes all child keyspace events
keyspaceEvents.mainEvent = event;
}
keyspaceEvents.events.push(event);
this._slideDelay(delay);
});
}
/**
* @param {Number} delay
* @private
* */
_slideDelay(delay) {
const self = this;
function process() {
const q = self._queue;
self._queue = null;
self._timeout = null;
processQueue(q);
}
if (delay === 0) {
// no delay, process immediately
if (this._timeout) {
clearTimeout(this._timeout);
}
return process();
}
const previousTimeout = this._timeout;
// Add the new timeout before removing the previous one performs better
this._timeout = setTimeout(process, delay);
if (previousTimeout) {
clearTimeout(previousTimeout);
}
}
/**
* Clears the timeout and invokes all pending callback.
*/
shutdown() {
if (!this._queue) {
return;
}
this._queue.callbacks.forEach(function (cb) {
cb();
});
this._queue = null;
clearTimeout(this._timeout);
this._timeout = null;
}
}
/**
* @param {{callbacks: Array, keyspaces: Object, mainEvent: Object}} q
* @private
*/
function processQueue (q) {
if (q.mainEvent) {
// refresh all by invoking 1 handler and invoke all pending callbacks
return promiseUtils.toCallback(q.mainEvent.handler(), (err) => {
for (let i = 0; i < q.callbacks.length; i++) {
q.callbacks[i](err);
}
});
}
utils.each(Object.keys(q.keyspaces), function eachKeyspace(name, next) {
const keyspaceEvents = q.keyspaces[name];
if (keyspaceEvents.mainEvent) {
// refresh a keyspace
return promiseUtils.toCallback(keyspaceEvents.mainEvent.handler(), function mainEventCallback(err) {
for (let i = 0; i < keyspaceEvents.events.length; i++) {
keyspaceEvents.events[i].callback(err);
}
next();
});
}
// deal with individual handlers and callbacks
keyspaceEvents.events.forEach(event => {
// sync handlers
event.handler();
event.callback();
});
next();
});
}
module.exports = EventDebouncer;