Proper management of resources is important for a successful program, especially if it runs continuously as, for example, a web service. In languages without garbage collection the developer of the program is responsible for managing the memory. Fortunately, in JavaScript we don’t have to do this but, nevertheless, there are plenty of other resources we have to manage: open files, database connections, client pools, locks etc. In this post, I would like to provide some examples how Bluebird, a promise library, can help you better manage this kind of resources. This post will be especially helpful if you are already using Bluebird but have not used its Resource Management API yet.
Basically, the Resource Management API consists of two additional methods:
.disposer(fn)
which accepts a callback function, in which you can write your logic of cleaning up resources..disposer(fn)
returns a disposer object which can safely used withPromise.using
Promise.using(resource, handler)
which accepts a disposer and defines a handler which utilizes the resource that is wrapped in the disposer. Promise.using and disposers make sure that the resource is disposed in any case.
The advantage of using disposers is that you can define how to clean up resources in the same place where you allocate them. I find this concept similar to the way how constructors and destructors work in many languages. So I have played a bit with this API and created some examples of using it together with some popular libraries.
Make sure to take a look at the official Bluebird docs before diving into the examples: using & disposer.
Temporary Files Example
If you need to work with temporary files, the tmp
module will come in handy:
const Promise = require('bluebird');
const tmp = require('tmp');
const fs = require('fs');
// creation of a temp file and it's clean up come together
// encapsulated in this function
function createTempFile() {
// tmp.file returns more than 2 arguments
return Promise.fromCallback(cb => tmp.file(cb), { multiArgs: true })
// to dispose we call cleanupCallback provided by tmp.file
.disposer(([path, fd, cleanupCallback], promise) => {
cleanupCallback();
})
}
// just to check the file aftewards
let rememberPath = null;
// we obtain the resource with Promise.using
Promise
.using(
createTempFile(),
([path]) => {
console.log('Doing work with the temp file', path)
fs.writeFileSync(path, 'test', 'utf-8')
rememberPath = path
}
)
// whatever happens in the handler, the file will be cleaned up
// by the disposer at this point
.finally(() => {
console.log('Temp file cleaned up:', !fs.existsSync(rememberPath));
})
Postgres Example
Most services written in Node use some sort of connection pooling so it is important to release connections back to the pool. In this example with node-postgres
we use Bluebird to make sure the connections are released back to the pool.
const pg = require('pg');
const Promise = require('bluebird');
const pool = new pg.Pool({
// config params
});
// this function gets a connection from a pool
// and releases it back when not needed
function connect(pool) {
return Promise
.fromCallback(cb => pool.connect(cb), { multiArgs: true })
.disposer(([client, done], promise) => {
done();
});
}
Promise
.using(
connect(pool),
([client, done]) => Promise.fromCallback(cb => client.query('SELECT $1::int AS number', ['1'], cb))
)
.then(result => {
console.log('db client was released to the pool');
console.log('query result', result.rows);
pool.end();
})
Redlock Example
With redlock
module there are two options: we can implement a disposer ourselves or use the disposer provided by the library:
const Promise = require('bluebird');
const client = require('redis').createClient(6379);
const Redlock = require('redlock');
const redlock = new Redlock(
[client],
{
driftFactor: 0.01,
retryCount: 3,
retryDelay: 200
}
);
// custom disposer
function withLock(resource, ttl) {
return redlock
.lock(resource, ttl)
.disposer(lock => lock.unlock());
}
const p1 = Promise
.using(
withLock('test', 10000),
() => console.log('Inside the first locked code section')
)
.finally(() => {
console.log('redlock was released')
})
function unlockErrorHandler(err) {
console.error(err);
}
// disposer provided by the library can be used with Promise.using directly
const p2 = Promise
.using(redlock.disposer('test2', 10000, unlockErrorHandler), function(lock) {
console.log('Inside the second locked code section');
})
.finally(() => {
console.log('redlock was released');
})
Promise.all([ p1, p2 ]).finally(() => client.quit())
I hope the examples come in handy to you and thanks for reading!
P.S. If you are interested in learning more about how the resource management API is implemented internally in Bluebird, here is the relevant module