pixl-server-storage 4.1.0 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/engines/Redis.js CHANGED
@@ -9,6 +9,8 @@ const Class = require("pixl-class");
9
9
  const Component = require("pixl-server/component");
10
10
  const Redis = require('ioredis');
11
11
  const Tools = require("pixl-tools");
12
+ const Perf = require("pixl-perf");
13
+ const Cache = require("pixl-cache");
12
14
 
13
15
  module.exports = Class.create({
14
16
 
@@ -47,6 +49,23 @@ module.exports = Class.create({
47
49
  this.keyTemplate = (r_config.keyTemplate || '').replace(/^\//, '').replace(/\/$/, '');
48
50
  delete r_config.keyTemplate;
49
51
 
52
+ // optional LRU cache
53
+ this.cache = null;
54
+ var cache_opts = r_config.cache;
55
+ if (cache_opts && cache_opts.enabled) {
56
+ this.logDebug(3, "Setting up LRU cache", cache_opts);
57
+ this.cache = new Cache( Tools.copyHashRemoveKeys(cache_opts, { enabled: 1 }) );
58
+ this.cache.on('expire', function(item, reason) {
59
+ self.logDebug(9, "Expiring LRU cache object: " + item.key + " due to: " + reason, {
60
+ key: item.key,
61
+ reason: reason,
62
+ totalCount: self.cache.count,
63
+ totalBytes: self.cache.bytes
64
+ });
65
+ });
66
+ }
67
+ delete r_config.cache;
68
+
50
69
  if (!r_config.username.length) delete r_config.username;
51
70
  if (!r_config.password.length) delete r_config.password;
52
71
 
@@ -93,13 +112,14 @@ module.exports = Class.create({
93
112
  // store key+value in Redis
94
113
  var self = this;
95
114
  key = this.prepKey(key);
115
+ var is_binary = this.storage.isBinaryKey(key);
96
116
 
97
- if (this.storage.isBinaryKey(key)) {
117
+ if (is_binary) {
98
118
  this.logDebug(9, "Storing Redis Binary Object: " + key, '' + value.length + ' bytes');
99
119
  }
100
120
  else {
101
121
  this.logDebug(9, "Storing Redis JSON Object: " + key, this.debugLevel(10) ? value : null);
102
- value = JSON.stringify( value );
122
+ value = Buffer.from( JSON.stringify( value ) );
103
123
  }
104
124
 
105
125
  this.redis.set( key, value, function(err) {
@@ -109,6 +129,11 @@ module.exports = Class.create({
109
129
  }
110
130
  else self.logDebug(9, "Store complete: " + key);
111
131
 
132
+ // possibly cache in LRU
133
+ if (!err && self.cache && !is_binary) {
134
+ self.cache.set( key, value, { date: Tools.timeNow(true) } );
135
+ }
136
+
112
137
  if (callback) callback(err);
113
138
  } );
114
139
  },
@@ -138,7 +163,30 @@ module.exports = Class.create({
138
163
  // The Redis API has no way to head / ping an object.
139
164
  // So, we have to do this the RAM-hard way...
140
165
 
141
- this.redis.get( key, function(err, data) {
166
+ // check cache first
167
+ if (this.cache && this.cache.has(key)) {
168
+ var item = this.cache.getMeta(key);
169
+
170
+ if (item.value.length == 0) {
171
+ process.nextTick( function() {
172
+ var err = new Error("Failed to head key: " + key + ": Not found");
173
+ err.code = "NoSuchKey";
174
+ callback( err );
175
+ } );
176
+ }
177
+ else {
178
+ process.nextTick( function() {
179
+ self.logDebug(9, "Cached head complete: " + key);
180
+ callback( null, {
181
+ mod: item.date,
182
+ len: item.value.length
183
+ } );
184
+ } );
185
+ }
186
+ return;
187
+ } // cache
188
+
189
+ this.redis.getBuffer( key, function(err, data) {
142
190
  if (err) {
143
191
  // an actual error
144
192
  err.message = "Failed to head key: " + key + ": " + err;
@@ -150,7 +198,12 @@ module.exports = Class.create({
150
198
  // always use "NoSuchKey" in error code
151
199
  var err = new Error("Failed to head key: " + key + ": Not found");
152
200
  err.code = "NoSuchKey";
153
-
201
+
202
+ if (self.cache && !self.storage.isBinaryKey(key)) {
203
+ // store 'empty' stub in cache
204
+ self.cache.set( key, Buffer.alloc(0), { date: 0 } );
205
+ }
206
+
154
207
  callback( err, null );
155
208
  }
156
209
  else {
@@ -163,44 +216,79 @@ module.exports = Class.create({
163
216
  // fetch Redis value given key
164
217
  var self = this;
165
218
  key = this.prepKey(key);
219
+ var is_binary = this.storage.isBinaryKey(key);
166
220
 
167
221
  this.logDebug(9, "Fetching Redis Object: " + key);
168
222
 
169
- var func = this.storage.isBinaryKey(key) ? 'getBuffer' : 'get';
170
- this.redis[func]( key, function(err, result) {
171
- if (!result) {
172
- if (err) {
173
- // an actual error
174
- err.message = "Failed to fetch key: " + key + ": " + err;
175
- self.logError('redis', ''+err);
176
- callback( err, null );
177
- }
178
- else {
179
- // record not found
180
- // always use "NoSuchKey" in error code
223
+ // check cache first
224
+ if (this.cache && !is_binary && this.cache.has(key)) {
225
+ var item = this.cache.getMeta(key);
226
+
227
+ if (item.value.length == 0) {
228
+ process.nextTick( function() {
181
229
  var err = new Error("Failed to fetch key: " + key + ": Not found");
182
230
  err.code = "NoSuchKey";
183
-
184
- callback( err, null );
185
- }
231
+ callback( err );
232
+ } );
186
233
  }
187
234
  else {
188
- if (self.storage.isBinaryKey(key)) {
189
- self.logDebug(9, "Binary fetch complete: " + key, '' + result.length + ' bytes');
190
- }
191
- else {
192
- try { result = JSON.parse( result.toString() ); }
235
+ process.nextTick( function() {
236
+ var data = null;
237
+ try { data = JSON.parse( item.value.toString() ); }
193
238
  catch (err) {
194
239
  self.logError('redis', "Failed to parse JSON record: " + key + ": " + err);
195
240
  callback( err, null );
196
241
  return;
197
242
  }
198
- self.logDebug(9, "JSON fetch complete: " + key, self.debugLevel(10) ? result : null);
243
+ self.logDebug(9, "Cached JSON fetch complete: " + key, self.debugLevel(10) ? data : null);
244
+
245
+ callback( null, data );
246
+ } );
247
+ }
248
+ return;
249
+ } // cache
250
+
251
+ this.redis.getBuffer( key, function(err, result) {
252
+ if (err) {
253
+ // an actual error
254
+ err.message = "Failed to fetch key: " + key + ": " + err;
255
+ self.logError('redis', ''+err);
256
+ return callback( err, null );
257
+ }
258
+ else if (!result || !result.length) {
259
+ // record not found
260
+ // always use "NoSuchKey" in error code
261
+ var err = new Error("Failed to fetch key: " + key + ": Not found");
262
+ err.code = "NoSuchKey";
263
+
264
+ if (self.cache && !is_binary) {
265
+ // store 'empty' stub in cache
266
+ self.cache.set( key, Buffer.alloc(0), { date: 0 } );
199
267
  }
200
268
 
201
- callback( null, result );
269
+ return callback( err, null );
202
270
  }
203
- } );
271
+
272
+ if (is_binary) {
273
+ self.logDebug(9, "Binary fetch complete: " + key, '' + result.length + ' bytes');
274
+ }
275
+ else {
276
+ // possibly cache in LRU
277
+ if (self.cache) {
278
+ self.cache.set( key, result, { date: Tools.timeNow(true) } );
279
+ }
280
+
281
+ try { result = JSON.parse( result.toString() ); }
282
+ catch (err) {
283
+ self.logError('redis', "Failed to parse JSON record: " + key + ": " + err);
284
+ callback( err, null );
285
+ return;
286
+ }
287
+ self.logDebug(9, "JSON fetch complete: " + key, self.debugLevel(10) ? result : null);
288
+ }
289
+
290
+ callback( null, result );
291
+ } ); // redis.getBuffer
204
292
  },
205
293
 
206
294
  getBuffer: function(key, callback) {
@@ -303,6 +391,7 @@ module.exports = Class.create({
303
391
  // delete Redis key given key
304
392
  var self = this;
305
393
  key = this.prepKey(key);
394
+ var is_binary = this.storage.isBinaryKey(key);
306
395
 
307
396
  this.logDebug(9, "Deleting Redis Object: " + key);
308
397
 
@@ -310,21 +399,166 @@ module.exports = Class.create({
310
399
  if (!err && !deleted) {
311
400
  err = new Error("Failed to fetch key: " + key + ": Not found");
312
401
  err.code = "NoSuchKey";
402
+
403
+ if (self.cache && !is_binary) {
404
+ // store 'empty' stub in cache
405
+ self.cache.set( key, Buffer.alloc(0), { date: 0 } );
406
+ }
313
407
  }
314
408
  if (err) {
315
409
  self.logError('redis', "Failed to delete object: " + key + ": " + err);
316
410
  }
317
- else self.logDebug(9, "Delete complete: " + key);
411
+ else {
412
+ self.logDebug(9, "Delete complete: " + key);
413
+
414
+ // possibly "delete" from LRU cache as well
415
+ if (self.cache && !is_binary) {
416
+ // store 'empty' stub in cache
417
+ self.cache.set( key, Buffer.alloc(0), { date: 0 } );
418
+ }
419
+ }
318
420
 
319
421
  callback(err);
320
422
  } );
321
423
  },
322
424
 
425
+ commitTransaction: function(trans, callback) {
426
+ // commit all transaction actions using a native Redis MULTI/EXEC transaction
427
+ var self = this;
428
+ var write_keys = [];
429
+ var write_values = [];
430
+ var write_modified = [];
431
+ var delete_keys = [];
432
+ var bad_state = null;
433
+ var multi = this.redis.multi();
434
+
435
+ this.logDebug(5, "Beginning transaction commit: " + trans.id, {
436
+ path: trans.path,
437
+ actions: Tools.numKeys(trans.keys)
438
+ });
439
+
440
+ Object.keys(trans.keys).forEach( function(key) {
441
+ var record_state = trans.keys[key];
442
+ var norm_key = self.storage.normalizeKey(key);
443
+
444
+ if (record_state == 'W') {
445
+ // Collect writes into parallel arrays so we can update the cache later
446
+ // without stringifying and Buffer-wrapping JSON a second time.
447
+ var item = trans.values[key];
448
+ var redis_key = self.prepKey(norm_key);
449
+ var value = Buffer.from( JSON.stringify( item.data ) );
450
+
451
+ write_keys.push( redis_key );
452
+ write_values.push( value );
453
+ write_modified.push( item.mod );
454
+
455
+ multi.set( redis_key, value );
456
+ }
457
+ else if (record_state == 'D') {
458
+ // Queue deletes into the same Redis transaction.
459
+ var redis_key = self.prepKey(norm_key);
460
+ delete_keys.push( redis_key );
461
+
462
+ multi.del( redis_key );
463
+ }
464
+ else {
465
+ bad_state = new Error("Unknown transaction record state: " + record_state + ": " + key);
466
+ }
467
+ });
468
+
469
+ if (bad_state) return callback(bad_state);
470
+
471
+ multi.exec( function(err, results) {
472
+ if (err) {
473
+ // With MULTI/EXEC over the network, a top-level exec error may leave
474
+ // the actual Redis outcome unknowable, so treat it as fatal.
475
+ err.fatal = true;
476
+ self.logError('redis', "Transaction commit failed: " + err);
477
+ return callback(err);
478
+ }
479
+
480
+ // Redis can return per-command errors from EXEC while still applying
481
+ // other commands. That is a partial commit, so it must be fatal.
482
+ var cmd_err = null;
483
+ if (results) {
484
+ results.forEach( function(item) {
485
+ if (item && item[0] && !cmd_err) cmd_err = item[0];
486
+ });
487
+ }
488
+ if (cmd_err) {
489
+ cmd_err.fatal = true;
490
+ self.logError('redis', "Transaction command failed: " + cmd_err);
491
+ return callback(cmd_err);
492
+ }
493
+
494
+ // commit succeeded, so now it is safe to update the engine LRU cache
495
+ if (self.cache) {
496
+ write_keys.forEach( function(key, idx) {
497
+ if (!self.storage.isBinaryKey(key)) {
498
+ self.cache.set( key, write_values[idx], { date: write_modified[idx] } );
499
+ }
500
+ });
501
+
502
+ delete_keys.forEach( function(key) {
503
+ if (!self.storage.isBinaryKey(key)) {
504
+ // store 'empty' stub in cache
505
+ self.cache.set( key, Buffer.alloc(0), { date: 0 } );
506
+ }
507
+ });
508
+ }
509
+
510
+ self.logDebug(5, "Transaction commit complete: " + trans.id, {
511
+ path: trans.path
512
+ });
513
+
514
+ callback();
515
+ } );
516
+ },
517
+
518
+ unitTestCleanup: function(callback) {
519
+ // cleanup all unit test data
520
+ var self = this;
521
+ this.logDebug(3, "Cleaning up Redis unit test database with FLUSHDB");
522
+
523
+ return this.redis.flushdb( function(err) {
524
+ if (err) {
525
+ self.logError('redis', "Failed to cleanup Redis unit test database: " + err);
526
+ return callback(err);
527
+ }
528
+
529
+ if (self.cache) self.cache.clear();
530
+ callback();
531
+ } );
532
+ },
533
+
323
534
  runMaintenance: function(callback) {
324
535
  // run daily maintenance
325
536
  callback();
326
537
  },
327
538
 
539
+ optimize: function(callback) {
540
+ // Redis does its own memory management, so there is nothing to run here.
541
+ var perf = new Perf();
542
+ perf.setScale(1);
543
+ perf.begin();
544
+ perf.begin('noop');
545
+ perf.end('noop');
546
+ perf.end();
547
+
548
+ process.nextTick( callback, null, {
549
+ engine: this.__name,
550
+ optimized: false,
551
+ perf: perf.metrics(),
552
+ operations: [
553
+ {
554
+ name: 'noop',
555
+ ok: true,
556
+ message: "Redis does not require storage optimization."
557
+ }
558
+ ]
559
+ } );
560
+ },
561
+
328
562
  shutdown: function(callback) {
329
563
  // shutdown storage
330
564
  this.logDebug(2, "Shutting down Redis");
@@ -9,6 +9,8 @@ const Class = require("pixl-class");
9
9
  const Component = require("pixl-server/component");
10
10
  const Redis = require('ioredis');
11
11
  const Tools = require("pixl-tools");
12
+ const Perf = require("pixl-perf");
13
+ const Cache = require("pixl-cache");
12
14
 
13
15
  module.exports = Class.create({
14
16
 
@@ -49,6 +51,22 @@ module.exports = Class.create({
49
51
 
50
52
  this.keyTemplate = (r_config.keyTemplate || '').replace(/^\//, '').replace(/\/$/, '');
51
53
 
54
+ // optional LRU cache
55
+ this.cache = null;
56
+ var cache_opts = r_config.cache;
57
+ if (cache_opts && cache_opts.enabled) {
58
+ this.logDebug(3, "Setting up LRU cache", cache_opts);
59
+ this.cache = new Cache( Tools.copyHashRemoveKeys(cache_opts, { enabled: 1 }) );
60
+ this.cache.on('expire', function(item, reason) {
61
+ self.logDebug(9, "Expiring LRU cache object: " + item.key + " due to: " + reason, {
62
+ key: item.key,
63
+ reason: reason,
64
+ totalCount: self.cache.count,
65
+ totalBytes: self.cache.bytes
66
+ });
67
+ });
68
+ }
69
+
52
70
  r_config.clusterOpts.clusterRetryStrategy = function(attempts) {
53
71
  if (attempts > r_config.connectRetries) return false;
54
72
  return attempts;
@@ -107,13 +125,14 @@ module.exports = Class.create({
107
125
  // store key+value in Redis
108
126
  var self = this;
109
127
  key = this.prepKey(key);
128
+ var is_binary = this.storage.isBinaryKey(key);
110
129
 
111
- if (this.storage.isBinaryKey(key)) {
130
+ if (is_binary) {
112
131
  this.logDebug(9, "Storing Redis Binary Object: " + key, '' + value.length + ' bytes');
113
132
  }
114
133
  else {
115
134
  this.logDebug(9, "Storing Redis JSON Object: " + key, this.debugLevel(10) ? value : null);
116
- value = JSON.stringify( value );
135
+ value = Buffer.from( JSON.stringify( value ) );
117
136
  }
118
137
 
119
138
  this.redis.set( key, value, function(err) {
@@ -123,6 +142,11 @@ module.exports = Class.create({
123
142
  }
124
143
  else self.logDebug(9, "Store complete: " + key);
125
144
 
145
+ // possibly cache in LRU
146
+ if (!err && self.cache && !is_binary) {
147
+ self.cache.set( key, value, { date: Tools.timeNow(true) } );
148
+ }
149
+
126
150
  if (callback) callback(err);
127
151
  } );
128
152
  },
@@ -152,7 +176,30 @@ module.exports = Class.create({
152
176
  // The Redis API has no way to head / ping an object.
153
177
  // So, we have to do this the RAM-hard way...
154
178
 
155
- this.redis.get( key, function(err, data) {
179
+ // check cache first
180
+ if (this.cache && this.cache.has(key)) {
181
+ var item = this.cache.getMeta(key);
182
+
183
+ if (item.value.length == 0) {
184
+ process.nextTick( function() {
185
+ var err = new Error("Failed to head key: " + key + ": Not found");
186
+ err.code = "NoSuchKey";
187
+ callback( err );
188
+ } );
189
+ }
190
+ else {
191
+ process.nextTick( function() {
192
+ self.logDebug(9, "Cached head complete: " + key);
193
+ callback( null, {
194
+ mod: item.date,
195
+ len: item.value.length
196
+ } );
197
+ } );
198
+ }
199
+ return;
200
+ } // cache
201
+
202
+ this.redis.getBuffer( key, function(err, data) {
156
203
  if (err) {
157
204
  // an actual error
158
205
  err.message = "Failed to head key: " + key + ": " + err;
@@ -164,7 +211,12 @@ module.exports = Class.create({
164
211
  // always use "NoSuchKey" in error code
165
212
  var err = new Error("Failed to head key: " + key + ": Not found");
166
213
  err.code = "NoSuchKey";
167
-
214
+
215
+ if (self.cache && !self.storage.isBinaryKey(key)) {
216
+ // store 'empty' stub in cache
217
+ self.cache.set( key, Buffer.alloc(0), { date: 0 } );
218
+ }
219
+
168
220
  callback( err, null );
169
221
  }
170
222
  else {
@@ -177,44 +229,79 @@ module.exports = Class.create({
177
229
  // fetch Redis value given key
178
230
  var self = this;
179
231
  key = this.prepKey(key);
232
+ var is_binary = this.storage.isBinaryKey(key);
180
233
 
181
234
  this.logDebug(9, "Fetching Redis Object: " + key);
182
235
 
183
- var func = this.storage.isBinaryKey(key) ? 'getBuffer' : 'get';
184
- this.redis[func]( key, function(err, result) {
185
- if (!result) {
186
- if (err) {
187
- // an actual error
188
- err.message = "Failed to fetch key: " + key + ": " + err;
189
- self.logError('redis', ''+err);
190
- callback( err, null );
191
- }
192
- else {
193
- // record not found
194
- // always use "NoSuchKey" in error code
236
+ // check cache first
237
+ if (this.cache && !is_binary && this.cache.has(key)) {
238
+ var item = this.cache.getMeta(key);
239
+
240
+ if (item.value.length == 0) {
241
+ process.nextTick( function() {
195
242
  var err = new Error("Failed to fetch key: " + key + ": Not found");
196
243
  err.code = "NoSuchKey";
197
-
198
- callback( err, null );
199
- }
244
+ callback( err );
245
+ } );
200
246
  }
201
247
  else {
202
- if (self.storage.isBinaryKey(key)) {
203
- self.logDebug(9, "Binary fetch complete: " + key, '' + result.length + ' bytes');
204
- }
205
- else {
206
- try { result = JSON.parse( result.toString() ); }
248
+ process.nextTick( function() {
249
+ var data = null;
250
+ try { data = JSON.parse( item.value.toString() ); }
207
251
  catch (err) {
208
252
  self.logError('redis', "Failed to parse JSON record: " + key + ": " + err);
209
253
  callback( err, null );
210
254
  return;
211
255
  }
212
- self.logDebug(9, "JSON fetch complete: " + key, self.debugLevel(10) ? result : null);
256
+ self.logDebug(9, "Cached JSON fetch complete: " + key, self.debugLevel(10) ? data : null);
257
+
258
+ callback( null, data );
259
+ } );
260
+ }
261
+ return;
262
+ } // cache
263
+
264
+ this.redis.getBuffer( key, function(err, result) {
265
+ if (err) {
266
+ // an actual error
267
+ err.message = "Failed to fetch key: " + key + ": " + err;
268
+ self.logError('redis', ''+err);
269
+ return callback( err, null );
270
+ }
271
+ else if (!result || !result.length) {
272
+ // record not found
273
+ // always use "NoSuchKey" in error code
274
+ var err = new Error("Failed to fetch key: " + key + ": Not found");
275
+ err.code = "NoSuchKey";
276
+
277
+ if (self.cache && !is_binary) {
278
+ // store 'empty' stub in cache
279
+ self.cache.set( key, Buffer.alloc(0), { date: 0 } );
213
280
  }
214
281
 
215
- callback( null, result );
282
+ return callback( err, null );
216
283
  }
217
- } );
284
+
285
+ if (is_binary) {
286
+ self.logDebug(9, "Binary fetch complete: " + key, '' + result.length + ' bytes');
287
+ }
288
+ else {
289
+ // possibly cache in LRU
290
+ if (self.cache) {
291
+ self.cache.set( key, result, { date: Tools.timeNow(true) } );
292
+ }
293
+
294
+ try { result = JSON.parse( result.toString() ); }
295
+ catch (err) {
296
+ self.logError('redis', "Failed to parse JSON record: " + key + ": " + err);
297
+ callback( err, null );
298
+ return;
299
+ }
300
+ self.logDebug(9, "JSON fetch complete: " + key, self.debugLevel(10) ? result : null);
301
+ }
302
+
303
+ callback( null, result );
304
+ } ); // redis.getBuffer
218
305
  },
219
306
 
220
307
  getBuffer: function(key, callback) {
@@ -317,6 +404,7 @@ module.exports = Class.create({
317
404
  // delete Redis key given key
318
405
  var self = this;
319
406
  key = this.prepKey(key);
407
+ var is_binary = this.storage.isBinaryKey(key);
320
408
 
321
409
  this.logDebug(9, "Deleting Redis Object: " + key);
322
410
 
@@ -324,11 +412,24 @@ module.exports = Class.create({
324
412
  if (!err && !deleted) {
325
413
  err = new Error("Failed to fetch key: " + key + ": Not found");
326
414
  err.code = "NoSuchKey";
415
+
416
+ if (self.cache && !is_binary) {
417
+ // store 'empty' stub in cache
418
+ self.cache.set( key, Buffer.alloc(0), { date: 0 } );
419
+ }
327
420
  }
328
421
  if (err) {
329
422
  self.logError('redis', "Failed to delete object: " + key + ": " + err);
330
423
  }
331
- else self.logDebug(9, "Delete complete: " + key);
424
+ else {
425
+ self.logDebug(9, "Delete complete: " + key);
426
+
427
+ // possibly "delete" from LRU cache as well
428
+ if (self.cache && !is_binary) {
429
+ // store 'empty' stub in cache
430
+ self.cache.set( key, Buffer.alloc(0), { date: 0 } );
431
+ }
432
+ }
332
433
 
333
434
  callback(err);
334
435
  } );
@@ -339,6 +440,29 @@ module.exports = Class.create({
339
440
  callback();
340
441
  },
341
442
 
443
+ optimize: function(callback) {
444
+ // Redis Cluster does its own memory management, so there is nothing to run here.
445
+ var perf = new Perf();
446
+ perf.setScale(1);
447
+ perf.begin();
448
+ perf.begin('noop');
449
+ perf.end('noop');
450
+ perf.end();
451
+
452
+ process.nextTick( callback, null, {
453
+ engine: this.__name,
454
+ optimized: false,
455
+ perf: perf.metrics(),
456
+ operations: [
457
+ {
458
+ name: 'noop',
459
+ ok: true,
460
+ message: "RedisCluster does not require storage optimization."
461
+ }
462
+ ]
463
+ } );
464
+ },
465
+
342
466
  shutdown: function(callback) {
343
467
  // shutdown storage
344
468
  this.logDebug(2, "Shutting down Redis");