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/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
@@ -79,7 +79,7 @@ module.exports = Class.create({
79
79
  });
80
80
  }
81
81
 
82
- var db_file = Path.join( this.baseDir, sql_config.filename );
82
+ var db_file = this.dbFile = Path.join( this.baseDir, sql_config.filename );
83
83
  this.logDebug(3, "Opening database file: " + db_file, sql_config.pragmas || {});
84
84
 
85
85
  try {
@@ -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,12 +430,107 @@ 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 } );
447
+ }
448
+ callback();
449
+ });
450
+ },
451
+
452
+ commitTransaction: function(trans, callback) {
453
+ // commit all transaction actions using a native SQLite transaction
454
+ var self = this;
455
+ var write_keys = [];
456
+ var write_values = [];
457
+ var write_modified = [];
458
+ var delete_keys = [];
459
+ var bad_state = null;
460
+
461
+ this.logDebug(5, "Beginning transaction commit: " + trans.id, {
462
+ path: trans.path,
463
+ actions: Tools.numKeys(trans.keys)
464
+ });
465
+
466
+ Object.keys(trans.keys).forEach( function(key) {
467
+ var record_state = trans.keys[key];
468
+ var norm_key = self.storage.normalizeKey(key);
469
+
470
+ if (record_state == 'W') {
471
+ // Collect writes into parallel arrays so we can update the cache later
472
+ // without stringifying and Buffer-wrapping JSON a second time.
473
+ var item = trans.values[key];
474
+ write_keys.push( self.prepKey(norm_key) );
475
+ write_values.push( Buffer.from( JSON.stringify( item.data ) ) );
476
+ write_modified.push( item.mod );
477
+ }
478
+ else if (record_state == 'D') {
479
+ // Collect deletes into the same native transaction.
480
+ delete_keys.push( self.prepKey(norm_key) );
481
+ }
482
+ else {
483
+ bad_state = new Error("Unknown transaction record state: " + record_state + ": " + key);
484
+ }
485
+ });
486
+
487
+ if (bad_state) return callback(bad_state);
488
+
489
+ process.nextTick(function() {
490
+ try {
491
+ var put_stmt = self.db.prepare(self.commands.put);
492
+ var delete_stmt = self.db.prepare(self.commands.delete);
493
+
494
+ var commit = self.db.transaction(function() {
495
+ delete_keys.forEach( function(key) {
496
+ delete_stmt.run({ key: key });
497
+ });
498
+
499
+ write_keys.forEach( function(key, idx) {
500
+ put_stmt.run({ key: key, value: write_values[idx], now: write_modified[idx] });
501
+ });
502
+ });
503
+
504
+ // Ask SQLite for the write transaction up front, since every commit
505
+ // replay here mutates rows.
506
+ commit.immediate();
507
+ }
508
+ catch (err) {
509
+ err.message = "Failed to commit transaction: " + trans.id + ": " + err;
510
+ self.logError('sqlite', '' + err);
511
+ return callback(err);
407
512
  }
513
+
514
+ // commit succeeded, so now it is safe to update the engine LRU cache
515
+ if (self.cache) {
516
+ write_keys.forEach( function(key, idx) {
517
+ if (!self.storage.isBinaryKey(key)) {
518
+ self.cache.set( key, write_values[idx], { date: write_modified[idx] } );
519
+ }
520
+ });
521
+
522
+ delete_keys.forEach( function(key) {
523
+ if (!self.storage.isBinaryKey(key)) {
524
+ // store 'empty' stub in cache
525
+ self.cache.set( key, Buffer.alloc(0), { date: 0 } );
526
+ }
527
+ });
528
+ }
529
+
530
+ self.logDebug(5, "Transaction commit complete: " + trans.id, {
531
+ path: trans.path
532
+ });
533
+
408
534
  callback();
409
535
  });
410
536
  },
@@ -552,6 +678,112 @@ module.exports = Class.create({
552
678
  });
553
679
  },
554
680
 
681
+ optimize: function(callback) {
682
+ // compact the SQLite database and run an integrity check
683
+ var self = this;
684
+ var perf = new Perf();
685
+ var report = {
686
+ engine: this.__name,
687
+ optimized: true,
688
+ file: this.dbFile,
689
+ operations: []
690
+ };
691
+
692
+ this.logDebug(3, "Running SQLite optimization");
693
+ perf.setScale(1);
694
+ perf.begin();
695
+
696
+ process.nextTick(function() {
697
+ try {
698
+ if (!self.db) throw new Error("SQLite database is not open.");
699
+ if (!self.dbFile || !fs.existsSync(self.dbFile)) throw new Error("SQLite database file could not be found.");
700
+
701
+ var old_size = fs.statSync(self.dbFile).size;
702
+
703
+ self.logDebug(7, "Running SQLite VACUUM");
704
+ perf.begin('vacuum');
705
+ self.db.exec("VACUUM;");
706
+ perf.end('vacuum');
707
+
708
+ var new_size = fs.statSync(self.dbFile).size;
709
+ var bytes_reclaimed = Math.max(0, old_size - new_size);
710
+
711
+ report.size_before = old_size;
712
+ report.size_after = new_size;
713
+ report.bytes_reclaimed = bytes_reclaimed;
714
+ report.operations.push({
715
+ name: 'vacuum',
716
+ ok: true,
717
+ size_before: old_size,
718
+ size_after: new_size,
719
+ bytes_reclaimed: bytes_reclaimed,
720
+ reclaimed: Tools.getTextFromBytes(bytes_reclaimed)
721
+ });
722
+
723
+ // If the DB is in WAL mode, truncate the WAL file after VACUUM
724
+ // so the caller sees disk usage reclaimed immediately.
725
+ var journal_mode = '';
726
+ try { journal_mode = ('' + self.db.pragma('journal_mode', { simple: true })).toUpperCase(); }
727
+ catch (err) {
728
+ var pragmas = self.config.get('pragmas') || {};
729
+ journal_mode = ('' + (pragmas.journal_mode || '')).toUpperCase();
730
+ }
731
+
732
+ if (journal_mode == 'WAL') {
733
+ self.logDebug(7, "Running SQLite WAL checkpoint");
734
+
735
+ perf.begin('checkpoint');
736
+ var rows = self.db.prepare("PRAGMA wal_checkpoint(TRUNCATE);").all();
737
+ perf.end('checkpoint');
738
+ var row = rows && rows[0] ? rows[0] : {};
739
+ var busy = row.busy || 0;
740
+
741
+ report.operations.push({
742
+ name: 'wal_checkpoint',
743
+ ok: !busy,
744
+ mode: 'TRUNCATE',
745
+ journal_mode: journal_mode,
746
+ busy: busy,
747
+ log: row.log || 0,
748
+ checkpointed: row.checkpointed || 0
749
+ });
750
+ }
751
+
752
+ self.logDebug(7, "Running SQLite integrity check");
753
+
754
+ perf.begin('integrity');
755
+ var rows = self.db.prepare("PRAGMA integrity_check;").all();
756
+ perf.end('integrity');
757
+ var results = rows.map( function(row) {
758
+ return ('' + row.integrity_check).trim();
759
+ }).filter( function(text) {
760
+ return !!text;
761
+ });
762
+
763
+ var result = results.join("\n") || "No response from integrity check.";
764
+ var ok = (result.toLowerCase() == 'ok');
765
+
766
+ report.integrity_ok = ok;
767
+ report.integrity_check = result;
768
+ report.operations.push({
769
+ name: 'integrity_check',
770
+ ok: ok,
771
+ result: result
772
+ });
773
+
774
+ perf.end();
775
+ report.perf = perf.metrics();
776
+ self.logDebug(3, "SQLite optimization complete", report);
777
+ callback(null, report);
778
+ }
779
+ catch (err) {
780
+ err.message = "SQLite optimization failed: " + err.message;
781
+ self.logError('sqlite', '' + err);
782
+ callback(err);
783
+ }
784
+ });
785
+ },
786
+
555
787
  shutdown: function(callback) {
556
788
  // shutdown storage
557
789
  var self = this;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pixl-server-storage",
3
- "version": "4.1.0",
3
+ "version": "4.2.0",
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",
package/storage.js CHANGED
@@ -30,7 +30,7 @@ module.exports = Class.create({
30
30
  maintenance: 0,
31
31
  log_event_types: {
32
32
  all:0, get:0, put:0, head:0, delete:0, expire_set:0, perf_sec:0, perf_min:0,
33
- commit:0, index:0, unindex:0, search:0, sort:0, maint:1
33
+ commit:0, index:0, unindex:0, search:0, sort:0, maint:1, optimize:1
34
34
  },
35
35
  max_recent_events: 0,
36
36
  cache_key_match: '',
@@ -787,6 +787,68 @@ module.exports = Class.create({
787
787
  ); // listEach
788
788
  },
789
789
 
790
+ optimize: function(callback) {
791
+ // run engine-specific storage optimization, if supported
792
+ var self = this;
793
+ if (!callback) callback = function() {};
794
+ if (!this.started) return callback( new Error("Storage has not completed startup.") );
795
+
796
+ // Optimize operates on the live backing store, so it should only be run
797
+ // from the root storage instance, after queued writes and locks settle.
798
+ if (this.currentTransactionPath) {
799
+ return callback( new Error("Cannot optimize storage from inside a transaction.") );
800
+ }
801
+
802
+ this.logDebug(3, "Starting storage optimization");
803
+
804
+ this.waitForQueueDrain( function() {
805
+ self.waitForAllLocks( function() {
806
+ var pf = self.perf.begin('optimize');
807
+ var engine = self.engine;
808
+ var optimize = engine && engine.optimize;
809
+
810
+ if (!optimize) {
811
+ pf.end();
812
+ var perf = new Perf();
813
+ perf.setScale(1);
814
+ perf.begin();
815
+ perf.begin('noop');
816
+ perf.end('noop');
817
+ perf.end();
818
+
819
+ var report = {
820
+ engine: engine ? engine.__name : '',
821
+ optimized: false,
822
+ perf: perf.metrics(),
823
+ operations: [
824
+ {
825
+ name: 'noop',
826
+ ok: true,
827
+ message: "No optimization is available for this engine."
828
+ }
829
+ ]
830
+ };
831
+
832
+ self.logTransaction('optimize', report.engine || 'none', report);
833
+ return callback(null, report);
834
+ }
835
+
836
+ optimize.call( engine, function(err, report) {
837
+ pf.end();
838
+ if (err) return callback(err);
839
+
840
+ if (!report) report = {};
841
+ if (!report.engine) report.engine = engine.__name;
842
+ if (typeof(report.optimized) == 'undefined') report.optimized = true;
843
+
844
+ self.logDebug(3, "Storage optimization complete", report);
845
+ self.logTransaction('optimize', report.engine || 'none', report);
846
+ callback(null, report);
847
+ } );
848
+ } ); // waitForLocks
849
+ } ); // waitForQueueDrain
850
+ },
851
+
790
852
  lock: function(key, wait, callback) {
791
853
  // lock key in exclusive mode, possibly wait until acquired
792
854
  if (!this.started) return callback( new Error("Storage has not completed startup.") );
package/test/config.json CHANGED
@@ -14,6 +14,10 @@
14
14
  "transactions": 0,
15
15
  "log_event_types": { "all": 1 },
16
16
 
17
+ "Hybrid": {
18
+ "docEngine": "SQLite",
19
+ "binaryEngine": "Filesystem"
20
+ },
17
21
  "Filesystem": {
18
22
  "base_dir": "data",
19
23
  "key_namespaces": 0,
package/test/test-main.js CHANGED
@@ -656,6 +656,23 @@ module.exports = {
656
656
  } );
657
657
  },
658
658
 
659
+ function testOptimize(test) {
660
+ // run engine-specific optimization, or a safe no-op if unsupported
661
+ test.expect(8);
662
+
663
+ this.storage.optimize( function(err, report) {
664
+ test.ok( !err, "No error running optimize: " + err );
665
+ test.ok( !!report, "Got an optimize report" );
666
+ test.ok( !!report.engine, "Optimize report has an engine name" );
667
+ test.ok( Array.isArray(report.operations), "Optimize report has operations" );
668
+ test.ok( report.operations.length >= 1, "Optimize report has at least one operation" );
669
+ test.ok( !!report.perf, "Optimize report has perf metrics" );
670
+ test.ok( report.perf.scale == 1, "Optimize perf metrics are in seconds" );
671
+ test.ok( typeof(report.perf.perf.total) == 'number', "Optimize perf metrics have a total" );
672
+ test.done();
673
+ } );
674
+ },
675
+
659
676
  function testMaintenance(test) {
660
677
  var self = this;
661
678
  test.expect(3);