diff --git a/README.md b/README.md index 952b77b..298ee4e 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,8 @@ var dns = require('dns'), dnscache = require('dnscache')({ "enable" : true, "ttl" : 300, - "cachesize" : 1000 + "cachesize" : 1000, + "useStale": false }); //to use the cached dns either of dnscache or dns can be called. @@ -52,6 +53,7 @@ Configuration * `ttl` - ttl for cache-entries. Default: `300` * `cachesize` - number of cache entries, defaults to `1000` * `cache` - If a custom cache needs to be used instead of the supplied cache implementation. Only for Advanced Usage. Custom Cache needs to have same interface for `get` and `set`. + * `useStale` - When cache expires, user can be served stale value and re-fetched in the background, defaults to `false`. Advanced Caching diff --git a/lib/cache.js b/lib/cache.js index 01e6c72..c023b98 100644 --- a/lib/cache.js +++ b/lib/cache.js @@ -8,7 +8,8 @@ var CacheObject = function (conf) { conf = conf || {}; conf.ttl = parseInt(conf.ttl, 10) || 300; //0 is not permissible conf.cachesize = parseInt(conf.cachesize, 10) || 1000; //0 is not permissible - + conf.useStale = conf.useStale || false; + this.ttl = conf.ttl * 1000; this.max = conf.cachesize; @@ -36,7 +37,6 @@ var CacheObject = function (conf) { } self.head.val = value; - self.head.hit = 0; self.head.ts = Date.now(); } else { // key is not exist @@ -44,7 +44,8 @@ var CacheObject = function (conf) { "key" : key, "val" : value, "hit" : 0, - "ts" : Date.now() + "ts" : Date.now(), + "stale": false }; if (!self.head) { @@ -84,7 +85,8 @@ var CacheObject = function (conf) { } next(function () { var value; - if (conf.ttl !== 0 && (Date.now() - self.data[key].ts) >= self.ttl) { + var expired = conf.ttl !== 0 && (Date.now() - self.data[key].ts) >= self.ttl; + if (expired && !conf.useStale) { if (self.data[key].newer) { if (self.data[key].older) { // in the middle of the list @@ -113,7 +115,13 @@ var CacheObject = function (conf) { self.data[key].hit = self.data[key].hit + 1; value = self.data[key].val; } - callback(null, value); + + var staleCallback; + if (expired && conf.useStale && !self.data[key].stale) { + self.data[key].stale = true; + staleCallback = function () { self.data[key].stale = false; }; + } + callback(null, value, staleCallback); }); }; }; diff --git a/lib/index.js b/lib/index.js index bf95626..4530cfe 100644 --- a/lib/index.js +++ b/lib/index.js @@ -14,6 +14,7 @@ var EnhanceDns = function (conf) { conf = conf || {}; conf.ttl = parseInt(conf.ttl, 10) || 300; //0 is not allowed ie it ttl is set to 0, it will take the default conf.cachesize = parseInt(conf.cachesize, 10); //0 is allowed but it will disable the caching + conf.useStale = conf.useStale || false; if (isNaN(conf.cachesize)) { conf.cachesize = 1000; //set default cache size to 1000 records max @@ -71,13 +72,26 @@ var EnhanceDns = function (conf) { } } - cache.get('lookup_' + domain + '_' + family + '_' + hints + '_' + all, function (error, record) { + cache.get('lookup_' + domain + '_' + family + '_' + hints + '_' + all, function (error, record, staleCallback) { if (record) { - /*istanbul ignore next - "all" option require node 4+*/ - if (Array.isArray(record)) { - return callback(error, record); + /*istanbul ignore next - doesn't throw under normal circumstances*/ + try { + /*istanbul ignore next - "all" option require node 4+*/ + if (Array.isArray(record)) { + callback(error, record); + } else { + callback(error, record.address, record.family); + } + } catch (e) { + if (staleCallback) { + staleCallback(); + } + throw e; + } + if (! staleCallback) { + return; } - return callback(error, record.address, record.family); + callback = staleCallback; } try{ @@ -118,9 +132,21 @@ var EnhanceDns = function (conf) { callback_new = type; } - cache.get('resolve_' + domain + '_' + type_new, function (error, record) { + cache.get('resolve_' + domain + '_' + type_new, function (error, record, staleCallback) { if (record) { - return callback_new(error, deepCopy(record), true); + /*istanbul ignore next - doesn't throw under normal circumstances*/ + try { + callback_new(error, deepCopy(record), true); + } catch (e) { + if (staleCallback) { + staleCallback(); + } + throw e; + } + if (! staleCallback) { + return; + } + callback_new = staleCallback; } try { backup_object.resolve(domain, type_new, function (err, addresses) { @@ -140,9 +166,21 @@ var EnhanceDns = function (conf) { // override dns.resolve4 method dns.resolve4 = function (domain, callback) { - cache.get('resolve4_' + domain, function (error, record) { + cache.get('resolve4_' + domain, function (error, record, staleCallback) { if (record) { - return callback(error, deepCopy(record)); + /*istanbul ignore next - doesn't throw under normal circumstances*/ + try { + callback(error, deepCopy(record)); + } catch (e) { + if (staleCallback) { + staleCallback(); + } + throw e; + } + if (! staleCallback) { + return; + } + callback = staleCallback; } try { backup_object.resolve4(domain, function (err, addresses) { @@ -162,9 +200,21 @@ var EnhanceDns = function (conf) { // override dns.resolve6 method dns.resolve6 = function (domain, callback) { - cache.get('resolve6_' + domain, function (error, record) { + cache.get('resolve6_' + domain, function (error, record, staleCallback) { if (record) { - return callback(error, deepCopy(record)); + /*istanbul ignore next - doesn't throw under normal circumstances*/ + try { + callback(error, deepCopy(record)); + } catch (e) { + if (staleCallback) { + staleCallback(); + } + throw e; + } + if (! staleCallback) { + return; + } + callback = staleCallback; } try { backup_object.resolve6(domain, function (err, addresses) { @@ -184,9 +234,21 @@ var EnhanceDns = function (conf) { // override dns.resolveMx method dns.resolveMx = function (domain, callback) { - cache.get('resolveMx_' + domain, function (error, record) { + cache.get('resolveMx_' + domain, function (error, record, staleCallback) { if (record) { - return callback(error, deepCopy(record)); + /*istanbul ignore next - doesn't throw under normal circumstances*/ + try { + callback(error, deepCopy(record)); + } catch (e) { + if (staleCallback) { + staleCallback(); + } + throw e; + } + if (! staleCallback) { + return; + } + callback = staleCallback; } try { backup_object.resolveMx(domain, function (err, addresses) { @@ -206,9 +268,21 @@ var EnhanceDns = function (conf) { // override dns.resolveTxt method dns.resolveTxt = function (domain, callback) { - cache.get('resolveTxt_' + domain, function (error, record) { + cache.get('resolveTxt_' + domain, function (error, record, staleCallback) { if (record) { - return callback(error, deepCopy(record)); + /*istanbul ignore next - doesn't throw under normal circumstances*/ + try { + callback(error, deepCopy(record)); + } catch (e) { + if (staleCallback) { + staleCallback(); + } + throw e; + } + if (! staleCallback) { + return; + } + callback = staleCallback; } try { backup_object.resolveTxt(domain, function (err, addresses) { @@ -228,9 +302,21 @@ var EnhanceDns = function (conf) { // override dns.resolveSrv method dns.resolveSrv = function (domain, callback) { - cache.get('resolveSrv_' + domain, function (error, record) { + cache.get('resolveSrv_' + domain, function (error, record, staleCallback) { if (record) { - return callback(error, deepCopy(record)); + /*istanbul ignore next - doesn't throw under normal circumstances*/ + try { + callback(error, deepCopy(record)); + } catch (e) { + if (staleCallback) { + staleCallback(); + } + throw e; + } + if (! staleCallback) { + return; + } + callback = staleCallback; } try { backup_object.resolveSrv(domain, function (err, addresses) { @@ -250,9 +336,21 @@ var EnhanceDns = function (conf) { // override dns.resolveNs method dns.resolveNs = function (domain, callback) { - cache.get('resolveNs_' + domain, function (error, record) { + cache.get('resolveNs_' + domain, function (error, record, staleCallback) { if (record) { - return callback(error, deepCopy(record)); + /*istanbul ignore next - doesn't throw under normal circumstances*/ + try { + callback(error, deepCopy(record)); + } catch (e) { + if (staleCallback) { + staleCallback(); + } + throw e; + } + if (! staleCallback) { + return; + } + callback = staleCallback; } try { backup_object.resolveNs(domain, function (err, addresses) { @@ -272,9 +370,21 @@ var EnhanceDns = function (conf) { // override dns.resolveCname method dns.resolveCname = function (domain, callback) { - cache.get('resolveCname_' + domain, function (error, record) { + cache.get('resolveCname_' + domain, function (error, record, staleCallback) { if (record) { - return callback(error, deepCopy(record)); + /*istanbul ignore next - doesn't throw under normal circumstances*/ + try { + callback(error, deepCopy(record)); + } catch (e) { + if (staleCallback) { + staleCallback(); + } + throw e; + } + if (! staleCallback) { + return; + } + callback = staleCallback; } try { backup_object.resolveCname(domain, function (err, addresses) { @@ -294,9 +404,21 @@ var EnhanceDns = function (conf) { // override dns.reverse method dns.reverse = function (ip, callback) { - cache.get('reverse_' + ip, function (error, record) { + cache.get('reverse_' + ip, function (error, record, staleCallback) { if (record) { - return callback(error, deepCopy(record)); + /*istanbul ignore next - doesn't throw under normal circumstances*/ + try { + callback(error, deepCopy(record)); + } catch (e) { + if (staleCallback) { + staleCallback(); + } + throw e; + } + if (! staleCallback) { + return; + } + callback = staleCallback; } try { backup_object.reverse(ip, function (err, addresses) { diff --git a/test/index.js b/test/index.js index 77a2a80..ff27bcc 100644 --- a/test/index.js +++ b/test/index.js @@ -10,12 +10,13 @@ var assert = require('assert'), mod = require('../lib/index.js')({ enable: true, ttl: 300, - cachesize: 1000 + cachesize: 1000, + useStale: true }); var dns = require('dns'); -var methods = [dns.lookup, dns.resolve, dns.resolve4, dns.resolve6, dns.resolveMx,dns.resolveTxt, - dns.resolveSrv, dns.resolveNs, dns.resolveCname, dns.reverse]; +var methods = ["lookup", "resolve", "resolve4", "resolve6", "resolveMx", "resolveTxt", + "resolveSrv", "resolveNs", "resolveCname", "reverse"]; var params = ["www.yahoo.com", "www.google.com", "www.google.com", "ipv6.google.com", "yahoo.com", "google.com", "www.yahoo.com", "yahoo.com", "www.yahoo.com", "173.236.27.26"]; var prefix = ['lookup_', 'resolve_', 'resolve4_', 'resolve6_', 'resolveMx_', 'resolveTxt_', @@ -35,10 +36,8 @@ describe('dnscache main test suite', function() { }); it('should verify internal cache is create for each call', function (done) { - var index = 0; - async.eachSeries(methods, function(method, cb) { - method(params[index], function(err, result) { - ++index; + async.eachOf(methods, function(name, index, cb) { + dns[name](params[index], function(err, result) { cb(err, result); }); }, function () { @@ -53,10 +52,8 @@ describe('dnscache main test suite', function() { }); it('verify hits are incremented', function (done) { - var index = 0; - async.eachSeries(methods, function(method, cb) { - method(params[index], function(err, result) { - ++index; + async.eachOf(methods, function(name, index, cb) { + dns[name](params[index], function(err, result) { cb(err, result); }); }, function () { @@ -105,8 +102,8 @@ describe('dnscache main test suite', function() { it('should error if the underlying dns method throws', function(done) { var errors = []; - async.each(methods, function(method, cb) { - method([], function(err) { + async.each(methods, function(name, cb) { + dns[name]([], function(err) { errors.push(err); cb(null); }); @@ -153,10 +150,8 @@ describe('dnscache main test suite', function() { }); it('not create a cache from an error in a lookup', function (done) { - var index = 0; - async.eachSeries(methods, function(method, cb) { - method('someerrordata', function(err) { - ++index; + async.eachOf(methods, function(name, index, cb) { + dns[name]('someerrordata', function(err) { cb(null, err); }); }, function () { @@ -234,6 +229,7 @@ describe('dnscache main test suite', function() { assert.ok(conf); assert.equal(conf.ttl, 300); assert.equal(conf.cachesize, 1000); + assert.equal(conf.useStale, false); done(); }); }); @@ -260,4 +256,96 @@ describe('dnscache main test suite', function() { }); }); } + + it('cache should evict if useStale disabled', function(done) { + //if created from other tests + if (require('dns').internalCache) { + delete require('dns').internalCache; + } + var testee = require('../lib/index.js')({ + enable: true, + ttl: 5, + cachesize: 1000, + useStale: false + }); + + async.eachOf(methods, function(name, index, cb) { + testee[name](params[index], function(err, result) { + cb(err, result); + }); + }, function (err) { + if (err) { + return done(err); + } + methods.forEach(function(name, index) { + var key = suffix[index] !== 'none' ? prefix[index] + params[index] + suffix[index] : prefix[index] + params[index]; + assert.equal(testee.internalCache.data[key].hit, 0, 'hit should be 0 for ' + key); + }); + + // wait until all cache expired. + setTimeout(function () { + async.eachOf(methods, function(name, index, cb) { + testee[name](params[index], function(err, result) { + cb(err, result); + }); + }, function (err) { + if (err) { + return done(err); + } + methods.forEach(function(name, index) { + var key = suffix[index] !== 'none' ? prefix[index] + params[index] + suffix[index] : prefix[index] + params[index]; + assert.equal(testee.internalCache.data[key].hit, 0, 'hit should be 0 for ' + key); + }); + + done(); + }); + }, 5*1000); + }); + }); + + it('cache should not evict if useStale enabled', function(done) { + //if created from other tests + if (require('dns').internalCache) { + delete require('dns').internalCache; + } + var testee = require('../lib/index.js')({ + enable: true, + ttl: 5, + cachesize: 1000, + useStale: true + }); + + async.eachOf(methods, function(name, index, cb) { + testee[name](params[index], function(err, result) { + cb(err, result); + }); + }, function (err) { + if (err) { + return done(err); + } + methods.forEach(function(name, index) { + var key = suffix[index] !== 'none' ? prefix[index] + params[index] + suffix[index] : prefix[index] + params[index]; + assert.equal(testee.internalCache.data[key].hit, 0, 'hit should be 0 for ' + key); + }); + + // wait until all cache expired. + setTimeout(function () { + async.eachOf(methods, function(name, index, cb) { + testee[name](params[index], function(err, result) { + cb(err, result); + }); + }, function (err) { + if (err) { + return done(err); + } + methods.forEach(function(name, index) { + var key = suffix[index] !== 'none' ? prefix[index] + params[index] + suffix[index] : prefix[index] + params[index]; + assert.equal(testee.internalCache.data[key].hit, 1, 'hit should be 1 for ' + key); + }); + + done(); + }); + }, 5*1000); + }); + }); });