I hope someone can give me some advice
I have created a module, that basically creates a redis connection using a singleton pattern which also uses promises.
However, before I return the redis connection, I first check if the connection is ready, on the ready event, I resolve the promise and likewise on any errors, I reject the promise.
My only concern is with this method, can I introduce memory leaks as the listeners function on ready and error may continue listening well after the promise has completed and should be cleaned up by the garbage collector.
I am not sure if this will create some kind of memory leaks..
Any advice would be much appreciated.
import redis from 'redis';
import redisearch from 'redis-redisearch';
redisearch(redis);
let redisClient: redis.RedisClient = null;
export function getRedisClient(): Promise {
return new Promise((resolve: any, reject: any) => {
if (redisClient && redisClient.connected) {
return resolve(redisClient);
}
redisClient = redis.createClient({
password: process.env.REDIS_PASSWORD,
retry_strategy: function (options) {
if (options.error && options.error.code === "ECONNREFUSED") {
// End reconnecting on a specific error and flush all commands with
// a individual error
return new Error("The server refused the connection");
}
if (options.total_retry_time > 1000 * 60 * 60) {
// End reconnecting after a specific timeout and flush all commands
// with a individual error
return new Error("Retry time exhausted");
}
if (options.attempt > 10) {
// End reconnecting with built in error
return undefined;
}
// reconnect after
return Math.min(options.attempt * 100, 3000);
},
});
redisClient.on("ready", function (error: any) {
console.log("connection is good");
return resolve(redisClient);
});
redisClient.on("error", function (error: any) {
console.log("reject error");
if (redisClient) {
redisClient.end(false);
redisClient = null;
return reject(error);
}
});
})
}
This pattern can create multiple redisClients if getRedisClient is called multiple times before any of the redisClients finish connecting. A slightly simpler pattern might be to cache a Promise<RedisClient> rather than the redisClient:
let redisClientP: Promise<RedisClient>;
function getRedisClient (): Promise<RedisClient> {
if (redisClientP) return redisClientP;
redisClientP = new Promise(...previous code)
return redisClientP;
}
If you haven't seen this type of cached Promise usage before, here's a small snippet demonstrating that you can access the .then on a Promise multiple times.
const p = new Promise((resolve) => resolve(2));
(async function main () {
for (let i = 0; i < 100; i++) {
console.log(i, await p);
}
})()
Related
I'm trying to test the following "worker" method in a Class without success as it is "an infinite loop"/recursive:
Class TheClassYaDaYaDa {
constructor(var1) {
(...)
}
async worker() {
return new Promise(async (resolve, reject) => {
try {
await this.doStuff();
return resolve();
} catch (err) {
return reject(err);
} finally {
this.worker();
}
});
}
}
And this is the test I'm building:
it('It should not throw error', async function () {
let error = false;
const var1 = 'parameterTest';
stub1 = sinon.stub(TheClassYaDaYaDa.prototype, 'doStuff').resolves();
// if I use the following stub logically the test will not succeed. If I don't I get an infinte loop
// stub2 = sinon.stub(TheClassYaDaYaDa.prototype, 'worker').resolves();
let classToTest;
try {
classToTest = new TheClassYaDaYaDa(limitedInstance);
result = await classToTest.worker();
} catch (err) {
error = err;
}
expect(error).to.be.false;
sinon.assert.calledOnce(stub1);
//also assert that the finally statement run at least once!
});
Is there a way to test this scenario?
I would approach this from a different angle, recursion is always tricky but one of the popular designs is to split the scheduling and running logic into two.
Consider the following:
async worker() {
await this.scheduleWorker();
}
private scheduleWorker() {
// return new Promise ...
}
It's now possible to test that the scheduleWorker function calls worker, and also that worker calls scheduleWorker (if private methods are accessible).
If not, you could mock the worker method to only return the worker logic on the first call and noops on subsequent calls. This would also work for your current design allowing you to assert the recursive call was correctly triggered.
I am not well versed on sinon but you want something similar to:
var originalWorker = classToTest.originalWorker;
classToTest.originalWorker = function() {
originalWorker();
classToTest.originalWorker = function() { }
}
Most mocking frameworks have similar controls to enable mocking methods multiple times (for subsequent calls).
I apologise in advance as I am new to programming and I have been stuck at this for quite some time. I have a connect() function which returns a promise (it is also embedded in a class - not shown). I want this function to retry with a delay if the connection is not establish (i.e. reject is returned) but I have been unable to do so; I tried using the async js library and promise-retry library to no avail - i cant understand the documentation. For clarity, socket.connect emits a 'connect' function if connection is established.
this.socket = new net.Socket();
this.client = new Modbus.client.TCP(this.socket, this.unitID);
const net = require('net');
const Modbus = require('jsmodbus');
connect() {
return new Promise((resolve, reject) => {
this.socket.connect(options);
this.socket.on('connect', () => {
logger.info('*****CONNECTION MADE*****');
//does something once connection made
resolve();
});
this.socket.on('error', (error) => {
logger.error('failed to connect');
this.disconnect();
reject();
});
})
}
First define a utility function for having the delay:
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
Then chain a .catch handler to the new Promise:
.catch(() => delay(1000).then(() => this.connect()));
Of course, you should avoid an infinite series of retries. So implement some logic to definitely give up: after a fixed number of attempts, or after a certain time has passed, ...etc.
For instance, give a parameter to connect how many attempts it should allow:
connect(attempts=3) {
Then the catch handler could be:
.catch((err) => {
if (--attempts <= 0) throw err; // give up
return delay(1000).then(() => this.connect(attempts));
});
I'd do it by making the function that does a single connection attempt (basically, renaming your connect to tryConnect or similar, perhaps even as a private method if you're using a new enough version of Node.js), and then having a function that calls it with the repeat and delay, something like this (see comments):
Utility function:
function delay(ms, value) {
return new Promise(resolve => setTimeout(resolve, ms, value);
}
The new connect:
async connect() {
for (let attempt = 0; attempt < MAX_ATTEMPTS; ++attempt) {
if (attempt > 0) {
// Last attempt failed, wait a moment
await delay(RETRY_DELAY_IN_MS);
}
try {
await tryConnect();
return; // It worked
} catch {
}
}
// Out of retries
throw new Error("Couldn't create connection");
}
(If you're using a slightly older Node.js, you may need to add (e) after the catch above. Leaving it off when you don't need it is a relatively new feature.)
Re your current implementation of what I'm calling tryConnect, here are a few notes as comments for how I'd change it:
tryConnect() {
return new Promise((resolve, reject) => {
// Keep these local for now
const socket = new net.Socket();
const client = new Modbus.client.TCP(socket, this.unitID);
// Add handlers before calling `connect
socket.on('connect', () => {
logger.info('*****CONNECTION MADE*****');
// NOW save these to the instance and resolve the promise
this.socket = socket;
this.client = client;
resolve();
});
socket.on('error', (error) => {
logger.error('failed to connect');
// It's not connected, so no `disconnect` call here
reject();
});
socket.connect(options);
});
}
In the function where connect is called,
It can be called recursively, eg
const outerFunction = (times = 0) => {
if (times < 4) {
socket
.connect(params)
.then(() => {
// do good stuff
})
.catch(e => {
// increment the times so that it wont run forever
times++;
setTimeout(() => {
// delay for two seconds then try conecting again
outerFunction(times);
}, 2000);
});
}
};
This way your connect function is tried three times with a spacing of 2 seconds, i hope this solves your issue
I try to write my simple eventemitter wrapper for amqplib/callback_api. I have trouble with handling sitaution when rabbit is not available or disconnected.
I have method getConnect which returns Promise, which resolves when connection established. But if connection is refused Promise obviously rejects. How to force this method do reconnection while connection will not wstablished
/**
* Async method getConnect for connection
* #returns {Promise<*>}
*/
getConnect = async () => {
return new Promise((resolve, reject) => {
amqp.connect(this.config.url, async function(err, conn) {
if (err) {
reject(err);
}
resolve(conn);
})
})
};
Whole code is here https://github.com/kimonniez/rabbitEE
Maybe, I'm already very sleepy, but I'm completely confused :) Thanks in advance!
Wrap your Promise inside an Observable
Promise is not built to handle "retry" logic. If you want to do that, you should look into Observables using the rxjs library. This will allow you to retry using an arbitrary time interval while catching errors.
const { from, interval, of } = rxjs;
const { catchError, mergeMap, tap, skipWhile, take } = rxjs.operators;
const THRESHOLD = 3;
const RETRY_INTERVAL = 1000;
// Equivalent to 'amqp.connect'
const functionThatThrows = number =>
number < THRESHOLD
? Promise.reject(new Error("ERROR"))
: Promise.resolve("OK");
// Equivalent to `getConnect`
const getConnect = () =>
interval(RETRY_INTERVAL)
.pipe(
mergeMap(x => from(functionThatThrows(x)).pipe(catchError(e => of(e)))),
skipWhile(x => {
const isError = x instanceof Error;
if (isError) console.log('Found error. Retrying...');
return isError;
}),
take(1)
).toPromise();
// Resolve only if the inner Promise is resolved
getConnect().then(console.log);
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.4.0/rxjs.umd.min.js"></script>
Explanation
Create a source with an interval of 1000. Meaning that it will retry each second
Call your amqp.connect which is equivalent to functionThatThrows in my example
Catch the error using the catchError operator and return it
Skip while the returned object is an error. This will allow your to resolve only if your Promise has been resolved and not rejected
Take the first resolved result using take(1)
Convert your observable into a promise using the toPromise utility function
Call your function and attach then like you do with a standard Promise
If you just want to keep tryng connecting until a connection is made, you can wrap the getConnect method into a new keepConnect method:
keepConnect = async () => {
while (true) {
try {
let conn = await getConnect()
return conn
} catch (e) {}
}
}
But I think it would be better to implement something like a "try to connect for n times", by changing the while condition. In general, a "while true" solution is not clean and could perform bad, with the risk to slow down the event loop (imagine if the connect method will always return an error in few milliseconds).
You could also implement a system of progressive delays between connection attempts, using the keepConnect wrapper as idea.
If you instead want to reconnect when the connection is lost, then this is related to Rabbit (that I don't know) and his events.
I have a custom connect function that creates a promise I want resolved once I make a websocket call and receive an acknowledgement. The remote server may be up, it may be down, but if it's unavailable I want to keep trying until I'm successful.
const socketIOClient = require('socket.io-client');
function createTimeoutCallback(callback)
{
let called = false;
let timerID = setTimeout(() => {
if (called) return;
called = true;
callback(new TimeoutError());
},
60*1000);
return function() {
if (called) return;
called = true;
clearTimeout(timerID);
callback.apply(this, arguments);
}
}
async function myConnect()
{
let mysocket = socketIOClient(url);
return new Promise((resolve, reject) => {
mysocket.emit('clientconnect', args, createTimeoutCallback((resp) => {
if (!(resp instanceof TimeoutError)) {
// SUCCESS
doSomething();
resolve();
}
// We timed out, try again
else {
mysocket.close();
setTimeout(myConnect, 60*1000);
}
}));
});
}
await connect();
// doSomething() gets called but we never get here
In the above code, if the endpoint is available, everything works fine. But I'm never returning from the myConnect() function when (1) I wait on its promise; and (2) the function needs to make several connection attempts (e.g., the server is not initially up); and (3) the endpoint finally comes back online and the connection succeeds. I suspect this has everything to do with me essentially abandoning the original promise on a reconnect attempt, but I don't want to reject it or the operation will prematurely fail.
I did find a workaround, which relies on an embedded function. With this technique there is only one promise which is always accessible and in scope, and the timer-based recursion (i.e., not stack-based) is always wrapped in that single promise that is never abandoned. I put this together before Jaromanda answered, so I can't confirm that his solution would work.
async function myConnect()
{
return new Promise((resolve, reject) => {
function innerConnect()
{
let mysocket = socketIOClient(url);
mysocket.emit('clientconnect', args, createTimeoutCallback((resp) => {
if (!(resp instanceof TimeoutError)) {
// SUCCESS
doSomething();
resolve();
}
// We timed out, try again
else {
mysocket.close();
setTimeout(innerConnect, 60*1000);
}
}));
}
innerConnect();
});
}
How can I stop a thrown error from propagating all the way down the chain? It shows in my catch() block but it doesn't stop and crashes the server with an uncaught exception.
I am running this as part of a node cron job (node-cron) as:
var cronJob = require('cron').CronJob;
var cron = require('../lib/cron')
var c = new cronJob('* * * * * *', function() {
console.log('Cron starting');
mycode.run();
}, function() {
console.log('Cron executed');
}, true);
c.start();
In my cron.js
module.exports = {
run: function() {
return job.getAndStore().catch(function(e) {
// This prints but it keeps on going so to speak - it doesn't 'catch', just notifies me
console.log('ERROR', e);
});
}
};
Console dump:
Cron starting
ERROR [TypeError: undefined is not a function]
Cron starting
Uncaught Exception
[TypeError: undefined is not a function]
TypeError: undefined is not a function
I have to do this which I know not quite right:
try {
run();
} catch(e) {
console.log('Now it stops')
}
The run() is part of some cron library that doesn't have any promise support so I am wrapping it in the function to call it.
Edit As I think my issue is related to subsequent calls I believe it has to do with how I handle the Mongo connection on 2+ calls:
// Create a Mongo connection
Job.prototype.getDb = function(id) {
var self = this;
return new P(function(resolve, reject) {
if (!self.db) {
return Mongo.connectAsync(self.options.connection)
.then(function(c) {
self.db = c;
debug('Got new connection');
resolve(c);
});
}
debug('Got existing connection');
resolve(self.db);
});
};
// Fetch stuff
Job.prototype.getAndStore = function(c) {
return this.getDb().then(function() {
throw new Error('Boom');
});
};
Your catch callback is only executed the first time. You are getting the uncaught exception in the second run of the cron job, and it looks like your job.getAndStore() does not return a rejected promise there but throws synchronously. It shouldn't, it should always return a promise.
You can use Bluebirds Promise.try to automatically catch such exceptions and transform them into a promise rejection. Or you wrap your getAndStore function in Promise.method:
var safeGetAndStore = Promise.method(job.getAndStore.bind(job));
module.exports = {
run: function() {
return safeGetAndStore().catch(function(e) {
console.log('ERROR', e);
});
}
};
In your specific case, the problem was that your job did cache the db connection and returned that when it was already available - but you needed to return a promise with a .then method. You should simply cache the promise itself:
Job.prototype.getDb = function(id) {
if (!this.db) {
this.db = Mongo.connectAsync(self.options.connection);
return this.db;
};
Use done, at least if bluebird implements it properly it will work as you expect.
catch(..) is just alias for then(null, ..) which is promise transformer that creates another promise for further processing.
So following should work for you:
module.exports = {
run: function() {
return job.getAndStore().done(null, function(e) {
// This prints but it keeps on going so to speak - it doesn't 'catch', just notifies me
console.log('ERROR', e);
});
}
};