Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: fixed the error handler & improve stability #448

Merged
merged 11 commits into from
Aug 2, 2022
244 changes: 151 additions & 93 deletions app/__tests__/error-handler.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/* eslint-disable global-require */
// eslint-disable-next-line max-classes-per-file
describe('error-handler', () => {
let config;

Expand Down Expand Up @@ -29,80 +30,136 @@ describe('error-handler', () => {
process.on = jest.fn().mockReturnValue(true);
});

describe('triggers process.on', () => {
beforeEach(() => {
const { runErrorHandler } = require('../error-handler');
runErrorHandler(mockLogger);
});
describe('errorHandlerWrapper', () => {
[
{
label: 'Error -1001',
code: -1001,
sendSlack: false,
featureToggleNotifyDebug: false
},
{
label: 'Error -1021',
code: -1021,
sendSlack: false,
featureToggleNotifyDebug: true
},
{
label: 'Error ECONNRESET',
code: 'ECONNRESET',
sendSlack: false,
featureToggleNotifyDebug: false
},
{
label: 'Error ECONNREFUSED',
code: 'ECONNREFUSED',
sendSlack: false,
featureToggleNotifyDebug: true
},
{
label: 'Error something else - with notify debug',
code: 'something',
sendSlack: true,
featureToggleNotifyDebug: true
},
{
label: 'Error something else - without notify debug',
code: 'something',
sendSlack: true,
featureToggleNotifyDebug: false
}
].forEach(errorInfo => {
describe(`${errorInfo.label}`, () => {
beforeEach(async () => {
config.get = jest.fn(key => {
if (key === 'featureToggle.notifyDebug') {
return errorInfo.featureToggleNotifyDebug;
}
return null;
});

it('with unhandledRejection', () => {
expect(process.on).toHaveBeenCalledWith(
'unhandledRejection',
expect.any(Function)
);
const { errorHandlerWrapper } = require('../error-handler');
await errorHandlerWrapper(mockLogger, 'WhateverJob', () => {
throw new (class CustomError extends Error {
constructor() {
super();
this.code = errorInfo.code;
this.message = `${errorInfo.code}`;
}
})();
});
});

if (errorInfo.sendSlack) {
it('triggers slack.sendMessage', () => {
expect(mockSlack.sendMessage).toHaveBeenCalled();
});
} else {
it('does not trigger slack.sendMessage', () => {
expect(mockSlack.sendMessage).not.toHaveBeenCalled();
});
}
});
});

it('with uncaughtException', () => {
expect(process.on).toHaveBeenCalledWith(
'uncaughtException',
expect.any(Function)
);
describe(`redlock error`, () => {
beforeEach(async () => {
config.get = jest.fn(_key => null);

const { errorHandlerWrapper } = require('../error-handler');
await errorHandlerWrapper(mockLogger, 'WhateverJob', () => {
throw new (class CustomError extends Error {
constructor() {
super();
this.code = 500;
this.message = `redlock:lock-XRPBUSD`;
}
})();
});
});

it('do not trigger slack.sendMessagage', () => {
expect(mockSlack.sendMessage).not.toHaveBeenCalled();
});
});
});

[
{
label: 'Error -1001',
code: -1001,
sendSlack: false,
featureToggleNotifyDebug: false
},
{
label: 'Error -1021',
code: -1021,
sendSlack: false,
featureToggleNotifyDebug: true
},
{
label: 'Error ECONNRESET',
code: 'ECONNRESET',
sendSlack: false,
featureToggleNotifyDebug: false
},
{
label: 'Error ECONNREFUSED',
code: 'ECONNREFUSED',
sendSlack: false,
featureToggleNotifyDebug: true
},
{
label: 'Error something else - with notify debug',
code: 'something',
sendSlack: true,
featureToggleNotifyDebug: true
},
{
label: 'Error something else - without notify debug',
code: 'something',
sendSlack: true,
featureToggleNotifyDebug: false
}
].forEach(errorInfo => {
describe(`${errorInfo.label}`, () => {
describe('runErrorHandler', () => {
describe('triggers process.on', () => {
beforeEach(() => {
const { runErrorHandler } = require('../error-handler');
runErrorHandler(mockLogger);
});

it('with unhandledRejection', () => {
expect(process.on).toHaveBeenCalledWith(
'unhandledRejection',
expect.any(Function)
);
});

it('with uncaughtException', () => {
expect(process.on).toHaveBeenCalledWith(
'uncaughtException',
expect.any(Function)
);
});
});

describe(`when uncaughtException received without notifyDebug`, () => {
beforeEach(async () => {
config.get = jest.fn(key => {
if (key === 'featureToggle.notifyDebug') {
return errorInfo.featureToggleNotifyDebug;
return false;
}
return null;
});

process.on = jest.fn().mockImplementation((event, error) => {
if (event === 'uncaughtException') {
error({
message: errorInfo.label,
code: errorInfo.code,
stack: errorInfo.code
message: `something-unexpected`,
code: 1000
});
}
});
Expand All @@ -111,53 +168,54 @@ describe('error-handler', () => {
runErrorHandler(mockLogger);
});

if (errorInfo.sendSlack) {
it('triggers slack.sendMessage', () => {
expect(mockSlack.sendMessage).toHaveBeenCalled();
});
} else {
it('does not trigger slack.sendMessage', () => {
expect(mockSlack.sendMessage).not.toHaveBeenCalled();
});
}
});
});

describe(`redlock error`, () => {
beforeEach(async () => {
process.on = jest.fn().mockImplementation((event, error) => {
if (event === 'uncaughtException') {
error({
message: `redlock:bot-lock:XRPBUSD`,
code: 500
});
}
it('triggers slack.sendMessage', () => {
expect(mockSlack.sendMessage).toHaveBeenCalled();
});

const { runErrorHandler } = require('../error-handler');
runErrorHandler(mockLogger);
});

it('do not trigger slack.sendMessagage', () => {
expect(mockSlack.sendMessage).not.toHaveBeenCalled();
});
});
describe(`when uncaughtException received with notifyDebug`, () => {
beforeEach(async () => {
config.get = jest.fn(key => {
if (key === 'featureToggle.notifyDebug') {
return true;
}
return null;
});

describe(`any warning received on unhandledRejection`, () => {
it('do not trigger slack.sendMessage', async () => {
expect(() => {
process.on = jest.fn().mockImplementation((event, error) => {
if (event === 'unhandledRejection') {
if (event === 'uncaughtException') {
error({
message: `redlock:bot-lock:XRPBUSD`,
code: 500
message: `something-unexpected`,
code: 1000
});
}
});

const { runErrorHandler } = require('../error-handler');
runErrorHandler(mockLogger);
}).toThrow(`redlock:bot-lock:XRPBUSD`);
});

it('triggers slack.sendMessage', () => {
expect(mockSlack.sendMessage).toHaveBeenCalled();
});
});

describe(`when unhandledRejection received`, () => {
it('throws an error', async () => {
expect(() => {
process.on = jest.fn().mockImplementation((event, error) => {
if (event === 'unhandledRejection') {
error({
message: `something-unhandled`,
code: 2000
});
}
});

const { runErrorHandler } = require('../error-handler');
runErrorHandler(mockLogger);
}).toThrow(`something-unhandled`);
});
});
});
});
2 changes: 1 addition & 1 deletion app/__tests__/server.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ describe('server', () => {
require('../server');
});

it('triggers errorHandler.run', () => {
it('triggers runErrorHandler', () => {
expect(mockRunErrorHandler).toHaveBeenCalled();
});

Expand Down
16 changes: 12 additions & 4 deletions app/binance/__tests__/orders.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
/* eslint-disable global-require */

describe('orders.js', () => {
let binanceMock;
let loggerMock;
Expand All @@ -9,6 +8,7 @@ describe('orders.js', () => {

let mockUpdateGridTradeLastOrder;
let mockGetOpenOrdersFromAPI;
let mockErrorHandlerWrapper;

beforeEach(async () => {
jest.clearAllMocks().resetModules();
Expand All @@ -18,6 +18,16 @@ describe('orders.js', () => {
loggerMock = logger;
cacheMock = cache;
mongoMock = mongo;

mockErrorHandlerWrapper = jest
.fn()
.mockImplementation((_logger, _job, callback) => {
callback();
});

jest.mock('../../error-handler', () => ({
errorHandlerWrapper: mockErrorHandlerWrapper
}));
});

describe('syncOpenOrders', () => {
Expand Down Expand Up @@ -106,9 +116,7 @@ describe('orders.js', () => {

loggerMock.error = jest.fn().mockResolvedValue(true);

mockGetOpenOrdersFromAPI = jest.fn().mockRejectedValue({
error: 'error thrown'
});
mockGetOpenOrdersFromAPI = jest.fn().mockResolvedValue(true);

const spyOnSetInterval = jest.spyOn(global, 'setInterval');
spyOnClearInterval = jest.spyOn(global, 'clearInterval');
Expand Down
11 changes: 11 additions & 0 deletions app/binance/__tests__/tickers.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,20 @@ describe('tickers.js', () => {
let mockExecuteTrailingTrade;

let mockWebsocketTickersClean;
let mockErrorHandlerWrapper;

beforeEach(() => {
jest.clearAllMocks().resetModules();

mockErrorHandlerWrapper = jest
.fn()
.mockImplementation((_logger, _job, callback) =>
Promise.resolve(callback())
);

jest.mock('../../error-handler', () => ({
errorHandlerWrapper: mockErrorHandlerWrapper
}));
});

describe('setupTickersWebsocket', () => {
Expand Down
2 changes: 1 addition & 1 deletion app/binance/candles.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ const setupCandlesWebsocket = async (logger, symbols) => {
};

/**
* Retrieve ATH candles for symbols from Binance API
* Retrieve candles for symbols from Binance API
*
* @param {*} logger
* @param {string[]} symbols
Expand Down
Loading