pixl-server-storage 4.1.1 → 4.2.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/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 {
@@ -449,6 +449,92 @@ module.exports = Class.create({
449
449
  });
450
450
  },
451
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);
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
+
534
+ callback();
535
+ });
536
+ },
537
+
452
538
  runMaintenance: function(callback) {
453
539
  // run daily maintenance
454
540
  // config.backups: { enabled, dir, filename, compress?, keep? }
@@ -592,6 +678,112 @@ module.exports = Class.create({
592
678
  });
593
679
  },
594
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
+
595
787
  shutdown: function(callback) {
596
788
  // shutdown storage
597
789
  var self = this;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pixl-server-storage",
3
- "version": "4.1.1",
3
+ "version": "4.2.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",
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);
package/test/test.js CHANGED
@@ -67,44 +67,54 @@ module.exports = {
67
67
  // save ref to storage
68
68
  self.storage = server.Storage;
69
69
 
70
- // build our test array dynamically
71
- // we have some repeating tests with different configuration options
72
- self.tests = self.tests.concat(
73
- listTests.tests,
70
+ var finishStartup = function() {
71
+ // build our test array dynamically
72
+ // we have some repeating tests with different configuration options
73
+ self.tests = self.tests.concat(
74
+ listTests.tests,
75
+
76
+ (self.args.splice || self.args.all || self.args.comprehensive) ?
77
+ listTests.generateSpliceTests() : [],
78
+
79
+ hashTests.tests
80
+ );
74
81
 
75
- (self.args.splice || self.args.all || self.args.comprehensive) ?
76
- listTests.generateSpliceTests() : [],
82
+ // now add transaction tests, which enable and init transactions
83
+ self.tests = self.tests.concat( transactionTests.tests );
77
84
 
78
- hashTests.tests
79
- );
80
-
81
- // now add transaction tests, which enable and init transactions
82
- self.tests = self.tests.concat( transactionTests.tests );
83
-
84
- // now repeat list and hash tests, with transactions enabled
85
- // augment test names for clarity
86
- [].concat( listTests.tests, hashTests.tests ).forEach( function(func) {
87
- var wrapper = function(test) { func.apply(this, [test]); };
88
- wrapper.testName = 'Transaction_' + func.name;
89
- self.tests.push( wrapper );
90
- } );
91
-
92
- // finally add indexer tests
93
- self.tests = self.tests.concat( indexerTests.tests );
94
-
95
- // log memory usage at end
96
- self.tests.push( function(test) {
97
- if (global.gc) { global.gc(); global.gc(); }
98
- test.ok( true );
99
- setTimeout( function() {
100
- self.storage.logDebug(9, "MEMORY USAGE: " + Tools.getTextFromBytes( process.memoryUsage.rss() ), process.memoryUsage() );
101
- test.done();
102
- }, 1 );
103
- } );
85
+ // now repeat list and hash tests, with transactions enabled
86
+ // augment test names for clarity
87
+ [].concat( listTests.tests, hashTests.tests ).forEach( function(func) {
88
+ var wrapper = function(test) { func.apply(this, [test]); };
89
+ wrapper.testName = 'Transaction_' + func.name;
90
+ self.tests.push( wrapper );
91
+ } );
92
+
93
+ // finally add indexer tests
94
+ self.tests = self.tests.concat( indexerTests.tests );
95
+
96
+ // log memory usage at end
97
+ self.tests.push( function(test) {
98
+ if (global.gc) { global.gc(); global.gc(); }
99
+ test.ok( true );
100
+ setTimeout( function() {
101
+ self.storage.logDebug(9, "MEMORY USAGE: " + Tools.getTextFromBytes( process.memoryUsage.rss() ), process.memoryUsage() );
102
+ test.done();
103
+ }, 1 );
104
+ } );
105
+
106
+ // startup complete
107
+ // delay this by 1ms so the log is in the correct order (pre-start is async)
108
+ setTimeout( function() { callback(); }, 1 );
109
+ };
104
110
 
105
- // startup complete
106
- // delay this by 1ms so the log is in the correct order (pre-start is async)
107
- setTimeout( function() { callback(); }, 1 );
111
+ if (self.storage.engine.unitTestCleanup) {
112
+ self.storage.engine.unitTestCleanup( function(err) {
113
+ if (err) return callback(err);
114
+ finishStartup();
115
+ } );
116
+ }
117
+ else finishStartup();
108
118
  } ); // startup
109
119
  } ); // delete
110
120
  },
@@ -127,9 +137,20 @@ module.exports = {
127
137
 
128
138
  tearDown: function (callback) {
129
139
  // clean up
130
- this.server.shutdown( function() {
131
- cp.exec("rm -rf transactions " + base_data_dir, callback);
132
- } );
140
+ var self = this;
141
+
142
+ var finishShutdown = function(cleanup_err) {
143
+ self.server.shutdown( function(shutdown_err) {
144
+ cp.exec("rm -rf transactions " + base_data_dir, function(rm_err) {
145
+ callback( cleanup_err || shutdown_err || rm_err );
146
+ } );
147
+ } );
148
+ };
149
+
150
+ if (this.storage && this.storage.engine && this.storage.engine.unitTestCleanup) {
151
+ this.storage.engine.unitTestCleanup( finishShutdown );
152
+ }
153
+ else finishShutdown();
133
154
  },
134
155
 
135
156
  multiCheck: function(map, storage, callback) {
package/transaction.js CHANGED
@@ -80,10 +80,10 @@ var TransStorageFunctions = {
80
80
  if (trans.keys[key] == 'W') {
81
81
  // we've written the key, so fetch our version
82
82
  this.logDebug(9, "Pinging Object from transaction: " + key);
83
+ var value = trans.values[key];
83
84
 
84
85
  setImmediate( function() {
85
86
  self.logDebug(9, "Head complete: " + key);
86
- var value = trans.values[key];
87
87
  callback( null, {
88
88
  mod: value.mod,
89
89
  len: value.len
@@ -121,11 +121,12 @@ var TransStorageFunctions = {
121
121
  if (trans.keys[key] == 'W') {
122
122
  // we've written the key, so fetch our version
123
123
  this.logDebug(9, "Fetching Object in transaction: " + key);
124
+ var value = trans.values[key];
124
125
 
125
126
  setImmediate( function() {
126
- var data = trans.values[key].data;
127
+ var data = value.data;
127
128
  self.logDebug(9, "JSON fetch complete: " + key, self.debugLevel(10) ? data : null);
128
- callback( err, Tools.copyHash(data, true) );
129
+ callback( null, Tools.copyHash(data, true) );
129
130
  } );
130
131
  }
131
132
  else if (trans.keys[key] == 'D') {
@@ -700,6 +701,107 @@ module.exports = Class.create({
700
701
  var num_bytes = 0;
701
702
  var pf = this.perf.begin('commit');
702
703
 
704
+ if (this.engine.commitTransaction) {
705
+ // native engine transaction support replaces our entire rollback-log commit system.
706
+ // The engine is responsible for applying all record changes atomically, using the
707
+ // full transaction object. We still own the outer locks, events, logs and cleanup.
708
+ async.waterfall(
709
+ [
710
+ function(callback) {
711
+ // acquire commit lock
712
+ self.lock( 'C|'+path, true, function() { callback(); } );
713
+ },
714
+ function(callback) {
715
+ // notify listeners that the commit is starting
716
+ self.emit('commitStart', trans);
717
+
718
+ // ask the engine to atomically commit all actions
719
+ self.engine.commitTransaction( trans, callback );
720
+ }
721
+ ],
722
+ function(err) {
723
+ // commit complete
724
+ var elapsed = pf.end();
725
+
726
+ if (err) {
727
+ var msg = "Failed to commit transaction: " + path + ": " + err.message;
728
+ self.logError('commit', msg, { id: trans.id });
729
+
730
+ if (err.fatal) {
731
+ // Native engine detected an unsafe transaction outcome. Do not fall
732
+ // through to abortTransaction(), as there is no rollback log to replay.
733
+ var fatal_err = new Error(msg);
734
+ fatal_err.fatal = true;
735
+ fatal_err.originalError = err;
736
+ return self.transFatalError(fatal_err);
737
+ }
738
+
739
+ // Note: in this case unlocking happens in abortTransaction
740
+ return callback( new Error(msg) );
741
+ }
742
+
743
+ // emulate the normal per-record put/delete side effects, since native
744
+ // engine commits bypass self.put() and self.delete() for atomicity.
745
+ Object.keys(trans.keys).forEach( function(key) {
746
+ var record_state = trans.keys[key];
747
+ var norm_key = self.normalizeKey(key);
748
+
749
+ if (record_state == 'W') {
750
+ var value = trans.values[key];
751
+ num_bytes += value.len;
752
+
753
+ // update the Storage-level RAM cache, if enabled for this key
754
+ if (self.cacheKeyRegex && norm_key.match(self.cacheKeyRegex)) {
755
+ self.cache[norm_key] = value.data;
756
+ }
757
+
758
+ self.logTransaction('put', norm_key, {
759
+ elapsed_ms: 0
760
+ });
761
+ self.emit('put', norm_key, value.data);
762
+ }
763
+ else if (record_state == 'D') {
764
+ // update the Storage-level RAM cache, if enabled for this key
765
+ if (self.cacheKeyRegex && norm_key.match(self.cacheKeyRegex) && (norm_key in self.cache)) {
766
+ delete self.cache[norm_key];
767
+ }
768
+
769
+ self.logTransaction('delete', norm_key, {
770
+ elapsed_ms: 0
771
+ });
772
+ self.emit('delete', norm_key);
773
+ }
774
+ });
775
+
776
+ self.logDebug(5, "Transaction committed successfully: " + trans.id, { path: path, actions: num_actions });
777
+ self.logTransaction('commit', path, {
778
+ id: trans.id,
779
+ elapsed_ms: elapsed,
780
+ actions: num_actions,
781
+ bytes_written: num_bytes
782
+ });
783
+
784
+ // transaction is complete
785
+ delete trans.values; // release memory
786
+ delete self.transactions[path];
787
+
788
+ // enqueue any pending tasks that got added during the transaction
789
+ if (trans.queue.length) {
790
+ trans.queue.forEach( self.enqueue.bind(self) );
791
+ trans.queue = []; // release memory
792
+ }
793
+
794
+ delete trans.keys; // release memory
795
+
796
+ self.emit('commitEnd', trans);
797
+ self.unlock( 'C|'+path );
798
+ self._transUnlock(path);
799
+ callback();
800
+ }
801
+ ); // native engine commit
802
+ return;
803
+ }
804
+
703
805
  async.waterfall(
704
806
  [
705
807
  function(callback) {