UNPKG

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)

1,540 lines (1,266 loc) 49 kB
'use strict'; import RootProcessPool from '../root/rootProcessPool'; import log4js from 'log4js'; import should from 'should'; import sinon from 'sinon'; import * as helpers from '../../../../helpers/helpers'; import * as assert from '../../../../helpers/test/assert'; import ControlSignal from '../controlSignal'; import * as errors from '../../errors'; import AsyncProcessPool from './asyncProcessPool'; import RootProcess from '../root/rootProcess'; import RootProcessContext from '../root/rootProcessContext'; /** * @test {AsyncProcessPool} */ describe('AsyncProcessPool', () => { let options: AsyncProcessPool.Options<ProcessMock> = { processFailoverThrottleDelayInMs: undefined, dependencies: [] }; let sandbox = sinon.createSandbox(); let pool: RootProcessPool<ProcessMock>; let logger = log4js.getLogger('test'); before(() => { log4js.configure(helpers.assembleLog4jsConfig({ levels: { 'AsyncProcessPool': 'TRACE' } })); }); beforeEach(() => { pool = new RootProcessPool(ProcessMock, options); }); afterEach(async () => { await pool.stop(); sandbox.restore(); }); /** * @test {AsyncProcessPool#scheduleProcess} */ describe('scheduleProcess', () => { /** * @test {AsyncProcessPool#scheduleProcess} */ it('should schedule processes', async () => { let process1 = stubProcess(); let process2 = stubProcess(); pool.scheduleProcess('test1', {args: [process1]}); pool.scheduleProcess('test2', {args: [process2]}); await helpers.waitPass(() => { sinon.assert.callCount(process1.start, 1); sinon.assert.callCount(process2.start, 1); sinon.assert.callCount(process1.run, 1); sinon.assert.callCount(process2.run, 1); }); await helpers.delay(25); sinon.assert.callCount(process1.stop, 0); sinon.assert.callCount(process2.stop, 0); }); /** * @test {AsyncProcessPool#scheduleProcess} */ it('should not schedule same process again if it has already scheduled', async () => { let process1 = stubProcess(); let process2 = stubProcess(); pool.scheduleProcess('test', {args: [process1]}); await helpers.waitPass(() => { sinon.assert.callCount(process1.start, 1); sinon.assert.callCount(process1.run, 1); }); pool.scheduleProcess('test', {args: [process2]}); await helpers.delay(25); sinon.assert.callCount(process2.start, 0); sinon.assert.callCount(process2.run, 0); }); /** * @test {AsyncProcessPool#scheduleProcess} */ it('should schedule same process again if previous one has canceled', async () => { let process1 = stubProcess(); let process2 = stubProcess(); pool.scheduleProcess('test', {args: [process1]}); await helpers.waitPass(() => { sinon.assert.callCount(process1.start, 1); sinon.assert.callCount(process1.run, 1); }); pool.cancelProcess('test'); pool.scheduleProcess('test', {args: [process2]}); await helpers.waitPass(() => { sinon.assert.callCount(process1.stop, 1); sinon.assert.callCount(process2.start, 1); sinon.assert.callCount(process2.run, 1); }); }); /** * @test {AsyncProcessPool#scheduleProcess} */ it('should throw error if stopped (by default)', async () => { let process1 = stubProcess(); pool.stop(); try { pool.scheduleProcess('test', {args: [process1]}); throw new Error('assert'); } catch (err) { logger.info(err); err.message.should.not.equal('assert'); } await helpers.delay(25); sinon.assert.notCalled(process1.start); sinon.assert.notCalled(process1.run); sinon.assert.notCalled(process1.stop); }); /** * @test {AsyncProcessPool#scheduleProcess} */ it('should not throw error if stopped if the throwIfStopped option is disabled', async () => { pool.stop(); pool.scheduleProcess('test', {args: [stubProcess()], throwIfStopped: false}); pool.getScheduledIds().should.deepEqual([]); }); /** * @test {AsyncProcessPool#scheduleProcess} */ it('should cancel process which is failing to be constructed', async () => { let localPool = new RootProcessPool(ProcessFailingToConstruct, {dependencies: []}); localPool.scheduleProcess('test', {args: []}); localPool.getScheduledIds().should.deepEqual([]); }); }); /** * @test {AsyncProcessPool#restartProcess} */ describe('restartProcess', () => { /** * @test {AsyncProcessPool#restartProcess} */ it('should gracefully restart process which is starting', async () => { let process = stubProcess(); pool.scheduleProcess('test', {args: [process]}); pool.restartProcess('test'); await helpers.waitPass(() => { assert.callOrder([ {spy: process.start, call: 0}, {spy: process.stop, call: 0}, {spy: process.start, call: 1}, {spy: process.run, call: 0} ]); }); }); /** * @test {AsyncProcessPool#restartProcess} */ it('should gracefully restart process which is running', async () => { let process = stubProcess(); pool.scheduleProcess('test', {args: [process]}); await helpers.waitPass(() => sinon.assert.callCount(process.run, 1)); pool.restartProcess('test'); await helpers.waitPass(() => { assert.callOrder([ {spy: process.start, call: 0}, {spy: process.run, call: 0}, {spy: process.stop, call: 0}, {spy: process.start, call: 1}, {spy: process.run, call: 1} ]); }); }); /** * @test {AsyncProcessPool#restartProcess} */ it('should not restart process again which is already going to be restarted', async () => { let process = stubProcess(); pool.scheduleProcess('test', {args: [process]}); await helpers.waitPass(() => sinon.assert.callCount(process.run, 1)); pool.restartProcess('test'); pool.restartProcess('test'); await helpers.delay(25); sinon.assert.callCount(process.start, 2); }); /** * @test {AsyncProcessPool#restartProcess} */ it('should force restart if process scheduled to be failover', async () => { let clock = sandbox.useFakeTimers(); let process = stubProcess(); let endPromise = helpers.createHandlePromise<void>(); process.run.returns(endPromise); pool.scheduleProcess('test', {args: [process]}); await helpers.waitPass(() => sinon.assert.callCount(process.run, 1), 25, {ignoreSinonClock: true}); process.run.returnsArg(0); endPromise.reject(new Error('test')); await clock.tickAsync(1000 * 5); pool.restartProcess('test'); await helpers.waitPass(() => { sinon.assert.callCount(process.start, 2); sinon.assert.callCount(process.run, 2); sinon.assert.callCount(process.stop, 1); }, 25, {ignoreSinonClock: true}); await clock.tickAsync(1000 * 15); sinon.assert.callCount(process.start, 2); sinon.assert.callCount(process.run, 2); sinon.assert.callCount(process.stop, 1); }); /** * @test {AsyncProcessPool#restartProcess} */ it('should restart process by instance', async () => { let process = stubProcess(); pool.scheduleProcess('test', {args: [process]}); let actualProcess = pool.getProcess('test'); await helpers.waitPass(() => sinon.assert.callCount(process.run, 1)); pool.restartProcess(actualProcess); pool.restartProcess(actualProcess); await helpers.waitPass(() => { assert.callOrder([ {spy: process.start, call: 0}, {spy: process.run, call: 0}, {spy: process.stop, call: 0}, {spy: process.start, call: 1}, {spy: process.run, call: 1} ]); }); }); /** * @test {AsyncProcessPool#restartProcess} */ it('should not restart process if given process instance is not actual', async () => { let process1 = stubProcess(); let process2 = stubProcess(); pool.scheduleProcess('test', {args: [process1]}); let actualProcess1 = pool.getProcess('test'); pool.cancelProcess('test'); pool.scheduleProcess('test', {args: [process2]}); await helpers.waitPass(() => sinon.assert.callCount(process2.run, 1)); pool.restartProcess(actualProcess1); await helpers.delay(25); sinon.assert.notCalled(process2.stop); }); }); /** * @test {AsyncProcessPool#cancelProcess} */ describe('cancelProcess', () => { /** * @test {AsyncProcessPool#cancelProcess} */ it('should cancel not scheduled process', async () => { await pool.cancelProcess('test'); }); /** * @test {AsyncProcessPool#cancelProcess} */ it('should cancel starting process', async () => { let process = stubProcess(); process.start.callsFake(stopPromise => stopPromise); pool.scheduleProcess('test', {args: [process]}); await helpers.delay(25); sinon.assert.callCount(process.start, 1); sinon.assert.callCount(process.run, 0); sinon.assert.callCount(process.stop, 0); await pool.cancelProcess('test'); sinon.assert.callCount(process.start, 1); sinon.assert.callCount(process.run, 0); sinon.assert.callCount(process.stop, 1); }); /** * @test {AsyncProcessPool#cancelProcess} */ it('should wait till process stopped at start stage', async () => { let process = stubProcess(); let startPromise = helpers.createHandlePromise<void>(); process.start.callsFake(() => startPromise); pool.scheduleProcess('test', {args: [process]}); await helpers.delay(25); sinon.assert.callCount(process.start, 1); sinon.assert.callCount(process.run, 0); sinon.assert.callCount(process.stop, 0); let cancelPromise = pool.cancelProcess('test'); await helpers.delay(25); sinon.assert.callCount(process.start, 1); sinon.assert.callCount(process.run, 0); sinon.assert.callCount(process.stop, 0); startPromise.resolve(); await cancelPromise; sinon.assert.callCount(process.start, 1); sinon.assert.callCount(process.run, 0); sinon.assert.callCount(process.stop, 1); }); /** * @test {AsyncProcessPool#cancelProcess} */ it('should cancel running process', async () => { let process = stubProcess(); pool.scheduleProcess('test', {args: [process]}); await helpers.delay(25); sinon.assert.callCount(process.start, 1); sinon.assert.callCount(process.run, 1); sinon.assert.callCount(process.stop, 0); await pool.cancelProcess('test'); sinon.assert.callCount(process.start, 1); sinon.assert.callCount(process.run, 1); sinon.assert.callCount(process.stop, 1); }); /** * @test {AsyncProcessPool#cancelProcess} */ it('should wait till process stopped at run stage', async () => { let process = stubProcess(); let runPromise = helpers.createHandlePromise<void>(); process.run.callsFake(() => runPromise); pool.scheduleProcess('test', {args: [process]}); await helpers.delay(25); sinon.assert.callCount(process.start, 1); sinon.assert.callCount(process.run, 1); sinon.assert.callCount(process.stop, 0); let cancelPromise = pool.cancelProcess('test'); await helpers.delay(25); sinon.assert.callCount(process.start, 1); sinon.assert.callCount(process.run, 1); sinon.assert.callCount(process.stop, 0); runPromise.resolve(); await cancelPromise; sinon.assert.callCount(process.start, 1); sinon.assert.callCount(process.run, 1); sinon.assert.callCount(process.stop, 1); }); /** * @test {AsyncProcessPool#cancelProcess} */ it('should cancel process scheduled to restart', async () => { let process = stubProcess(); pool.scheduleProcess('test', {args: [process]}); await helpers.waitPass(() => { sinon.assert.callCount(process.run, 1); }); pool.restartProcess('test'); await pool.cancelProcess('test'); await helpers.delay(25); sinon.assert.callCount(process.start, 1); sinon.assert.callCount(process.run, 1); sinon.assert.callCount(process.stop, 1); }); /** * @test {AsyncProcessPool#cancelProcess} */ it('should wait till process stopped at stop stage', async () => { let process = stubProcess(); let stopPromise = helpers.createHandlePromise<void>(); process.stop.callsFake(() => stopPromise); pool.scheduleProcess('test', {args: [process]}); await helpers.waitPass(() => sinon.assert.callCount(process.run, 1)); let cancelPromise = helpers.wrapHandlePromise(pool.cancelProcess('test')); await helpers.delay(25); sinon.assert.callCount(process.start, 1); sinon.assert.callCount(process.run, 1); sinon.assert.callCount(process.stop, 1); cancelPromise.completed.should.be.false(); stopPromise.resolve(); await cancelPromise; }); /** * @test {AsyncProcessPool#cancelProcess} */ it('should not cancel process until all usages are canceled', async () => { let process1 = stubProcess(); let process2 = stubProcess(); pool.scheduleProcess('test1', {args: [process1], usage: 'usage1'}); pool.scheduleProcess('test1', {args: [process1], usage: 'usage2'}); pool.scheduleProcess('test2', {args: [process2], usage: 'usage1'}); await pool.waitProcess('test1'); await pool.waitProcess('test2'); await pool.cancelProcess('test1'); await pool.cancelProcess('test1', {usage: 'usage1'}); sinon.assert.callCount(process1.stop, 0); await pool.cancelProcess('test1', {usage: 'usage2'}); sinon.assert.callCount(process1.stop, 1); }); /** * @test {AsyncProcessPool#cancelProcess} */ it('should cancel process by all usages with one corresponding option', async () => { let process1 = stubProcess(); pool.scheduleProcess('test1', {args: [process1], usage: 'usage1'}); pool.scheduleProcess('test1', {args: [process1], usage: 'usage2'}); await pool.waitProcess('test1'); await pool.cancelProcess('test1', {allUsages: true}); sinon.assert.callCount(process1.stop, 1); }); }); /** * @test {AsyncProcessPool#getScheduledIds} */ describe('getScheduledIds', () => { /** * @test {AsyncProcessPool#getScheduledIds} */ it('should return scheduled process IDs', () => { pool.scheduleProcess('test1', {args: [stubProcess()]}); pool.scheduleProcess('test2', {args: [stubProcess()]}); pool.scheduleProcess('test3', {args: [stubProcess()]}); pool.cancelProcess('test2'); pool.restartProcess('test3'); pool.getScheduledIds().should.deepEqual(['test1', 'test3']); }); }); /** * @test {AsyncProcessPool#hasScheduled} */ describe('hasScheduled', () => { /** * @test {AsyncProcessPool#hasScheduled} */ it('should return whether a process has been sheduled', () => { pool.scheduleProcess('test1', {args: [stubProcess()]}); pool.scheduleProcess('test2', {args: [stubProcess()]}); pool.scheduleProcess('test3', {args: [stubProcess()]}); pool.cancelProcess('test2'); pool.restartProcess('test3'); pool.hasScheduled('test1').should.be.true(); pool.hasScheduled('test2').should.be.false(); pool.hasScheduled('test3').should.be.true(); pool.hasScheduled('test4').should.be.false(); }); }); /** * @test {AsyncProcessPool#hasScheduledBy} */ describe('hasScheduledBy', () => { /** * @test {AsyncProcessPool#hasScheduledBy} */ it('should return whether process has scheduled by specific usage ID', async () => { let process1 = stubProcess(); pool.scheduleProcess('test1', {args: [process1], usage: 'usage1'}); pool.scheduleProcess('test1', {args: [process1], usage: 'usage2'}); pool.hasScheduledBy('wrong', 'usage1').should.be.false(); pool.hasScheduledBy('test1', 'usage1').should.be.true(); pool.hasScheduledBy('test1', 'usage2').should.be.true(); pool.hasScheduledBy('test1', 'usage3').should.be.false(); }); }); /** * @test {AsyncProcessPool#getScheduledBy} */ describe('getScheduledBy', () => { /** * @test {AsyncProcessPool#hasScheduledBy} */ it('should return process IDs scheduled by specific usage ID', async () => { let process1 = stubProcess(); let process2 = stubProcess(); pool.scheduleProcess('test1', {args: [process1], usage: 'usage1'}); pool.scheduleProcess('test1', {args: [process1], usage: 'usage2'}); pool.scheduleProcess('test2', {args: [process2], usage: 'usage2'}); pool.getScheduledBy('usage1').should.deepEqual(['test1']); pool.getScheduledBy('usage2').should.deepEqual(['test1', 'test2']); pool.getScheduledBy('usage3').should.deepEqual([]); }); }); /** * @test {AsyncProcessPool#waitProcess} */ describe('waitProcess', () => { /** * @test {AsyncProcessPool#waitProcess} */ it('should wait till process will be started', async () => { let process = stubProcess(); let startPromise = helpers.createHandlePromise<void>(); process.start.returns(startPromise); pool.scheduleProcess('test', {args: [process]}); let waitPromise = helpers.wrapHandlePromise(pool.waitProcess('test')); await helpers.delay(25); waitPromise.completed.should.be.false(); startPromise.resolve(); should((await waitPromise).process).equal(process); }); /** * @test {AsyncProcessPool#waitProcess} */ it('should return undefined if not scheduled', async () => { should(await pool.waitProcess('test')).be.undefined(); }); /** * @test {AsyncProcessPool#waitProcess} */ it('should throw if not scheduled if corresponding option enabled', async () => { await pool.waitProcess('test', {throwIfNotScheduled: true}).should.be.rejectedWith(Error); }); /** * @test {AsyncProcessPool#waitProcess} */ it('should return undefined if canceled during waiting', async () => { let process = stubProcess(); process.start.returnsArg(0); pool.scheduleProcess('test', {args: [process]}); let waitPromise = helpers.wrapHandlePromise(pool.waitProcess('test')); await helpers.delay(25); waitPromise.completed.should.be.false(); pool.cancelProcess('test'); should(await waitPromise).be.undefined(); }); /** * @test {AsyncProcessPool#waitProcess} */ it('should throw error if canceled during waiting if corresponding option enabled', async () => { let process = stubProcess(); process.start.returnsArg(0); pool.scheduleProcess('test', {args: [process]}); let waitPromise = helpers.wrapHandlePromise(pool.waitProcess('test', {throwIfNotScheduled: true})); await helpers.delay(25); waitPromise.completed.should.be.false(); pool.cancelProcess('test'); await waitPromise.should.be.rejectedWith(Error); }); /** * @test {AsyncProcessPool#waitProcess} */ it('should not throw error if process canceled and scheduled again during waiting', async () => { let process1 = stubProcess(); process1.start.callsFake(stopPromise => stopPromise); pool.scheduleProcess('test', {args: [process1]}); let waitPromise = helpers.wrapHandlePromise(pool.waitProcess('test')); await helpers.delay(25); waitPromise.completed.should.be.false(); let process2 = stubProcess(); pool.cancelProcess('test'); pool.scheduleProcess('test', {args: [process2]}); should((await waitPromise).process).equal(process2); }); /** * @test {AsyncProcessPool#waitProcess} */ it('should wait till process will be failovered if current process is in stopped state after error', async () => { let process = stubProcess(); let startPromise = helpers.createHandlePromise<void>(); process.start.returns(startPromise); pool.scheduleProcess('test', {args: [process]}); let waitPromise = helpers.wrapHandlePromise(pool.waitProcess('test')); await helpers.delay(25); waitPromise.completed.should.be.false(); sandbox.stub(options, 'processFailoverThrottleDelayInMs').value(50); startPromise.reject(new Error('test')); await helpers.delay(25); waitPromise.completed.should.be.false(); process.start.resolves(); should((await waitPromise).process).equal(process); }); /** * @test {AsyncProcessPool#waitProcess} */ it('should not return started process which was scheduled to restart immediately right after start', async () => { let process1 = stubProcess(); let startPromise = helpers.createHandlePromise<void>(); process1.start.returns(startPromise); pool.scheduleProcess('test', {args: [process1]}); let waitPromise = helpers.wrapHandlePromise(pool.waitProcess('test')); await helpers.delay(25); waitPromise.completed.should.be.false(); let process2 = stubProcess(); startPromise.resolve(); pool.cancelProcess('test'); pool.scheduleProcess('test', {args: [process2]}); should((await waitPromise).process).equal(process2); }); /** * @test {AsyncProcessPool#waitProcess} */ it('should return with existing process if it is already started successfully', async () => { let process = stubProcess(); pool.scheduleProcess('test', {args: [process]}); await helpers.delay(25); sinon.assert.callCount(process.start, 1); let waitPromise = helpers.wrapHandlePromise(pool.waitProcess('test')); await helpers.delay(25); waitPromise.completed.should.be.true(); waitPromise.result.process.should.equal(process); }); /** * @test {AsyncProcessPool#waitProcess} */ it('should wait if current process is failed to start and waiting to be restarted', async () => { sandbox.stub(options, 'processFailoverThrottleDelayInMs').value(0); let process = stubProcess(); let stopPromise = helpers.createHandlePromise<void>(); process.start .onCall(0).rejects(new Error('test')) .onCall(1).resolves(); process.stop.returns(stopPromise); pool.scheduleProcess('test', {args: [process]}); await helpers.delay(25); sinon.assert.callCount(process.start, 1); sinon.assert.callCount(process.stop, 1); let waitPromise = helpers.wrapHandlePromise(pool.waitProcess('test')); await helpers.delay(25); waitPromise.completed.should.be.false(); stopPromise.resolve(); await waitPromise; }); /** * @test {AsyncProcessPool#waitProcess} */ it('should wait for new process if current one is still actively running but scheduled to cancel', async () => { let process1 = stubProcess(); let process2 = stubProcess(); pool.scheduleProcess('test', {args: [process1]}); pool.cancelProcess('test'); pool.scheduleProcess('test', {args: [process2]}); (await pool.waitProcess('test')).process.should.equal(process2); }); /** * @test {ChildScheduler#waitProcess} */ it('should time out waiting for the process', async () => { let process = stubProcess(); process.start.returnsArg(0); pool.scheduleProcess('test', {args: [process]}); should(await pool.waitProcess('test', {timeoutInMs: 25})).be.undefined(); }); /** * @test {ChildScheduler#waitProcess} */ it('should throw TimeoutError on timeout if corresponding option enabled', async () => { let process = stubProcess(); process.start.returnsArg(0); pool.scheduleProcess('test', {args: [process]}); await pool.waitProcess('test', { timeoutInMs: 25, throwOnTimeout: true }).should.be.rejectedWith(errors.TimeoutError); }); /** * @test {ChildScheduler#waitProcess} */ it('should wait until given stop promise resolves', async () => { let process = stubProcess(); process.start.returnsArg(0); pool.scheduleProcess('test', {args: [process]}); should(await pool.waitProcess('test', { stopPromise: helpers.delay(25) })).be.undefined(); }); /** * @test {ChildScheduler#waitProcess} */ it('should reject with given stop promise error', async () => { let process = stubProcess(); process.start.returnsArg(0); pool.scheduleProcess('test', {args: [process]}); await pool.waitProcess('test', { stopPromise: Promise.reject(new Error('test')) }).should.be.rejectedWith('test'); }); }); /** * @test {AsyncProcessPool#stop} */ describe('stop', () => { /** * @test {AsyncProcessPool#stop} */ it('should cancel all scheduled processes and wait till they are stopped', async () => { let process1 = stubProcess(); let process2 = stubProcess(); let stopPromise = helpers.createHandlePromise<void>(); process1.stop.returns(stopPromise); process2.stop.returns(stopPromise); pool.scheduleProcess('test1', {args: [process1]}); pool.scheduleProcess('test2', {args: [process2]}); let promise = helpers.wrapHandlePromise(pool.stop()); await helpers.delay(25); sinon.assert.callCount(process1.stop, 1); sinon.assert.callCount(process2.stop, 1); promise.completed.should.be.false(); stopPromise.resolve(); await promise; }); /** * @test {AsyncProcessPool#stop} */ it('should wait for all still running but already canceled processes stopped', async () => { let process1 = stubProcess(); let process2 = stubProcess(); let stopPromise = helpers.createHandlePromise<void>(); process1.stop.returns(stopPromise); process2.stop.returns(stopPromise); pool.scheduleProcess('test1', {args: [process1]}); pool.scheduleProcess('test2', {args: [process2]}); pool.cancelProcess('test1'); pool.cancelProcess('test2'); let promise = helpers.wrapHandlePromise(pool.stop()); await helpers.delay(25); sinon.assert.callCount(process1.stop, 1); sinon.assert.callCount(process2.stop, 1); promise.completed.should.be.false(); stopPromise.resolve(); await promise; }); }); /** * @test {AsyncProcessPool} */ describe('common', () => { /** * @test {AsyncProcessPool} */ it('should not run process if it was canceled when it was starting', async () => { let process = stubProcess(); pool.scheduleProcess('test', {args: [process]}); pool.cancelProcess('test'); await helpers.waitPass(() => { sinon.assert.callCount(process.start, 1); sinon.assert.callCount(process.run, 0); sinon.assert.callCount(process.stop, 1); }); }); /** * @test {AsyncProcessPool} */ it('should failover process with a delay if it was stopped unexpectedly', async () => { let clock = sandbox.useFakeTimers(); let process = stubProcess(); let endPromise = helpers.createHandlePromise<void>(); process.run.returns(endPromise); pool.scheduleProcess('test', {args: [process]}); await helpers.waitPass(() => sinon.assert.callCount(process.run, 1), 25, {ignoreSinonClock: true}); process.run.returnsArg(0); endPromise.resolve(); await clock.tickAsync(1000 * 9); sinon.assert.callCount(process.run, 1); sinon.assert.callCount(process.stop, 1); await clock.tickAsync(1000 * 2); sinon.assert.callCount(process.start, 2); sinon.assert.callCount(process.run, 2); sinon.assert.callCount(process.stop, 1); }); /** * @test {AsyncProcessPool} */ it('should stop waiting for failover after unexpected stop if canceled', async () => { let clock = sandbox.useFakeTimers(); let process = stubProcess(); let endPromise = helpers.createHandlePromise<void>(); process.run.returns(endPromise); pool.scheduleProcess('test', {args: [process]}); await helpers.waitPass(() => sinon.assert.callCount(process.run, 1), 25, {ignoreSinonClock: true}); process.run.returnsArg(0); endPromise.resolve(); await clock.tickAsync(1000 * 5); await pool.cancelProcess('test'); await clock.tickAsync(1000 * 15); sinon.assert.callCount(process.start, 1); sinon.assert.callCount(process.run, 1); sinon.assert.callCount(process.stop, 1); }); /** * @test {AsyncProcessPool} */ it('should failover process with a delay if start failed with an error', async () => { let clock = sandbox.useFakeTimers(); let process = stubProcess(); let startPromise = helpers.createHandlePromise<void>(); process.start.returns(startPromise); pool.scheduleProcess('test', {args: [process]}); sinon.assert.callCount(process.start, 1); process.start.resolves(); startPromise.reject(new Error('test')); await clock.tickAsync(1000 * 9); sinon.assert.callCount(process.run, 0); sinon.assert.callCount(process.stop, 1); await clock.tickAsync(1000 * 2); sinon.assert.callCount(process.start, 2); sinon.assert.callCount(process.run, 1); sinon.assert.callCount(process.stop, 1); }); /** * @test {AsyncProcessPool} */ it('should failover process with a delay if run failed with an error', async () => { let clock = sandbox.useFakeTimers(); let process = stubProcess(); let endPromise = helpers.createHandlePromise<void>(); process.run.returns(endPromise); pool.scheduleProcess('test', {args: [process]}); await helpers.waitPass(() => sinon.assert.callCount(process.run, 1), 25, {ignoreSinonClock: true}); process.run.returnsArg(0); endPromise.reject(new Error('test')); await clock.tickAsync(1000 * 9); sinon.assert.callCount(process.run, 1); sinon.assert.callCount(process.stop, 1); await clock.tickAsync(1000 * 2); sinon.assert.callCount(process.start, 2); sinon.assert.callCount(process.run, 2); sinon.assert.callCount(process.stop, 1); }); /** * @test {AsyncProcessPool} */ it('should stop waiting for failover after error if canceled', async () => { let clock = sandbox.useFakeTimers(); let process = stubProcess(); let endPromise = helpers.createHandlePromise<void>(); process.run.returns(endPromise); pool.scheduleProcess('test', {args: [process]}); await helpers.waitPass(() => sinon.assert.callCount(process.run, 1), 25, {ignoreSinonClock: true}); process.run.returnsArg(0); endPromise.reject(new Error('test')); await clock.tickAsync(1000 * 5); await pool.cancelProcess('test'); await clock.tickAsync(1000 * 15); sinon.assert.callCount(process.start, 1); sinon.assert.callCount(process.run, 1); sinon.assert.callCount(process.stop, 1); }); }); /** * @test {ControlSignal} */ describe('ControlSignal', () => { /** * @test {ControlSignal} */ it('should cancel the process if it throws a cancel signal', async () => { let process1 = stubProcess(); process1.run.throws(new ControlSignal({action: 'cancel'})); pool.scheduleProcess('test1', {args: [process1]}); await helpers.waitTrue(() => !pool.hasScheduled('test1')); sinon.assert.callCount(process1.start, 1); sinon.assert.callCount(process1.run, 1); sinon.assert.callCount(process1.stop, 1); }); /** * @test {ControlSignal} */ it('should failover the process if it throws a failover signal', async () => { sandbox.stub(options, 'processFailoverThrottleDelayInMs').value(1000 * 30); let clock = sandbox.useFakeTimers({now: new Date(), shouldAdvanceTime: false}); let process1 = stubProcess(); process1.run .onCall(0).throws(new ControlSignal({action: 'failover'})) .onCall(1).returnsArg(0); pool.scheduleProcess('test1', {args: [process1]}); await helpers.waitPass(() => { sinon.assert.callCount(process1.start, 1); sinon.assert.callCount(process1.run, 1); sinon.assert.callCount(process1.stop, 1); pool.hasScheduled('test1').should.be.true(); }, 25, {ignoreSinonClock: true}); await clock.tickAsync(1000 * 29); await helpers.delay(25, {ignoreSinonClock: true}); sinon.assert.callCount(process1.start, 1); await clock.tickAsync(1000 * 2); await helpers.waitPass(() => { sinon.assert.callCount(process1.start, 2); sinon.assert.callCount(process1.run, 2); sinon.assert.callCount(process1.stop, 1); pool.hasScheduled('test1').should.be.true(); }, 25, {ignoreSinonClock: true}); }); /** * @test {ControlSignal} */ it('should not failover the process if it stops being scheduled till the failover', async () => { sandbox.stub(options, 'processFailoverThrottleDelayInMs').value(1000 * 60 * 60 * 24); let process1 = stubProcess(); process1.run.callsFake(async () => { pool.cancelProcess('test1'); throw new ControlSignal({action: 'failover'}); }); pool.scheduleProcess('test1', {args: [process1]}); await helpers.waitPass(() => { sinon.assert.callCount(process1.start, 1); sinon.assert.callCount(process1.run, 1); sinon.assert.callCount(process1.stop, 1); }); await helpers.delay(25); sinon.assert.callCount(process1.start, 1); pool.hasScheduled('test1').should.be.false(); }); /** * @test {ControlSignal} */ it('should restart the process if it throws a stop signal', async () => { sandbox.stub(options, 'processFailoverThrottleDelayInMs').value(1000 * 30); sandbox.useFakeTimers({now: new Date(), shouldAdvanceTime: false}); let process1 = stubProcess(); process1.run .onCall(0).throws(new ControlSignal({action: 'stop'})) .onCall(1).returnsArg(0); pool.scheduleProcess('test1', {args: [process1]}); await helpers.waitPass(() => { sinon.assert.callCount(process1.start, 2); sinon.assert.callCount(process1.run, 2); sinon.assert.callCount(process1.stop, 1); pool.hasScheduled('test1').should.be.true(); }, 25, {ignoreSinonClock: true}); }); /** * @test {ControlSignal} */ it('should not restart the process if it stops being scheduled till the restart', async () => { let process1 = stubProcess(); process1.run.callsFake(async () => { pool.cancelProcess('test1'); throw new ControlSignal({action: 'stop'}); }); pool.scheduleProcess('test1', {args: [process1]}); await helpers.waitPass(() => { sinon.assert.callCount(process1.start, 1); sinon.assert.callCount(process1.run, 1); sinon.assert.callCount(process1.stop, 1); }); await helpers.delay(25); sinon.assert.callCount(process1.start, 1); pool.hasScheduled('test1').should.be.false(); }); }); /** * @test {AsyncProcessPool} */ describe('AsyncProcess.inject', () => { /** * @test {AsyncProcessPool} */ it('should typize and inject process dependencies', async () => { let injectPool = new RootProcessPool(ProcessWithInject, { dependencies: [1, '2'] }); injectPool.scheduleProcess('test', {args: []}); let process = await injectPool.waitProcess('test'); process.dep1.should.equal(1); process.dep2.should.equal('2'); }); }); /** * @test {AsyncProcessPool} */ describe('failoverThrottleDelay', () => { /** * @test {AsyncProcessPool} */ describe('exponential', () => { /** * @test {AsyncProcessPool} */ it('should throttle failing process with increasing delay', async () => { let clock = sandbox.useFakeTimers(); let process = stubProcess(); process.run.rejects(new Error('test')); pool.scheduleProcess('test', { args: [process], failoverThrottleDelay: { mode: 'exponential', minDelayInMs: 1000, maxDelayInMs: 10000, resetDelayInMs: 5000 } }); await helpers.delay(25, {ignoreSinonClock: true}); sinon.assert.callCount(process.run, 1); await clock.tickAsync(900); sinon.assert.callCount(process.run, 1); await clock.tickAsync(101); sinon.assert.callCount(process.run, 2); await clock.tickAsync(1900); sinon.assert.callCount(process.run, 2); await clock.tickAsync(101); sinon.assert.callCount(process.run, 3); await clock.tickAsync(3900); sinon.assert.callCount(process.run, 3); await clock.tickAsync(101); sinon.assert.callCount(process.run, 4); await clock.tickAsync(7900); sinon.assert.callCount(process.run, 4); await clock.tickAsync(101); sinon.assert.callCount(process.run, 5); await clock.tickAsync(9900); sinon.assert.callCount(process.run, 5); await clock.tickAsync(101); sinon.assert.callCount(process.run, 6); await clock.tickAsync(9900); sinon.assert.callCount(process.run, 6); await clock.tickAsync(101); sinon.assert.callCount(process.run, 7); }); /** * @test {AsyncProcessPool} */ it('should reset throttling delay if enough time passed since last throttling', async () => { let clock = sandbox.useFakeTimers(); let process = stubProcess(); process.run.rejects(new Error('test')); pool.scheduleProcess('test', { args: [process], failoverThrottleDelay: { mode: 'exponential', minDelayInMs: 1000, maxDelayInMs: 10000, resetDelayInMs: 5000 } }); await helpers.delay(25, {ignoreSinonClock: true}); sinon.assert.callCount(process.run, 1); await clock.tickAsync(900); sinon.assert.callCount(process.run, 1); await clock.tickAsync(101); sinon.assert.callCount(process.run, 2); await clock.tickAsync(1900); sinon.assert.callCount(process.run, 2); await clock.tickAsync(101); sinon.assert.callCount(process.run, 3); let promise1 = helpers.createHandlePromise<void>(); process.run.returns(promise1); await clock.tickAsync(3900); sinon.assert.callCount(process.run, 3); await clock.tickAsync(101); sinon.assert.callCount(process.run, 4); clock.tick(5100); process.run.rejects(new Error('test')); promise1.reject(new Error('test')); await helpers.delay(25, {ignoreSinonClock: true}); sinon.assert.callCount(process.run, 4); await clock.tickAsync(900); sinon.assert.callCount(process.run, 4); await clock.tickAsync(101); sinon.assert.callCount(process.run, 5); await clock.tickAsync(1900); sinon.assert.callCount(process.run, 5); await clock.tickAsync(101); sinon.assert.callCount(process.run, 6); }); /** * @test {AsyncProcessPool} */ it('should not reset throttling if reconnecting process on same schedulement', async () => { let clock = sandbox.useFakeTimers(); let process = stubProcess(); process.run.rejects(new Error('test')); pool.scheduleProcess('test', { args: [process], failoverThrottleDelay: { mode: 'exponential', minDelayInMs: 1000, maxDelayInMs: 10000, resetDelayInMs: 5000 } }); await helpers.delay(25, {ignoreSinonClock: true}); sinon.assert.callCount(process.run, 1); await clock.tickAsync(900); sinon.assert.callCount(process.run, 1); await clock.tickAsync(101); sinon.assert.callCount(process.run, 2); await clock.tickAsync(1900); sinon.assert.callCount(process.run, 2); await clock.tickAsync(101); sinon.assert.callCount(process.run, 3); await clock.tickAsync(3900); sinon.assert.callCount(process.run, 3); await clock.tickAsync(101); sinon.assert.callCount(process.run, 4); pool.restartProcess('test'); await helpers.delay(25, {ignoreSinonClock: true}); sinon.assert.callCount(process.run, 5); await clock.tickAsync(7900); sinon.assert.callCount(process.run, 5); await clock.tickAsync(101); sinon.assert.callCount(process.run, 6); await clock.tickAsync(9900); sinon.assert.callCount(process.run, 6); await clock.tickAsync(101); sinon.assert.callCount(process.run, 7); await clock.tickAsync(9900); sinon.assert.callCount(process.run, 7); await clock.tickAsync(101); sinon.assert.callCount(process.run, 8); }); /** * @test {AsyncProcessPool} */ it('should start waiting for throttling delay to reset only after next failed start attempt', async () => { let clock = sandbox.useFakeTimers(); let process = stubProcess(); process.run.rejects(new Error('test')); pool.scheduleProcess('test', { args: [process], failoverThrottleDelay: { mode: 'exponential', minDelayInMs: 1000, maxDelayInMs: 10000, resetDelayInMs: 5000 } }); await helpers.delay(25, {ignoreSinonClock: true}); sinon.assert.callCount(process.start, 1); await clock.tickAsync(900); sinon.assert.callCount(process.start, 1); await clock.tickAsync(101); sinon.assert.callCount(process.start, 2); await clock.tickAsync(1900); sinon.assert.callCount(process.start, 2); await clock.tickAsync(101); sinon.assert.callCount(process.start, 3); let promise1 = helpers.createHandlePromise<void>(); process.start.returns(promise1); await clock.tickAsync(3900); sinon.assert.callCount(process.start, 3); await clock.tickAsync(101); sinon.assert.callCount(process.start, 4); clock.tick(5100); promise1.reject(new Error('test')); await helpers.delay(25, {ignoreSinonClock: true}); sinon.assert.callCount(process.start, 4); await clock.tickAsync(7900); sinon.assert.callCount(process.start, 4); await clock.tickAsync(101); sinon.assert.callCount(process.start, 5); await clock.tickAsync(9900); sinon.assert.callCount(process.start, 5); await clock.tickAsync(101); sinon.assert.callCount(process.start, 6); }); /** * @test {AsyncProcessPool} */ it('should start waiting for throttling delay to reset only after next successful start attempt', async () => { let clock = sandbox.useFakeTimers(); let process = stubProcess(); process.run.rejects(new Error('test')); pool.scheduleProcess('test', { args: [process], failoverThrottleDelay: { mode: 'exponential', minDelayInMs: 1000, maxDelayInMs: 10000, resetDelayInMs: 5000 } }); await helpers.delay(25, {ignoreSinonClock: true}); sinon.assert.callCount(process.start, 1); await clock.tickAsync(900); sinon.assert.callCount(process.start, 1); await clock.tickAsync(101); sinon.assert.callCount(process.start, 2); await clock.tickAsync(1900); sinon.assert.callCount(process.start, 2); await clock.tickAsync(101); sinon.assert.callCount(process.start, 3); let startPromise = helpers.createHandlePromise<void>(); let runPromise = helpers.createHandlePromise<void>(); process.start.returns(startPromise); process.run.returns(runPromise); await clock.tickAsync(3900); sinon.assert.callCount(process.start, 3); await clock.tickAsync(101); sinon.assert.callCount(process.start, 4); clock.tick(5100); startPromise.resolve(); runPromise.reject(new Error('test')); await helpers.delay(25, {ignoreSinonClock: true}); sinon.assert.callCount(process.start, 4); await clock.tickAsync(7900); sinon.assert.callCount(process.start, 4); await clock.tickAsync(101); sinon.assert.callCount(process.start, 5); await clock.tickAsync(9900); sinon.assert.callCount(process.start, 5); await clock.tickAsync(101); sinon.assert.callCount(process.start, 6); }); /** * @test {AsyncProcessPool} */ it('should not reset throttling delay after failed start if reset delay is 0', async () => { let clock = sandbox.useFakeTimers(); let process = stubProcess(); process.start.rejects(new Error('test')); pool.scheduleProcess('test', { args: [process], failoverThrottleDelay: { mode: 'exponential', minDelayInMs: 1000, maxDelayInMs: 10000, resetDelayInMs: 0 } }); await helpers.delay(25, {ignoreSinonClock: true}); sinon.assert.callCount(process.start, 1); await clock.tickAsync(900); sinon.assert.callCount(process.start, 1); await clock.tickAsync(101); sinon.assert.callCount(process.start, 2); await clock.tickAsync(1900); sinon.assert.callCount(process.start, 2); await clock.tickAsync(101); sinon.assert.callCount(process.start, 3); await clock.tickAsync(3900); sinon.assert.callCount(process.start, 3); await clock.tickAsync(101); sinon.assert.callCount(process.start, 4); await clock.tickAsync(7900); sinon.assert.callCount(process.start, 4); await clock.tickAsync(101); sinon.assert.callCount(process.start, 5); await clock.tickAsync(9900); sinon.assert.callCount(process.start, 5); await clock.tickAsync(101); sinon.assert.callCount(process.start, 6); }); }); }); /** * Stubs a process * @returns stubbed async process */ function stubProcess(): sinon.SinonStubbedInstance<RootProcess> { let result = sandbox.createStubInstance(ProcessMock); result.start.resolves(); result.run.callsFake(stopPromise => stopPromise); result.stop.resolves(); return result; } }); /** * This mock will just return given process when constructing to simplify testing */ class ProcessMock extends RootProcess { public process: RootProcess; constructor(context: RootProcessContext) { super(context); } initialize(process: RootProcess): void { this.process = process; } async start(stopPromise: helpers.HandlePromise<void>): Promise<void> { return this.process.start(stopPromise); } async run(stopPromise: helpers.HandlePromise<void>): Promise<void> { return this.process.run(stopPromise); } async stop(): Promise<void> { return this.process.stop(); } } class ProcessFailingToConstruct extends RootProcess { constructor(context: RootProcessContext) { super(context); throw new Error('test'); } async start(stopPromise: helpers.HandlePromise<void>): Promise<void> {} async run(stopPromise: helpers.HandlePromise<void>): Promise<void> {} async stop(): Promise<void> {} } class ProcessWithInject extends RootProcess { public dep1: number; public dep2?: string; inject(dep1: number, dep2?: string): void { this.dep1 = dep1; this.dep2 = dep2; } async start(stopPromise: helpers.HandlePromise<void>): Promise<void> {} async run(stopPromise: helpers.HandlePromise<void>): Promise<void> { return stopPromise; } async stop(): Promise<void> {} }