metaapi.cloud-sdk
Version:
SDK for MetaApi, a professional cloud forex API which includes MetaTrader REST API and MetaTrader websocket API. Supports both MetaTrader 5 (MT5) and MetaTrader 4 (MT4). CopyFactory copy trading API included. (https://metaapi.cloud)
312 lines (286 loc) • 11.5 kB
text/typescript
'use strict';
import TimeoutError from '../timeoutError';
import OptionsValidator from '../optionsValidator';
import LoggerManager, {Logger} from '../../logger';
import MetaApiWebsocketClient from './metaApiWebsocket.client';
/**
* Options for synchronization throttler
* @typedef {Object} SynchronizationThrottlerOpts
* @property {Number} [maxConcurrentSynchronizations] amount of maximum allowed concurrent synchronizations
* @property {Number} [queueTimeoutInSeconds] allowed time for a synchronization in queue
* @property {Number} [synchronizationTimeoutInSeconds] time after which a synchronization slot
* is freed to be used by another synchronization
*/
/**
* Synchronization throttler used to limit the amount of concurrent synchronizations to prevent application
* from being overloaded due to excessive number of synchronisation responses being sent.
*/
export default class SynchronizationThrottler {
private _maxConcurrentSynchronizations: any;
private _queueTimeoutInSeconds: any;
private _synchronizationTimeoutInSeconds: any;
private _client: MetaApiWebsocketClient;
private _region: any;
private _socketInstanceIndex: any;
private _synchronizationIds: {};
private _accountsBySynchronizationIds: {};
private _synchronizationQueue: any[];
private _removeOldSyncIdsInterval: any;
private _processQueueInterval: any;
private _instanceNumber: any;
private _logger: Logger;
/**
* Constructs the synchronization throttler
* @param {MetaApiWebsocketClient} client MetaApi websocket client
* @param {Number} socketInstanceIndex index of socket instance that uses the throttler
* @param {Number} instanceNumber instance index number
* @param {String} region server region
* @param {SynchronizationThrottlerOpts} opts synchronization throttler options
*/
constructor(client, socketInstanceIndex, instanceNumber, region, opts: SynchronizationThrottlerOpts) {
const validator = new OptionsValidator();
opts = opts || {};
this._maxConcurrentSynchronizations = validator.validateNonZero(opts.maxConcurrentSynchronizations, 15,
'synchronizationThrottler.maxConcurrentSynchronizations');
this._queueTimeoutInSeconds = validator.validateNonZero(opts.queueTimeoutInSeconds, 300,
'synchronizationThrottler.queueTimeoutInSeconds');
this._synchronizationTimeoutInSeconds = validator.validateNonZero(opts.synchronizationTimeoutInSeconds, 10,
'synchronizationThrottler.synchronizationTimeoutInSeconds');
this._client = client;
this._region = region;
this._socketInstanceIndex = socketInstanceIndex;
this._synchronizationIds = {};
this._accountsBySynchronizationIds = {};
this._synchronizationQueue = [];
this._removeOldSyncIdsInterval = null;
this._processQueueInterval = null;
this._instanceNumber = instanceNumber;
this._logger = LoggerManager.getLogger('SynchronizationThrottler');
}
/**
* Initializes the synchronization throttler
*/
start() {
if(!this._removeOldSyncIdsInterval) {
this._removeOldSyncIdsInterval = setInterval(() => this._removeOldSyncIdsJob(), 1000);
this._processQueueInterval = setInterval(() => this._processQueueJob(), 1000);
}
}
/**
* Deinitializes the throttler
*/
stop() {
clearInterval(this._removeOldSyncIdsInterval);
this._removeOldSyncIdsInterval = null;
clearInterval(this._processQueueInterval);
this._processQueueInterval = null;
}
async _removeOldSyncIdsJob() {
const now = Date.now();
for (let key of Object.keys(this._synchronizationIds)) {
if ((now - this._synchronizationIds[key]) > this._synchronizationTimeoutInSeconds * 1000) {
delete this._synchronizationIds[key];
}
}
while (this._synchronizationQueue.length && (Date.now() - this._synchronizationQueue[0].queueTime) >
this._queueTimeoutInSeconds * 1000) {
this._removeFromQueue(this._synchronizationQueue[0].synchronizationId, 'timeout');
}
this._advanceQueue();
}
/**
* Fills a synchronization slot with synchronization id
* @param {String} synchronizationId synchronization id
*/
updateSynchronizationId(synchronizationId) {
if(this._accountsBySynchronizationIds[synchronizationId]) {
this._synchronizationIds[synchronizationId] = Date.now();
}
}
/**
* Returns the list of currently synchronizing account ids
*/
get synchronizingAccounts() {
const synchronizingAccounts = [];
Object.keys(this._synchronizationIds).forEach(key => {
const accountData = this._accountsBySynchronizationIds[key];
if(accountData && !synchronizingAccounts.includes(accountData.accountId)) {
synchronizingAccounts.push(accountData.accountId);
}
});
return synchronizingAccounts;
}
/**
* Returns the list of currenly active synchronization ids
* @return {String[]} synchronization ids
*/
get activeSynchronizationIds() {
return Object.keys(this._accountsBySynchronizationIds);
}
/**
* Returns the amount of maximum allowed concurrent synchronizations
* @return {number} maximum allowed concurrent synchronizations
*/
get maxConcurrentSynchronizations() {
const calculatedMax = Math.max(Math.ceil(
this._client.subscribedAccountIds(this._instanceNumber, this._socketInstanceIndex, this._region).length / 10), 1);
return Math.min(calculatedMax, this._maxConcurrentSynchronizations);
}
/**
* Returns flag whether there are free slots for synchronization requests
* @return {Boolean} flag whether there are free slots for synchronization requests
*/
get isSynchronizationAvailable() {
if (this._client.socketInstances[this._region][this._instanceNumber].reduce((acc, socketInstance) =>
acc + socketInstance.synchronizationThrottler.synchronizingAccounts.length, 0) >=
this._maxConcurrentSynchronizations) {
return false;
}
return this.synchronizingAccounts.length < this.maxConcurrentSynchronizations;
}
/**
* Removes synchronizations from queue and from the list by parameters
* @param {String} accountId account id
* @param {Number} instanceIndex account instance index
* @param {String} host account host name
*/
removeIdByParameters(accountId, instanceIndex, host) {
for (let key of Object.keys(this._accountsBySynchronizationIds)) {
if(this._accountsBySynchronizationIds[key].accountId === accountId &&
this._accountsBySynchronizationIds[key].instanceIndex === instanceIndex &&
this._accountsBySynchronizationIds[key].host === host) {
this.removeSynchronizationId(key);
}
}
}
/**
* Removes synchronization id from slots and removes ids for the same account from the queue
* @param {String} synchronizationId synchronization id
*/
removeSynchronizationId(synchronizationId) {
if (this._accountsBySynchronizationIds[synchronizationId]) {
const accountId = this._accountsBySynchronizationIds[synchronizationId].accountId;
const instanceIndex = this._accountsBySynchronizationIds[synchronizationId].instanceIndex;
const host = this._accountsBySynchronizationIds[synchronizationId].host;
for (let key of Object.keys(this._accountsBySynchronizationIds)) {
if(this._accountsBySynchronizationIds[key].accountId === accountId &&
this._accountsBySynchronizationIds[key].instanceIndex === instanceIndex &&
this._accountsBySynchronizationIds[key].host === host) {
this._removeFromQueue(key, 'cancel');
delete this._accountsBySynchronizationIds[key];
}
}
}
if(this._synchronizationIds[synchronizationId]) {
delete this._synchronizationIds[synchronizationId];
}
this._advanceQueue();
}
/**
* Clears synchronization ids on disconnect
*/
onDisconnect() {
this._synchronizationQueue.forEach(synchronization => {
synchronization.resolve('cancel');
});
this._synchronizationIds = {};
this._accountsBySynchronizationIds = {};
this._synchronizationQueue = [];
this.stop();
this.start();
}
_advanceQueue() {
let index = 0;
while(this.isSynchronizationAvailable && this._synchronizationQueue.length &&
index < this._synchronizationQueue.length) {
const queueItem = this._synchronizationQueue[index];
queueItem.resolve('synchronize');
this.updateSynchronizationId(queueItem.synchronizationId);
index++;
}
}
_removeFromQueue(synchronizationId, result) {
this._synchronizationQueue.forEach((syncItem, i) => {
if(syncItem.synchronizationId === synchronizationId) {
syncItem.resolve(result);
}
});
this._synchronizationQueue = this._synchronizationQueue.filter(item =>
item.synchronizationId !== synchronizationId);
}
async _processQueueJob() {
try {
while (this._synchronizationQueue.length) {
const queueItem = this._synchronizationQueue[0];
await this._synchronizationQueue[0].promise;
if(this._synchronizationQueue.length && this._synchronizationQueue[0].synchronizationId ===
queueItem.synchronizationId) {
this._synchronizationQueue.shift();
}
}
} catch (err) {
this._logger.error('Error processing queue job', err);
}
}
/**
* Schedules to send a synchronization request for account
* @param {String} accountId account id
* @param {Object} request request to send
* @param {Object} hashes terminal state hashes
*/
async scheduleSynchronize(accountId, request, hashes) {
const synchronizationId = request.requestId;
for (let key of Object.keys(this._accountsBySynchronizationIds)) {
if(this._accountsBySynchronizationIds[key].accountId === accountId &&
this._accountsBySynchronizationIds[key].instanceIndex === request.instanceIndex &&
this._accountsBySynchronizationIds[key].host === request.host) {
this.removeSynchronizationId(key);
}
}
this._accountsBySynchronizationIds[synchronizationId] = {accountId, instanceIndex: request.instanceIndex,
host: request.host};
if(!this.isSynchronizationAvailable) {
let resolve;
let requestResolve = new Promise((res) => {
resolve = res;
});
this._synchronizationQueue.push({
synchronizationId: synchronizationId,
promise: requestResolve,
resolve,
queueTime: Date.now()
});
const result = await requestResolve;
if(result === 'cancel') {
return false;
} else if(result === 'timeout') {
throw new TimeoutError(`Account ${accountId} synchronization ${synchronizationId}` +
' timed out in synchronization queue');
}
}
this.updateSynchronizationId(synchronizationId);
request.specificationsHashes = hashes.specificationsHashes;
request.positionsHashes = hashes.positionsHashes;
request.ordersHashes = hashes.ordersHashes;
await this._client.rpcRequest(accountId, request);
return true;
}
}
/**
* Options for synchronization throttler
*/
export type SynchronizationThrottlerOpts = {
/**
* amount of maximum allowed concurrent synchronizations
*/
maxConcurrentSynchronizations?: number,
/**
* allowed time for a synchronization in queue
*/
queueTimeoutInSeconds?: number,
/**
* time after which a synchronization slot
* is freed to be used by another synchronization
*/
synchronizationTimeoutInSeconds?: number
}