pixl-server-storage 4.1.0 → 4.1.1

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/README.md CHANGED
@@ -58,6 +58,7 @@ Here is the table of contents for this current document:
58
58
  * [Redis](#redis)
59
59
  + [RedisCluster](#rediscluster)
60
60
  * [SQLite](#sqlite)
61
+ * [Postgres](#postgres)
61
62
  * [Hybrid](#hybrid)
62
63
  - [Key Normalization](#key-normalization)
63
64
  - [Basic Functions](#basic-functions)
@@ -589,7 +590,12 @@ Then configure your storage thusly:
589
590
  "host": "127.0.0.1",
590
591
  "port": 6379,
591
592
  "keyPrefix": "",
592
- "keyTemplate": ""
593
+ "keyTemplate": "",
594
+ "cache": {
595
+ "enabled": true,
596
+ "maxItems": 100000,
597
+ "maxBytes": 104857600
598
+ }
593
599
  }
594
600
  }
595
601
  ```
@@ -600,6 +606,8 @@ The optional `keyPrefix` property works similarly to the [S3 Key Prefix](#s3-key
600
606
 
601
607
  The optional `keyTemplate` property works similarly to the [S3 Key Template](#s3-key-template) feature. It allows you to specify an exact layout of MD5 hash characters, which can be prefixed, mixed in with or postfixed after the key.
602
608
 
609
+ See [S3 Cache](#s3-cache) for details on the `cache` section, as it works in the same way.
610
+
603
611
  ### RedisCluster
604
612
 
605
613
  If you want to use a Redis cluster (e.g. [AWS ElastiCache](https://aws.amazon.com/elasticache/)), then here is how to do that. First, you will need to manually install the following module into your app:
@@ -625,7 +633,12 @@ Then configure your storage thusly:
625
633
  }
626
634
  },
627
635
  "keyPrefix": "",
628
- "keyTemplate": ""
636
+ "keyTemplate": "",
637
+ "cache": {
638
+ "enabled": true,
639
+ "maxItems": 100000,
640
+ "maxBytes": 104857600
641
+ }
629
642
  }
630
643
  }
631
644
  ```
@@ -640,6 +653,8 @@ The optional `keyPrefix` property works similarly to the [S3 Key Prefix](#s3-key
640
653
 
641
654
  The optional `keyTemplate` property works similarly to the [S3 Key Template](#s3-key-template) feature. It allows you to specify an exact layout of MD5 hash characters, which can be prefixed, mixed in with or postfixed after the key.
642
655
 
656
+ See [S3 Cache](#s3-cache) for details on the `cache` section, as it works in the same way.
657
+
643
658
  ## SQLite
644
659
 
645
660
  If you want to use [SQLite](https://sqlite.com/) as a backing store, here is how to do so. First, you need to manually install the [better-sqlite3](https://www.npmjs.com/package/better-sqlite3) module into your app:
@@ -215,7 +215,7 @@ module.exports = Class.create({
215
215
  }
216
216
  else {
217
217
  this.logDebug(9, "Storing JSON Object: " + key, this.debugLevel(10) ? value : file);
218
- value = this.pretty ? JSON.stringify( value, null, "\t" ) : JSON.stringify( value );
218
+ value = Buffer.from( this.pretty ? JSON.stringify( value, null, "\t" ) : JSON.stringify( value ) );
219
219
  }
220
220
 
221
221
  var dir = path.dirname( file );
@@ -378,13 +378,22 @@ module.exports = Class.create({
378
378
  if (this.cache && this.cache.has(key)) {
379
379
  var item = this.cache.getMeta(key);
380
380
 
381
- process.nextTick( function() {
382
- self.logDebug(9, "Cached head complete: " + key);
383
- callback( null, {
384
- mod: item.date,
385
- len: item.value.length
381
+ if (item.value.length == 0) {
382
+ process.nextTick( function() {
383
+ var err = new Error("Failed to head key: " + key + ": File not found");
384
+ err.code = "NoSuchKey";
385
+ callback( err );
386
386
  } );
387
- } );
387
+ }
388
+ else {
389
+ process.nextTick( function() {
390
+ self.logDebug(9, "Cached head complete: " + key);
391
+ callback( null, {
392
+ mod: item.date,
393
+ len: item.value.length
394
+ } );
395
+ } );
396
+ }
388
397
  return;
389
398
  } // cache
390
399
 
@@ -393,6 +402,11 @@ module.exports = Class.create({
393
402
  if (err.message.match(/ENOENT/)) {
394
403
  err.message = "File not found";
395
404
  err.code = "NoSuchKey";
405
+
406
+ if (self.cache && !self.storage.isBinaryKey(key)) {
407
+ // store 'empty' stub in cache
408
+ self.cache.set( key, Buffer.alloc(0), { date: 0 } );
409
+ }
396
410
  }
397
411
  else {
398
412
  // log fs errors that aren't simple missing files (i.e. I/O errors)
@@ -421,30 +435,42 @@ module.exports = Class.create({
421
435
 
422
436
  // check cache first
423
437
  if (this.cache && !is_binary && this.cache.has(key)) {
424
- var data = this.cache.get(key);
438
+ var item = this.cache.getMeta(key);
425
439
 
426
- process.nextTick( function() {
427
- try { data = JSON.parse( data ); }
428
- catch (e) {
429
- self.logError('file', "Failed to parse JSON record: " + key + ": " + e);
430
- callback( e, null );
431
- return;
432
- }
433
- self.logDebug(9, "Cached JSON fetch complete: " + key, self.debugLevel(10) ? data : null);
434
-
435
- callback( null, data );
436
- } );
440
+ if (item.value.length == 0) {
441
+ process.nextTick( function() {
442
+ var err = new Error("Failed to fetch key: " + key + ": File not found");
443
+ err.code = "NoSuchKey";
444
+ callback( err );
445
+ } );
446
+ }
447
+ else {
448
+ process.nextTick( function() {
449
+ var data = null;
450
+ try { data = JSON.parse( item.value.toString() ); }
451
+ catch (e) {
452
+ self.logError('file', "Failed to parse JSON record: " + key + ": " + e);
453
+ callback( e, null );
454
+ return;
455
+ }
456
+ self.logDebug(9, "Cached JSON fetch complete: " + key, self.debugLevel(10) ? data : null);
457
+
458
+ callback( null, data );
459
+ } );
460
+ }
437
461
  return;
438
462
  } // cache
439
463
 
440
- var opts = {};
441
- if (!this.storage.isBinaryKey(key)) opts = { encoding: 'utf8' };
442
-
443
- fs.readFile(file, opts, function (err, data) {
464
+ fs.readFile(file, function (err, data) {
444
465
  if (err) {
445
466
  if (err.message.match(/ENOENT/)) {
446
467
  err.message = "File not found";
447
468
  err.code = "NoSuchKey";
469
+
470
+ if (self.cache && !is_binary) {
471
+ // store 'empty' stub in cache
472
+ self.cache.set( key, Buffer.alloc(0), { date: 0 } );
473
+ }
448
474
  }
449
475
  else {
450
476
  // log fs errors that aren't simple missing files (i.e. I/O errors)
@@ -461,7 +487,7 @@ module.exports = Class.create({
461
487
  }
462
488
 
463
489
  if (!is_binary) {
464
- try { data = JSON.parse( data ); }
490
+ try { data = JSON.parse( data.toString() ); }
465
491
  catch (e) {
466
492
  self.logError('file', "Failed to parse JSON record: " + key + ": " + e);
467
493
  callback( e, null );
@@ -595,6 +621,11 @@ module.exports = Class.create({
595
621
  if (err.message.match(/ENOENT/)) {
596
622
  err.message = "File not found";
597
623
  err.code = "NoSuchKey";
624
+
625
+ if (self.cache && !self.storage.isBinaryKey(key)) {
626
+ // store 'empty' stub in cache
627
+ self.cache.set( key, Buffer.alloc(0), { date: 0 } );
628
+ }
598
629
  }
599
630
 
600
631
  self.logError('file', "Failed to delete file: " + key + ": " + file + ": " + err.message);
@@ -605,9 +636,10 @@ module.exports = Class.create({
605
636
  else {
606
637
  self.logDebug(9, "Delete complete: " + key);
607
638
 
608
- // possibly delete from LRU cache as well
609
- if (self.cache && self.cache.has(key)) {
610
- self.cache.delete(key);
639
+ // possibly "delete" from LRU cache as well
640
+ if (self.cache && !self.storage.isBinaryKey(key)) {
641
+ // store 'empty' stub in cache
642
+ self.cache.set( key, Buffer.alloc(0), { date: 0 } );
611
643
  }
612
644
 
613
645
  // cleanup parent dirs if empty
@@ -204,13 +204,22 @@ module.exports = Class.create({
204
204
  if (this.cache && this.cache.has(key)) {
205
205
  var item = this.cache.getMeta(key);
206
206
 
207
- process.nextTick( function() {
208
- self.logDebug(9, "Cached head complete: " + key);
209
- callback( null, {
210
- mod: item.date,
211
- len: item.value.length
207
+ if (item.value.length == 0) {
208
+ process.nextTick( function() {
209
+ var err = new Error("Failed to head key: " + key + ": Not found");
210
+ err.code = "NoSuchKey";
211
+ callback( err );
212
212
  } );
213
- } );
213
+ }
214
+ else {
215
+ process.nextTick( function() {
216
+ self.logDebug(9, "Cached head complete: " + key);
217
+ callback( null, {
218
+ mod: item.date,
219
+ len: item.value.length
220
+ } );
221
+ } );
222
+ }
214
223
  return;
215
224
  } // cache
216
225
 
@@ -227,6 +236,12 @@ module.exports = Class.create({
227
236
  if (!row) {
228
237
  var err = new Error("Failed to head key: " + key + ": Not found");
229
238
  err.code = "NoSuchKey";
239
+
240
+ if (self.cache && !self.storage.isBinaryKey(key)) {
241
+ // store 'empty' stub in cache
242
+ self.cache.set( key, Buffer.alloc(0), { date: 0 } );
243
+ }
244
+
230
245
  return callback(err, null);
231
246
  }
232
247
  callback(null, { mod: parseInt(row.modified, 10), len: row.len });
@@ -242,19 +257,29 @@ module.exports = Class.create({
242
257
 
243
258
  // check cache first
244
259
  if (this.cache && !is_binary && this.cache.has(key)) {
245
- var data = this.cache.get(key);
260
+ var item = this.cache.getMeta(key);
246
261
 
247
- process.nextTick( function() {
248
- try { data = JSON.parse( data.toString() ); }
249
- catch (e) {
250
- self.logError('pg', "Failed to parse JSON record: " + key + ": " + e);
251
- callback( e, null );
252
- return;
253
- }
254
- self.logDebug(9, "Cached JSON fetch complete: " + key, self.debugLevel(10) ? data : null);
255
-
256
- callback( null, data );
257
- } );
262
+ if (item.value.length == 0) {
263
+ process.nextTick( function() {
264
+ var err = new Error("Failed to fetch key: " + key + ": Not found");
265
+ err.code = "NoSuchKey";
266
+ callback( err );
267
+ } );
268
+ }
269
+ else {
270
+ process.nextTick( function() {
271
+ var data = null;
272
+ try { data = JSON.parse( item.value.toString() ); }
273
+ catch (e) {
274
+ self.logError('pg', "Failed to parse JSON record: " + key + ": " + e);
275
+ callback( e, null );
276
+ return;
277
+ }
278
+ self.logDebug(9, "Cached JSON fetch complete: " + key, self.debugLevel(10) ? data : null);
279
+
280
+ callback( null, data );
281
+ } );
282
+ }
258
283
  return;
259
284
  } // cache
260
285
 
@@ -273,6 +298,12 @@ module.exports = Class.create({
273
298
  if (!row) {
274
299
  var err = new Error("Failed to fetch key: " + key + ": Not found");
275
300
  err.code = "NoSuchKey";
301
+
302
+ if (self.cache && !is_binary) {
303
+ // store 'empty' stub in cache
304
+ self.cache.set( key, Buffer.alloc(0), { date: 0 } );
305
+ }
306
+
276
307
  return callback(err, null);
277
308
  }
278
309
  if (is_binary) {
@@ -412,11 +443,20 @@ module.exports = Class.create({
412
443
  if (!res.rowCount) {
413
444
  var err = new Error("Failed to delete object: " + key + ": Not found");
414
445
  err.code = "NoSuchKey";
446
+
447
+ if (self.cache && !self.storage.isBinaryKey(key)) {
448
+ // store 'empty' stub in cache
449
+ self.cache.set( key, Buffer.alloc(0), { date: 0 } );
450
+ }
451
+
415
452
  return callback(err);
416
453
  }
417
454
  self.logDebug(9, "Delete complete: " + key);
418
- if (self.cache && self.cache.has(key)) {
419
- self.cache.delete(key);
455
+
456
+ // possibly "delete" from LRU cache as well
457
+ if (self.cache && !self.storage.isBinaryKey(key)) {
458
+ // store 'empty' stub in cache
459
+ self.cache.set( key, Buffer.alloc(0), { date: 0 } );
420
460
  }
421
461
  callback();
422
462
  } ); // query
package/engines/Redis.js CHANGED
@@ -9,6 +9,7 @@ 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 Cache = require("pixl-cache");
12
13
 
13
14
  module.exports = Class.create({
14
15
 
@@ -47,6 +48,23 @@ module.exports = Class.create({
47
48
  this.keyTemplate = (r_config.keyTemplate || '').replace(/^\//, '').replace(/\/$/, '');
48
49
  delete r_config.keyTemplate;
49
50
 
51
+ // optional LRU cache
52
+ this.cache = null;
53
+ var cache_opts = r_config.cache;
54
+ if (cache_opts && cache_opts.enabled) {
55
+ this.logDebug(3, "Setting up LRU cache", cache_opts);
56
+ this.cache = new Cache( Tools.copyHashRemoveKeys(cache_opts, { enabled: 1 }) );
57
+ this.cache.on('expire', function(item, reason) {
58
+ self.logDebug(9, "Expiring LRU cache object: " + item.key + " due to: " + reason, {
59
+ key: item.key,
60
+ reason: reason,
61
+ totalCount: self.cache.count,
62
+ totalBytes: self.cache.bytes
63
+ });
64
+ });
65
+ }
66
+ delete r_config.cache;
67
+
50
68
  if (!r_config.username.length) delete r_config.username;
51
69
  if (!r_config.password.length) delete r_config.password;
52
70
 
@@ -93,13 +111,14 @@ module.exports = Class.create({
93
111
  // store key+value in Redis
94
112
  var self = this;
95
113
  key = this.prepKey(key);
114
+ var is_binary = this.storage.isBinaryKey(key);
96
115
 
97
- if (this.storage.isBinaryKey(key)) {
116
+ if (is_binary) {
98
117
  this.logDebug(9, "Storing Redis Binary Object: " + key, '' + value.length + ' bytes');
99
118
  }
100
119
  else {
101
120
  this.logDebug(9, "Storing Redis JSON Object: " + key, this.debugLevel(10) ? value : null);
102
- value = JSON.stringify( value );
121
+ value = Buffer.from( JSON.stringify( value ) );
103
122
  }
104
123
 
105
124
  this.redis.set( key, value, function(err) {
@@ -109,6 +128,11 @@ module.exports = Class.create({
109
128
  }
110
129
  else self.logDebug(9, "Store complete: " + key);
111
130
 
131
+ // possibly cache in LRU
132
+ if (!err && self.cache && !is_binary) {
133
+ self.cache.set( key, value, { date: Tools.timeNow(true) } );
134
+ }
135
+
112
136
  if (callback) callback(err);
113
137
  } );
114
138
  },
@@ -138,7 +162,30 @@ module.exports = Class.create({
138
162
  // The Redis API has no way to head / ping an object.
139
163
  // So, we have to do this the RAM-hard way...
140
164
 
141
- this.redis.get( key, function(err, data) {
165
+ // check cache first
166
+ if (this.cache && this.cache.has(key)) {
167
+ var item = this.cache.getMeta(key);
168
+
169
+ if (item.value.length == 0) {
170
+ process.nextTick( function() {
171
+ var err = new Error("Failed to head key: " + key + ": Not found");
172
+ err.code = "NoSuchKey";
173
+ callback( err );
174
+ } );
175
+ }
176
+ else {
177
+ process.nextTick( function() {
178
+ self.logDebug(9, "Cached head complete: " + key);
179
+ callback( null, {
180
+ mod: item.date,
181
+ len: item.value.length
182
+ } );
183
+ } );
184
+ }
185
+ return;
186
+ } // cache
187
+
188
+ this.redis.getBuffer( key, function(err, data) {
142
189
  if (err) {
143
190
  // an actual error
144
191
  err.message = "Failed to head key: " + key + ": " + err;
@@ -150,7 +197,12 @@ module.exports = Class.create({
150
197
  // always use "NoSuchKey" in error code
151
198
  var err = new Error("Failed to head key: " + key + ": Not found");
152
199
  err.code = "NoSuchKey";
153
-
200
+
201
+ if (self.cache && !self.storage.isBinaryKey(key)) {
202
+ // store 'empty' stub in cache
203
+ self.cache.set( key, Buffer.alloc(0), { date: 0 } );
204
+ }
205
+
154
206
  callback( err, null );
155
207
  }
156
208
  else {
@@ -163,44 +215,79 @@ module.exports = Class.create({
163
215
  // fetch Redis value given key
164
216
  var self = this;
165
217
  key = this.prepKey(key);
218
+ var is_binary = this.storage.isBinaryKey(key);
166
219
 
167
220
  this.logDebug(9, "Fetching Redis Object: " + key);
168
221
 
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
222
+ // check cache first
223
+ if (this.cache && !is_binary && this.cache.has(key)) {
224
+ var item = this.cache.getMeta(key);
225
+
226
+ if (item.value.length == 0) {
227
+ process.nextTick( function() {
181
228
  var err = new Error("Failed to fetch key: " + key + ": Not found");
182
229
  err.code = "NoSuchKey";
183
-
184
- callback( err, null );
185
- }
230
+ callback( err );
231
+ } );
186
232
  }
187
233
  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() ); }
234
+ process.nextTick( function() {
235
+ var data = null;
236
+ try { data = JSON.parse( item.value.toString() ); }
193
237
  catch (err) {
194
238
  self.logError('redis', "Failed to parse JSON record: " + key + ": " + err);
195
239
  callback( err, null );
196
240
  return;
197
241
  }
198
- self.logDebug(9, "JSON fetch complete: " + key, self.debugLevel(10) ? result : null);
242
+ self.logDebug(9, "Cached JSON fetch complete: " + key, self.debugLevel(10) ? data : null);
243
+
244
+ callback( null, data );
245
+ } );
246
+ }
247
+ return;
248
+ } // cache
249
+
250
+ this.redis.getBuffer( key, function(err, result) {
251
+ if (err) {
252
+ // an actual error
253
+ err.message = "Failed to fetch key: " + key + ": " + err;
254
+ self.logError('redis', ''+err);
255
+ return callback( err, null );
256
+ }
257
+ else if (!result || !result.length) {
258
+ // record not found
259
+ // always use "NoSuchKey" in error code
260
+ var err = new Error("Failed to fetch key: " + key + ": Not found");
261
+ err.code = "NoSuchKey";
262
+
263
+ if (self.cache && !is_binary) {
264
+ // store 'empty' stub in cache
265
+ self.cache.set( key, Buffer.alloc(0), { date: 0 } );
199
266
  }
200
267
 
201
- callback( null, result );
268
+ return callback( err, null );
202
269
  }
203
- } );
270
+
271
+ if (is_binary) {
272
+ self.logDebug(9, "Binary fetch complete: " + key, '' + result.length + ' bytes');
273
+ }
274
+ else {
275
+ // possibly cache in LRU
276
+ if (self.cache) {
277
+ self.cache.set( key, result, { date: Tools.timeNow(true) } );
278
+ }
279
+
280
+ try { result = JSON.parse( result.toString() ); }
281
+ catch (err) {
282
+ self.logError('redis', "Failed to parse JSON record: " + key + ": " + err);
283
+ callback( err, null );
284
+ return;
285
+ }
286
+ self.logDebug(9, "JSON fetch complete: " + key, self.debugLevel(10) ? result : null);
287
+ }
288
+
289
+ callback( null, result );
290
+ } ); // redis.getBuffer
204
291
  },
205
292
 
206
293
  getBuffer: function(key, callback) {
@@ -303,6 +390,7 @@ module.exports = Class.create({
303
390
  // delete Redis key given key
304
391
  var self = this;
305
392
  key = this.prepKey(key);
393
+ var is_binary = this.storage.isBinaryKey(key);
306
394
 
307
395
  this.logDebug(9, "Deleting Redis Object: " + key);
308
396
 
@@ -310,11 +398,24 @@ module.exports = Class.create({
310
398
  if (!err && !deleted) {
311
399
  err = new Error("Failed to fetch key: " + key + ": Not found");
312
400
  err.code = "NoSuchKey";
401
+
402
+ if (self.cache && !is_binary) {
403
+ // store 'empty' stub in cache
404
+ self.cache.set( key, Buffer.alloc(0), { date: 0 } );
405
+ }
313
406
  }
314
407
  if (err) {
315
408
  self.logError('redis', "Failed to delete object: " + key + ": " + err);
316
409
  }
317
- else self.logDebug(9, "Delete complete: " + key);
410
+ else {
411
+ self.logDebug(9, "Delete complete: " + key);
412
+
413
+ // possibly "delete" from LRU cache as well
414
+ if (self.cache && !is_binary) {
415
+ // store 'empty' stub in cache
416
+ self.cache.set( key, Buffer.alloc(0), { date: 0 } );
417
+ }
418
+ }
318
419
 
319
420
  callback(err);
320
421
  } );
@@ -9,6 +9,7 @@ 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 Cache = require("pixl-cache");
12
13
 
13
14
  module.exports = Class.create({
14
15
 
@@ -49,6 +50,22 @@ module.exports = Class.create({
49
50
 
50
51
  this.keyTemplate = (r_config.keyTemplate || '').replace(/^\//, '').replace(/\/$/, '');
51
52
 
53
+ // optional LRU cache
54
+ this.cache = null;
55
+ var cache_opts = r_config.cache;
56
+ if (cache_opts && cache_opts.enabled) {
57
+ this.logDebug(3, "Setting up LRU cache", cache_opts);
58
+ this.cache = new Cache( Tools.copyHashRemoveKeys(cache_opts, { enabled: 1 }) );
59
+ this.cache.on('expire', function(item, reason) {
60
+ self.logDebug(9, "Expiring LRU cache object: " + item.key + " due to: " + reason, {
61
+ key: item.key,
62
+ reason: reason,
63
+ totalCount: self.cache.count,
64
+ totalBytes: self.cache.bytes
65
+ });
66
+ });
67
+ }
68
+
52
69
  r_config.clusterOpts.clusterRetryStrategy = function(attempts) {
53
70
  if (attempts > r_config.connectRetries) return false;
54
71
  return attempts;
@@ -107,13 +124,14 @@ module.exports = Class.create({
107
124
  // store key+value in Redis
108
125
  var self = this;
109
126
  key = this.prepKey(key);
127
+ var is_binary = this.storage.isBinaryKey(key);
110
128
 
111
- if (this.storage.isBinaryKey(key)) {
129
+ if (is_binary) {
112
130
  this.logDebug(9, "Storing Redis Binary Object: " + key, '' + value.length + ' bytes');
113
131
  }
114
132
  else {
115
133
  this.logDebug(9, "Storing Redis JSON Object: " + key, this.debugLevel(10) ? value : null);
116
- value = JSON.stringify( value );
134
+ value = Buffer.from( JSON.stringify( value ) );
117
135
  }
118
136
 
119
137
  this.redis.set( key, value, function(err) {
@@ -123,6 +141,11 @@ module.exports = Class.create({
123
141
  }
124
142
  else self.logDebug(9, "Store complete: " + key);
125
143
 
144
+ // possibly cache in LRU
145
+ if (!err && self.cache && !is_binary) {
146
+ self.cache.set( key, value, { date: Tools.timeNow(true) } );
147
+ }
148
+
126
149
  if (callback) callback(err);
127
150
  } );
128
151
  },
@@ -152,7 +175,30 @@ module.exports = Class.create({
152
175
  // The Redis API has no way to head / ping an object.
153
176
  // So, we have to do this the RAM-hard way...
154
177
 
155
- this.redis.get( key, function(err, data) {
178
+ // check cache first
179
+ if (this.cache && this.cache.has(key)) {
180
+ var item = this.cache.getMeta(key);
181
+
182
+ if (item.value.length == 0) {
183
+ process.nextTick( function() {
184
+ var err = new Error("Failed to head key: " + key + ": Not found");
185
+ err.code = "NoSuchKey";
186
+ callback( err );
187
+ } );
188
+ }
189
+ else {
190
+ process.nextTick( function() {
191
+ self.logDebug(9, "Cached head complete: " + key);
192
+ callback( null, {
193
+ mod: item.date,
194
+ len: item.value.length
195
+ } );
196
+ } );
197
+ }
198
+ return;
199
+ } // cache
200
+
201
+ this.redis.getBuffer( key, function(err, data) {
156
202
  if (err) {
157
203
  // an actual error
158
204
  err.message = "Failed to head key: " + key + ": " + err;
@@ -164,7 +210,12 @@ module.exports = Class.create({
164
210
  // always use "NoSuchKey" in error code
165
211
  var err = new Error("Failed to head key: " + key + ": Not found");
166
212
  err.code = "NoSuchKey";
167
-
213
+
214
+ if (self.cache && !self.storage.isBinaryKey(key)) {
215
+ // store 'empty' stub in cache
216
+ self.cache.set( key, Buffer.alloc(0), { date: 0 } );
217
+ }
218
+
168
219
  callback( err, null );
169
220
  }
170
221
  else {
@@ -177,44 +228,79 @@ module.exports = Class.create({
177
228
  // fetch Redis value given key
178
229
  var self = this;
179
230
  key = this.prepKey(key);
231
+ var is_binary = this.storage.isBinaryKey(key);
180
232
 
181
233
  this.logDebug(9, "Fetching Redis Object: " + key);
182
234
 
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
235
+ // check cache first
236
+ if (this.cache && !is_binary && this.cache.has(key)) {
237
+ var item = this.cache.getMeta(key);
238
+
239
+ if (item.value.length == 0) {
240
+ process.nextTick( function() {
195
241
  var err = new Error("Failed to fetch key: " + key + ": Not found");
196
242
  err.code = "NoSuchKey";
197
-
198
- callback( err, null );
199
- }
243
+ callback( err );
244
+ } );
200
245
  }
201
246
  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() ); }
247
+ process.nextTick( function() {
248
+ var data = null;
249
+ try { data = JSON.parse( item.value.toString() ); }
207
250
  catch (err) {
208
251
  self.logError('redis', "Failed to parse JSON record: " + key + ": " + err);
209
252
  callback( err, null );
210
253
  return;
211
254
  }
212
- self.logDebug(9, "JSON fetch complete: " + key, self.debugLevel(10) ? result : null);
255
+ self.logDebug(9, "Cached JSON fetch complete: " + key, self.debugLevel(10) ? data : null);
256
+
257
+ callback( null, data );
258
+ } );
259
+ }
260
+ return;
261
+ } // cache
262
+
263
+ this.redis.getBuffer( key, function(err, result) {
264
+ if (err) {
265
+ // an actual error
266
+ err.message = "Failed to fetch key: " + key + ": " + err;
267
+ self.logError('redis', ''+err);
268
+ return callback( err, null );
269
+ }
270
+ else if (!result || !result.length) {
271
+ // record not found
272
+ // always use "NoSuchKey" in error code
273
+ var err = new Error("Failed to fetch key: " + key + ": Not found");
274
+ err.code = "NoSuchKey";
275
+
276
+ if (self.cache && !is_binary) {
277
+ // store 'empty' stub in cache
278
+ self.cache.set( key, Buffer.alloc(0), { date: 0 } );
213
279
  }
214
280
 
215
- callback( null, result );
281
+ return callback( err, null );
216
282
  }
217
- } );
283
+
284
+ if (is_binary) {
285
+ self.logDebug(9, "Binary fetch complete: " + key, '' + result.length + ' bytes');
286
+ }
287
+ else {
288
+ // possibly cache in LRU
289
+ if (self.cache) {
290
+ self.cache.set( key, result, { date: Tools.timeNow(true) } );
291
+ }
292
+
293
+ try { result = JSON.parse( result.toString() ); }
294
+ catch (err) {
295
+ self.logError('redis', "Failed to parse JSON record: " + key + ": " + err);
296
+ callback( err, null );
297
+ return;
298
+ }
299
+ self.logDebug(9, "JSON fetch complete: " + key, self.debugLevel(10) ? result : null);
300
+ }
301
+
302
+ callback( null, result );
303
+ } ); // redis.getBuffer
218
304
  },
219
305
 
220
306
  getBuffer: function(key, callback) {
@@ -317,6 +403,7 @@ module.exports = Class.create({
317
403
  // delete Redis key given key
318
404
  var self = this;
319
405
  key = this.prepKey(key);
406
+ var is_binary = this.storage.isBinaryKey(key);
320
407
 
321
408
  this.logDebug(9, "Deleting Redis Object: " + key);
322
409
 
@@ -324,11 +411,24 @@ module.exports = Class.create({
324
411
  if (!err && !deleted) {
325
412
  err = new Error("Failed to fetch key: " + key + ": Not found");
326
413
  err.code = "NoSuchKey";
414
+
415
+ if (self.cache && !is_binary) {
416
+ // store 'empty' stub in cache
417
+ self.cache.set( key, Buffer.alloc(0), { date: 0 } );
418
+ }
327
419
  }
328
420
  if (err) {
329
421
  self.logError('redis', "Failed to delete object: " + key + ": " + err);
330
422
  }
331
- else self.logDebug(9, "Delete complete: " + key);
423
+ else {
424
+ self.logDebug(9, "Delete complete: " + key);
425
+
426
+ // possibly "delete" from LRU cache as well
427
+ if (self.cache && !is_binary) {
428
+ // store 'empty' stub in cache
429
+ self.cache.set( key, Buffer.alloc(0), { date: 0 } );
430
+ }
431
+ }
332
432
 
333
433
  callback(err);
334
434
  } );
package/engines/S3.js CHANGED
@@ -149,7 +149,7 @@ module.exports = Class.create({
149
149
  }
150
150
  else {
151
151
  this.logDebug(9, "Storing S3 JSON Object: " + key, this.debugLevel(10) ? params.Body : null);
152
- params.Body = this.pretty ? JSON.stringify( params.Body, null, "\t" ) : JSON.stringify( params.Body );
152
+ params.Body = Buffer.from( this.pretty ? JSON.stringify( params.Body, null, "\t" ) : JSON.stringify( params.Body ) );
153
153
  params.ContentType = 'application/json';
154
154
  }
155
155
 
@@ -257,13 +257,22 @@ module.exports = Class.create({
257
257
  if (this.cache && this.cache.has(orig_key)) {
258
258
  var item = this.cache.getMeta(orig_key);
259
259
 
260
- process.nextTick( function() {
261
- self.logDebug(9, "Cached head complete: " + orig_key);
262
- callback( null, {
263
- mod: item.date,
264
- len: item.value.length
260
+ if (item.value.length == 0) {
261
+ process.nextTick( function() {
262
+ var err = new Error("Failed to head key: " + key + ": Not found");
263
+ err.code = "NoSuchKey";
264
+ callback( err );
265
265
  } );
266
- } );
266
+ }
267
+ else {
268
+ process.nextTick( function() {
269
+ self.logDebug(9, "Cached head complete: " + orig_key);
270
+ callback( null, {
271
+ mod: item.date,
272
+ len: item.value.length
273
+ } );
274
+ } );
275
+ }
267
276
  return;
268
277
  } // cache
269
278
 
@@ -288,6 +297,11 @@ module.exports = Class.create({
288
297
  // always include "Not found" in error message
289
298
  err = new Error("Failed to head key: " + key + ": Not found");
290
299
  err.code = "NoSuchKey";
300
+
301
+ if (self.cache && !self.storage.isBinaryKey(orig_key)) {
302
+ // store 'empty' stub in cache
303
+ self.cache.set( orig_key, Buffer.alloc(0), { date: 0 } );
304
+ }
291
305
  }
292
306
  else if (err.name == 'SlowDown') {
293
307
  // special behavior for SlowDown errors
@@ -315,19 +329,29 @@ module.exports = Class.create({
315
329
 
316
330
  // check cache first
317
331
  if (this.cache && !is_binary && this.cache.has(orig_key)) {
318
- var data = this.cache.get(orig_key);
332
+ var item = this.cache.getMeta(orig_key);
319
333
 
320
- process.nextTick( function() {
321
- try { data = JSON.parse( data ); }
322
- catch (e) {
323
- self.logError('file', "Failed to parse JSON record: " + orig_key + ": " + e);
324
- callback( e, null );
325
- return;
326
- }
327
- self.logDebug(9, "Cached JSON fetch complete: " + orig_key, self.debugLevel(10) ? data : null);
328
-
329
- callback( null, data );
330
- } );
334
+ if (item.value.length == 0) {
335
+ process.nextTick( function() {
336
+ var err = new Error("Failed to fetch key: " + key + ": Not found");
337
+ err.code = "NoSuchKey";
338
+ callback( err );
339
+ } );
340
+ }
341
+ else {
342
+ process.nextTick( function() {
343
+ var data = null;
344
+ try { data = JSON.parse( item.value.toString() ); }
345
+ catch (e) {
346
+ self.logError('file', "Failed to parse JSON record: " + orig_key + ": " + e);
347
+ callback( e, null );
348
+ return;
349
+ }
350
+ self.logDebug(9, "Cached JSON fetch complete: " + orig_key, self.debugLevel(10) ? data : null);
351
+
352
+ callback( null, data );
353
+ } );
354
+ }
331
355
  return;
332
356
  } // cache
333
357
 
@@ -350,14 +374,12 @@ module.exports = Class.create({
350
374
  self.logDebug(9, "Binary fetch complete: " + key, '' + body.length + ' bytes');
351
375
  }
352
376
  else {
353
- body = body.toString();
354
-
355
377
  // possibly cache in LRU
356
378
  if (self.cache) {
357
379
  self.cache.set( orig_key, body, { date: Tools.timeNow(true) } );
358
380
  }
359
381
 
360
- try { body = JSON.parse( body ); }
382
+ try { body = JSON.parse( body.toString() ); }
361
383
  catch (e) {
362
384
  self.logError('s3', "Failed to parse JSON record: " + key + ": " + e);
363
385
  callback( e, null );
@@ -378,6 +400,11 @@ module.exports = Class.create({
378
400
  // always include "Not found" in error message
379
401
  err = new Error("Failed to fetch key: " + key + ": Not found");
380
402
  err.code = "NoSuchKey";
403
+
404
+ if (self.cache && !is_binary) {
405
+ // store 'empty' stub in cache
406
+ self.cache.set( orig_key, Buffer.alloc(0), { date: 0 } );
407
+ }
381
408
  }
382
409
  else if (err.name == 'SlowDown') {
383
410
  // special behavior for SlowDown errors
@@ -577,9 +604,10 @@ module.exports = Class.create({
577
604
  self.logDebug(9, "Delete complete: " + key);
578
605
  self.storage.emit('billing', 's3_delete', 1);
579
606
 
580
- // possibly delete from LRU cache as well
581
- if (self.cache && self.cache.has(orig_key)) {
582
- self.cache.delete(orig_key);
607
+ // possibly "delete" from LRU cache as well
608
+ if (self.cache && !self.storage.isBinaryKey(orig_key)) {
609
+ // store 'empty' stub in cache
610
+ self.cache.set( orig_key, Buffer.alloc(0), { date: 0 } );
583
611
  }
584
612
 
585
613
  if (callback) process.nextTick( function() { callback(null, data); } );
package/engines/SQLite.js CHANGED
@@ -191,13 +191,22 @@ module.exports = Class.create({
191
191
  if (this.cache && this.cache.has(key)) {
192
192
  var item = this.cache.getMeta(key);
193
193
 
194
- process.nextTick( function() {
195
- self.logDebug(9, "Cached head complete: " + key);
196
- callback( null, {
197
- mod: item.date,
198
- len: item.value.length
194
+ if (item.value.length == 0) {
195
+ process.nextTick( function() {
196
+ var err = new Error("Failed to head key: " + key + ": Not found");
197
+ err.code = "NoSuchKey";
198
+ callback( err );
199
199
  } );
200
- } );
200
+ }
201
+ else {
202
+ process.nextTick( function() {
203
+ self.logDebug(9, "Cached head complete: " + key);
204
+ callback( null, {
205
+ mod: item.date,
206
+ len: item.value.length
207
+ } );
208
+ } );
209
+ }
201
210
  return;
202
211
  } // cache
203
212
 
@@ -215,6 +224,12 @@ module.exports = Class.create({
215
224
  if (!row) {
216
225
  var err = new Error("Failed to head key: " + key + ": Not found");
217
226
  err.code = "NoSuchKey";
227
+
228
+ if (self.cache && !self.storage.isBinaryKey(key)) {
229
+ // store 'empty' stub in cache
230
+ self.cache.set( key, Buffer.alloc(0), { date: 0 } );
231
+ }
232
+
218
233
  return callback(err, null);
219
234
  }
220
235
  callback(null, { mod: row.modified, len: row['length(value)'] });
@@ -230,19 +245,29 @@ module.exports = Class.create({
230
245
 
231
246
  // check cache first
232
247
  if (this.cache && !is_binary && this.cache.has(key)) {
233
- var data = this.cache.get(key);
248
+ var item = this.cache.getMeta(key);
234
249
 
235
- process.nextTick( function() {
236
- try { data = JSON.parse( data.toString() ); }
237
- catch (e) {
238
- self.logError('file', "Failed to parse JSON record: " + key + ": " + e);
239
- callback( e, null );
240
- return;
241
- }
242
- self.logDebug(9, "Cached JSON fetch complete: " + key, self.debugLevel(10) ? data : null);
243
-
244
- callback( null, data );
245
- } );
250
+ if (item.value.length == 0) {
251
+ process.nextTick( function() {
252
+ var err = new Error("Failed to fetch key: " + key + ": Not found");
253
+ err.code = "NoSuchKey";
254
+ callback( err );
255
+ } );
256
+ }
257
+ else {
258
+ process.nextTick( function() {
259
+ var data = null;
260
+ try { data = JSON.parse( item.value.toString() ); }
261
+ catch (e) {
262
+ self.logError('file', "Failed to parse JSON record: " + key + ": " + e);
263
+ callback( e, null );
264
+ return;
265
+ }
266
+ self.logDebug(9, "Cached JSON fetch complete: " + key, self.debugLevel(10) ? data : null);
267
+
268
+ callback( null, data );
269
+ } );
270
+ }
246
271
  return;
247
272
  } // cache
248
273
 
@@ -263,6 +288,12 @@ module.exports = Class.create({
263
288
  if (!row) {
264
289
  var err = new Error("Failed to fetch key: " + key + ": Not found");
265
290
  err.code = "NoSuchKey";
291
+
292
+ if (self.cache && !is_binary) {
293
+ // store 'empty' stub in cache
294
+ self.cache.set( key, Buffer.alloc(0), { date: 0 } );
295
+ }
296
+
266
297
  return callback(err, null);
267
298
  }
268
299
  if (is_binary) {
@@ -399,11 +430,20 @@ module.exports = Class.create({
399
430
  if (!info.changes) {
400
431
  var err = new Error("Failed to delete object: " + key + ": Not found");
401
432
  err.code = "NoSuchKey";
433
+
434
+ if (self.cache && !self.storage.isBinaryKey(key)) {
435
+ // store 'empty' stub in cache
436
+ self.cache.set( key, Buffer.alloc(0), { date: 0 } );
437
+ }
438
+
402
439
  return callback(err);
403
440
  }
404
441
  self.logDebug(9, "Delete complete: " + key);
405
- if (self.cache && self.cache.has(key)) {
406
- self.cache.delete(key);
442
+
443
+ // possibly "delete" from LRU cache as well
444
+ if (self.cache && !self.storage.isBinaryKey(key)) {
445
+ // store 'empty' stub in cache
446
+ self.cache.set( key, Buffer.alloc(0), { date: 0 } );
407
447
  }
408
448
  callback();
409
449
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pixl-server-storage",
3
- "version": "4.1.0",
3
+ "version": "4.1.1",
4
4
  "description": "A key/value/list storage component for the pixl-server framework.",
5
5
  "author": "Joseph Huckaby <jhuckaby@gmail.com>",
6
6
  "homepage": "https://github.com/jhuckaby/pixl-server-storage",