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/README.md +18 -2
- package/docs/API.md +58 -0
- package/docs/Indexer.md +62 -0
- package/docs/Transactions.md +102 -60
- package/engines/Hybrid.js +104 -2
- package/engines/Postgres.js +231 -3
- package/engines/Redis.js +133 -0
- package/engines/RedisCluster.js +24 -0
- package/engines/SQLite.js +193 -1
- package/package.json +1 -1
- package/storage.js +63 -1
- package/test/config.json +4 -0
- package/test/test-main.js +17 -0
- package/test/test.js +59 -38
- package/transaction.js +105 -3
- package/test/data/backups/backup-2026-02-15-18-10-45.db.gz +0 -0
- package/test/data/backups/backup-2026-02-15-18-10-58.db.gz +0 -0
- package/test/data/backups/backup-2026-02-15-18-11-10.db.gz +0 -0
- package/test/data/backups/backup-2026-02-15-18-11-23.db.gz +0 -0
- package/test/data/backups/backup-2026-02-15-18-11-35.db.gz +0 -0
- package/test/data/backups/backup-2026-04-05-20-23-56.db.gz +0 -0
- package/test/data/backups/backup-2026-06-08-20-33-01.db.gz +0 -0
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.
|
|
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
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
76
|
-
|
|
82
|
+
// now add transaction tests, which enable and init transactions
|
|
83
|
+
self.tests = self.tests.concat( transactionTests.tests );
|
|
77
84
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
131
|
-
|
|
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 =
|
|
127
|
+
var data = value.data;
|
|
127
128
|
self.logDebug(9, "JSON fetch complete: " + key, self.debugLevel(10) ? data : null);
|
|
128
|
-
callback(
|
|
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) {
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|