pixl-server-storage 4.1.1 → 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/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/data/backups/backup-2026-06-14-15-49-56.db.gz +0 -0
- package/test/data/backups/backup-2026-06-14-15-58-00.db.gz +0 -0
- package/test/data/backups/backup-2026-06-14-16-29-30.db.gz +0 -0
- package/test/data/backups/backup-2026-06-14-17-29-36.db.gz +0 -0
- package/test/data/backups/backup-2026-06-14-17-31-49.db.gz +0 -0
- package/test/data/backups/backup-2026-06-14-18-02-10.db.gz +0 -0
- package/test/data/backups/backup-2026-06-14-18-02-55.db.gz +0 -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/Hybrid.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
var Class = require("pixl-class");
|
|
6
6
|
var Component = require("pixl-server/component");
|
|
7
|
-
var
|
|
7
|
+
var Perf = require("pixl-perf");
|
|
8
8
|
var async = require('async');
|
|
9
9
|
|
|
10
10
|
module.exports = Class.create({
|
|
@@ -20,7 +20,8 @@ module.exports = Class.create({
|
|
|
20
20
|
startup: function(callback) {
|
|
21
21
|
// setup multiple sub-engines
|
|
22
22
|
this.logDebug(2, "Setting up hybrid engine", this.config.get() );
|
|
23
|
-
|
|
23
|
+
var self = this;
|
|
24
|
+
|
|
24
25
|
var binaryClass = require( './' + this.config.get('binaryEngine') + '.js' );
|
|
25
26
|
this.binaryEngine = new binaryClass();
|
|
26
27
|
this.binaryEngine.storage = this.storage;
|
|
@@ -31,6 +32,17 @@ module.exports = Class.create({
|
|
|
31
32
|
this.docEngine.storage = this.storage;
|
|
32
33
|
this.docEngine.init( this.server, this.storage.config.getSub( this.config.get('docEngine') ) );
|
|
33
34
|
|
|
35
|
+
// Native transactions only apply to document records. Binary keys are
|
|
36
|
+
// already passed straight through by the outer transaction wrapper, so
|
|
37
|
+
// only expose this hook if the doc engine can atomically commit JSON.
|
|
38
|
+
delete this.commitTransaction;
|
|
39
|
+
if (this.docEngine.commitTransaction) {
|
|
40
|
+
this.logDebug(3, "Hybrid engine will use native doc engine transactions");
|
|
41
|
+
this.commitTransaction = function(trans, callback) {
|
|
42
|
+
self.docEngine.commitTransaction( trans, callback );
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
34
46
|
async.series([
|
|
35
47
|
this.binaryEngine.startup.bind(this.binaryEngine),
|
|
36
48
|
this.docEngine.startup.bind(this.docEngine)
|
|
@@ -115,6 +127,96 @@ module.exports = Class.create({
|
|
|
115
127
|
], callback );
|
|
116
128
|
},
|
|
117
129
|
|
|
130
|
+
optimize: function(callback) {
|
|
131
|
+
// run optimization on child engines that support it
|
|
132
|
+
var self = this;
|
|
133
|
+
var perf = new Perf();
|
|
134
|
+
var results = [];
|
|
135
|
+
|
|
136
|
+
this.logDebug(3, "Running hybrid optimization");
|
|
137
|
+
perf.setScale(1);
|
|
138
|
+
perf.begin();
|
|
139
|
+
|
|
140
|
+
var optimizeEngine = function(label, engine, callback) {
|
|
141
|
+
var tracker = perf.begin(label);
|
|
142
|
+
|
|
143
|
+
if (!engine || !engine.optimize) {
|
|
144
|
+
var child_perf = new Perf();
|
|
145
|
+
child_perf.setScale(1);
|
|
146
|
+
child_perf.begin();
|
|
147
|
+
child_perf.begin('noop');
|
|
148
|
+
child_perf.end('noop');
|
|
149
|
+
child_perf.end();
|
|
150
|
+
tracker.end();
|
|
151
|
+
|
|
152
|
+
results.push({
|
|
153
|
+
role: label,
|
|
154
|
+
engine: engine ? engine.__name : '',
|
|
155
|
+
optimized: false,
|
|
156
|
+
perf: child_perf.metrics(),
|
|
157
|
+
operations: [
|
|
158
|
+
{
|
|
159
|
+
name: 'noop',
|
|
160
|
+
ok: true,
|
|
161
|
+
message: "No optimization is available for this engine."
|
|
162
|
+
}
|
|
163
|
+
]
|
|
164
|
+
});
|
|
165
|
+
return process.nextTick(callback);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
engine.optimize( function(err, report) {
|
|
169
|
+
tracker.end();
|
|
170
|
+
if (err) return callback(err);
|
|
171
|
+
if (!report) report = {};
|
|
172
|
+
report.role = label;
|
|
173
|
+
if (!report.engine) report.engine = engine.__name;
|
|
174
|
+
results.push(report);
|
|
175
|
+
callback();
|
|
176
|
+
} );
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
async.series([
|
|
180
|
+
function(callback) {
|
|
181
|
+
optimizeEngine( 'binary', self.binaryEngine, callback );
|
|
182
|
+
},
|
|
183
|
+
function(callback) {
|
|
184
|
+
optimizeEngine( 'doc', self.docEngine, callback );
|
|
185
|
+
}
|
|
186
|
+
], function(err) {
|
|
187
|
+
if (err) return callback(err);
|
|
188
|
+
perf.end();
|
|
189
|
+
|
|
190
|
+
var report = {
|
|
191
|
+
engine: self.__name,
|
|
192
|
+
optimized: results.some( function(item) { return !!item.optimized; } ),
|
|
193
|
+
perf: perf.metrics(),
|
|
194
|
+
engines: results
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
report.operations = results.reduce( function(list, item) {
|
|
198
|
+
return list.concat( item.operations || [] );
|
|
199
|
+
}, [] );
|
|
200
|
+
|
|
201
|
+
callback(null, report);
|
|
202
|
+
} );
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
unitTestCleanup: function(callback) {
|
|
206
|
+
// cleanup all child engines that provide a unit test cleanup hook
|
|
207
|
+
var tasks = [];
|
|
208
|
+
|
|
209
|
+
if (this.binaryEngine && this.binaryEngine.unitTestCleanup) {
|
|
210
|
+
tasks.push( this.binaryEngine.unitTestCleanup.bind(this.binaryEngine) );
|
|
211
|
+
}
|
|
212
|
+
if (this.docEngine && this.docEngine.unitTestCleanup) {
|
|
213
|
+
tasks.push( this.docEngine.unitTestCleanup.bind(this.docEngine) );
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (!tasks.length) return process.nextTick(callback);
|
|
217
|
+
async.series( tasks, callback );
|
|
218
|
+
},
|
|
219
|
+
|
|
118
220
|
shutdown: function(callback) {
|
|
119
221
|
// shutdown storage
|
|
120
222
|
this.logDebug(2, "Shutting down hybrid system");
|
package/engines/Postgres.js
CHANGED
|
@@ -53,7 +53,9 @@ module.exports = Class.create({
|
|
|
53
53
|
get: `SELECT modified, value FROM ${pg_config.table} WHERE key = $key LIMIT 1`,
|
|
54
54
|
head: `SELECT modified, octet_length(value) AS len FROM ${pg_config.table} WHERE key = $key LIMIT 1`,
|
|
55
55
|
put: `INSERT INTO ${pg_config.table} (key, value, modified) VALUES ($key, $value, $now) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, modified = EXCLUDED.modified`,
|
|
56
|
-
delete: `DELETE FROM ${pg_config.table} WHERE key = $key
|
|
56
|
+
delete: `DELETE FROM ${pg_config.table} WHERE key = $key`,
|
|
57
|
+
transPut: `INSERT INTO ${pg_config.table} (key, value, modified) SELECT * FROM unnest($keys::text[], $values::bytea[], $modified::bigint[]) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, modified = EXCLUDED.modified`,
|
|
58
|
+
transDelete: `DELETE FROM ${pg_config.table} WHERE key = ANY($keys::text[])`
|
|
57
59
|
};
|
|
58
60
|
|
|
59
61
|
// optional LRU cache
|
|
@@ -98,9 +100,15 @@ module.exports = Class.create({
|
|
|
98
100
|
},
|
|
99
101
|
|
|
100
102
|
query(sql, args, callback) {
|
|
103
|
+
// run query against the pool
|
|
104
|
+
this.queryWithClient( this.db, sql, args, callback );
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
queryWithClient: function(client, sql, args, callback) {
|
|
101
108
|
// run and log db query, and error if applicable
|
|
102
109
|
// allow for named variables in sql, expand to numbered $ variables for pg
|
|
103
110
|
var self = this;
|
|
111
|
+
if (!args) args = {};
|
|
104
112
|
|
|
105
113
|
// perform $ placeholder substitution to populate sargs array
|
|
106
114
|
var sargs = [];
|
|
@@ -111,7 +119,7 @@ module.exports = Class.create({
|
|
|
111
119
|
return value;
|
|
112
120
|
} );
|
|
113
121
|
|
|
114
|
-
|
|
122
|
+
client.query( sql, sargs, function(err, res) {
|
|
115
123
|
if (err) {
|
|
116
124
|
self.logError('db', "Failed to execute SQL: " + err, { sql, err });
|
|
117
125
|
return callback(err);
|
|
@@ -123,7 +131,7 @@ module.exports = Class.create({
|
|
|
123
131
|
});
|
|
124
132
|
|
|
125
133
|
callback(null, res);
|
|
126
|
-
} ); //
|
|
134
|
+
} ); // client.query
|
|
127
135
|
},
|
|
128
136
|
|
|
129
137
|
prepKey: function(key) {
|
|
@@ -462,11 +470,231 @@ module.exports = Class.create({
|
|
|
462
470
|
} ); // query
|
|
463
471
|
},
|
|
464
472
|
|
|
473
|
+
commitTransaction: function(trans, callback) {
|
|
474
|
+
// commit all transaction actions using a native Postgres transaction
|
|
475
|
+
// IMPORTANT: all queries must run on this one checked-out client
|
|
476
|
+
var self = this;
|
|
477
|
+
var did_begin = false;
|
|
478
|
+
var write_keys = [];
|
|
479
|
+
var write_values = [];
|
|
480
|
+
var write_modified = [];
|
|
481
|
+
var delete_keys = [];
|
|
482
|
+
var bad_state = null;
|
|
483
|
+
var query_step = '';
|
|
484
|
+
|
|
485
|
+
this.logDebug(5, "Beginning transaction commit: " + trans.id, {
|
|
486
|
+
path: trans.path,
|
|
487
|
+
actions: Tools.numKeys(trans.keys)
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
Object.keys(trans.keys).forEach( function(key) {
|
|
491
|
+
var record_state = trans.keys[key];
|
|
492
|
+
var norm_key = self.storage.normalizeKey(key);
|
|
493
|
+
|
|
494
|
+
if (record_state == 'W') {
|
|
495
|
+
// Collect writes into parallel arrays for one bulk UPSERT below.
|
|
496
|
+
var item = trans.values[key];
|
|
497
|
+
write_keys.push( self.prepKey(norm_key) );
|
|
498
|
+
write_values.push( Buffer.from( JSON.stringify( item.data ) ) );
|
|
499
|
+
write_modified.push( item.mod );
|
|
500
|
+
}
|
|
501
|
+
else if (record_state == 'D') {
|
|
502
|
+
// Collect deletes into one bulk DELETE below.
|
|
503
|
+
delete_keys.push( self.prepKey(norm_key) );
|
|
504
|
+
}
|
|
505
|
+
else {
|
|
506
|
+
bad_state = new Error("Unknown transaction record state: " + record_state + ": " + key);
|
|
507
|
+
}
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
if (bad_state) return callback(bad_state);
|
|
511
|
+
|
|
512
|
+
this.db.connect( function(err, client, release) {
|
|
513
|
+
if (err) {
|
|
514
|
+
self.logError('pg', "Failed to acquire Postgres client for transaction commit: " + err);
|
|
515
|
+
return callback(err);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
var releaseClient = function(err) {
|
|
519
|
+
// release the pg client exactly once
|
|
520
|
+
if (!release) return;
|
|
521
|
+
var rel = release;
|
|
522
|
+
release = null;
|
|
523
|
+
rel(err);
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
async.series(
|
|
527
|
+
[
|
|
528
|
+
function(callback) {
|
|
529
|
+
// start native Postgres transaction
|
|
530
|
+
query_step = 'BEGIN';
|
|
531
|
+
self.queryWithClient( client, "BEGIN", {}, function(err) {
|
|
532
|
+
if (!err) did_begin = true;
|
|
533
|
+
callback(err);
|
|
534
|
+
} );
|
|
535
|
+
},
|
|
536
|
+
function(callback) {
|
|
537
|
+
// delete all final-deleted records in one set-based SQL statement
|
|
538
|
+
if (!delete_keys.length) return callback();
|
|
539
|
+
|
|
540
|
+
query_step = 'DELETE';
|
|
541
|
+
self.queryWithClient( client, self.commands.transDelete, {
|
|
542
|
+
keys: delete_keys
|
|
543
|
+
}, function(err, res) {
|
|
544
|
+
if (err) return callback(err);
|
|
545
|
+
|
|
546
|
+
if (res.rowCount < delete_keys.length) {
|
|
547
|
+
self.logDebug(5, "Some records were already deleted", {
|
|
548
|
+
requested: delete_keys.length,
|
|
549
|
+
deleted: res.rowCount
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
callback();
|
|
554
|
+
} );
|
|
555
|
+
},
|
|
556
|
+
function(callback) {
|
|
557
|
+
// upsert all final-written records in one set-based SQL statement
|
|
558
|
+
if (!write_keys.length) return callback();
|
|
559
|
+
|
|
560
|
+
query_step = 'UPSERT';
|
|
561
|
+
self.queryWithClient( client, self.commands.transPut, {
|
|
562
|
+
keys: write_keys,
|
|
563
|
+
values: write_values,
|
|
564
|
+
modified: write_modified
|
|
565
|
+
}, callback );
|
|
566
|
+
},
|
|
567
|
+
function(callback) {
|
|
568
|
+
// atomically reveal all transaction actions
|
|
569
|
+
query_step = 'COMMIT';
|
|
570
|
+
self.queryWithClient( client, "COMMIT", {}, callback );
|
|
571
|
+
}
|
|
572
|
+
],
|
|
573
|
+
function(err) {
|
|
574
|
+
if (!err) {
|
|
575
|
+
// commit succeeded, so now it is safe to update the engine LRU cache
|
|
576
|
+
if (self.cache) {
|
|
577
|
+
write_keys.forEach( function(key, idx) {
|
|
578
|
+
if (!self.storage.isBinaryKey(key)) {
|
|
579
|
+
self.cache.set( key, write_values[idx], { date: write_modified[idx] } );
|
|
580
|
+
}
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
delete_keys.forEach( function(key) {
|
|
584
|
+
if (!self.storage.isBinaryKey(key)) {
|
|
585
|
+
// store 'empty' stub in cache
|
|
586
|
+
self.cache.set( key, Buffer.alloc(0), { date: 0 } );
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
self.logDebug(5, "Transaction commit complete: " + trans.id, {
|
|
592
|
+
path: trans.path
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
releaseClient();
|
|
596
|
+
return callback();
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
self.logError('pg', "Transaction commit failed: " + err);
|
|
600
|
+
|
|
601
|
+
// If COMMIT itself fails, the outcome may be unknowable from the app side:
|
|
602
|
+
// Postgres may have committed, but the client may have lost the response.
|
|
603
|
+
// Surface this to the outer transaction layer as a fatal consistency error.
|
|
604
|
+
if (query_step == 'COMMIT') {
|
|
605
|
+
err.fatal = true;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
if (!did_begin) {
|
|
609
|
+
releaseClient(err);
|
|
610
|
+
return callback(err);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// rollback best-effort. If this fails, discard the client from the pool.
|
|
614
|
+
self.queryWithClient( client, "ROLLBACK", {}, function(rollback_err) {
|
|
615
|
+
if (rollback_err) {
|
|
616
|
+
self.logError('pg', "Transaction rollback failed: " + rollback_err);
|
|
617
|
+
err.fatal = true;
|
|
618
|
+
err.rollbackError = rollback_err;
|
|
619
|
+
releaseClient(rollback_err);
|
|
620
|
+
}
|
|
621
|
+
else {
|
|
622
|
+
self.logDebug(5, "Transaction rollback complete: " + trans.id);
|
|
623
|
+
releaseClient();
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
callback(err);
|
|
627
|
+
} );
|
|
628
|
+
}
|
|
629
|
+
); // series
|
|
630
|
+
} ); // db.connect
|
|
631
|
+
},
|
|
632
|
+
|
|
633
|
+
unitTestCleanup: function(callback) {
|
|
634
|
+
// cleanup all unit test data, leaving the table itself intact
|
|
635
|
+
var self = this;
|
|
636
|
+
var table = this.config.get('table');
|
|
637
|
+
|
|
638
|
+
this.logDebug(3, "Cleaning up Postgres unit test table: " + table);
|
|
639
|
+
|
|
640
|
+
this.query( `TRUNCATE TABLE ${table}`, {}, function(err) {
|
|
641
|
+
if (err) {
|
|
642
|
+
self.logError('pg', "Failed to cleanup Postgres unit test table: " + err);
|
|
643
|
+
return callback(err);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
if (self.cache) self.cache.clear();
|
|
647
|
+
callback();
|
|
648
|
+
} );
|
|
649
|
+
},
|
|
650
|
+
|
|
465
651
|
runMaintenance: function(callback) {
|
|
466
652
|
// run daily maintenance
|
|
467
653
|
callback();
|
|
468
654
|
},
|
|
469
655
|
|
|
656
|
+
optimize: function(callback) {
|
|
657
|
+
// run Postgres VACUUM on the storage table
|
|
658
|
+
var self = this;
|
|
659
|
+
var table = this.config.get('table');
|
|
660
|
+
var perf = new Perf();
|
|
661
|
+
var vacuum = null;
|
|
662
|
+
|
|
663
|
+
this.logDebug(3, "Running Postgres optimization", { table: table });
|
|
664
|
+
perf.setScale(1);
|
|
665
|
+
perf.begin();
|
|
666
|
+
vacuum = perf.begin('vacuum');
|
|
667
|
+
|
|
668
|
+
this.query( `VACUUM ${table}`, {}, function(err, res) {
|
|
669
|
+
vacuum.end();
|
|
670
|
+
perf.end();
|
|
671
|
+
|
|
672
|
+
if (err) {
|
|
673
|
+
err.message = "Postgres optimization failed: " + err.message;
|
|
674
|
+
self.logError('pg', '' + err);
|
|
675
|
+
return callback(err);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
var report = {
|
|
679
|
+
engine: self.__name,
|
|
680
|
+
optimized: true,
|
|
681
|
+
table: table,
|
|
682
|
+
perf: perf.metrics(),
|
|
683
|
+
operations: [
|
|
684
|
+
{
|
|
685
|
+
name: 'vacuum',
|
|
686
|
+
ok: true,
|
|
687
|
+
command: res.command || 'VACUUM',
|
|
688
|
+
rowCount: res.rowCount || 0
|
|
689
|
+
}
|
|
690
|
+
]
|
|
691
|
+
};
|
|
692
|
+
|
|
693
|
+
self.logDebug(3, "Postgres optimization complete", report);
|
|
694
|
+
callback(null, report);
|
|
695
|
+
} );
|
|
696
|
+
},
|
|
697
|
+
|
|
470
698
|
shutdown: function(callback) {
|
|
471
699
|
// shutdown storage
|
|
472
700
|
var self = this;
|
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 Perf = require("pixl-perf");
|
|
12
13
|
const Cache = require("pixl-cache");
|
|
13
14
|
|
|
14
15
|
module.exports = Class.create({
|
|
@@ -421,11 +422,143 @@ module.exports = Class.create({
|
|
|
421
422
|
} );
|
|
422
423
|
},
|
|
423
424
|
|
|
425
|
+
commitTransaction: function(trans, callback) {
|
|
426
|
+
// commit all transaction actions using a native Redis MULTI/EXEC transaction
|
|
427
|
+
var self = this;
|
|
428
|
+
var write_keys = [];
|
|
429
|
+
var write_values = [];
|
|
430
|
+
var write_modified = [];
|
|
431
|
+
var delete_keys = [];
|
|
432
|
+
var bad_state = null;
|
|
433
|
+
var multi = this.redis.multi();
|
|
434
|
+
|
|
435
|
+
this.logDebug(5, "Beginning transaction commit: " + trans.id, {
|
|
436
|
+
path: trans.path,
|
|
437
|
+
actions: Tools.numKeys(trans.keys)
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
Object.keys(trans.keys).forEach( function(key) {
|
|
441
|
+
var record_state = trans.keys[key];
|
|
442
|
+
var norm_key = self.storage.normalizeKey(key);
|
|
443
|
+
|
|
444
|
+
if (record_state == 'W') {
|
|
445
|
+
// Collect writes into parallel arrays so we can update the cache later
|
|
446
|
+
// without stringifying and Buffer-wrapping JSON a second time.
|
|
447
|
+
var item = trans.values[key];
|
|
448
|
+
var redis_key = self.prepKey(norm_key);
|
|
449
|
+
var value = Buffer.from( JSON.stringify( item.data ) );
|
|
450
|
+
|
|
451
|
+
write_keys.push( redis_key );
|
|
452
|
+
write_values.push( value );
|
|
453
|
+
write_modified.push( item.mod );
|
|
454
|
+
|
|
455
|
+
multi.set( redis_key, value );
|
|
456
|
+
}
|
|
457
|
+
else if (record_state == 'D') {
|
|
458
|
+
// Queue deletes into the same Redis transaction.
|
|
459
|
+
var redis_key = self.prepKey(norm_key);
|
|
460
|
+
delete_keys.push( redis_key );
|
|
461
|
+
|
|
462
|
+
multi.del( redis_key );
|
|
463
|
+
}
|
|
464
|
+
else {
|
|
465
|
+
bad_state = new Error("Unknown transaction record state: " + record_state + ": " + key);
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
if (bad_state) return callback(bad_state);
|
|
470
|
+
|
|
471
|
+
multi.exec( function(err, results) {
|
|
472
|
+
if (err) {
|
|
473
|
+
// With MULTI/EXEC over the network, a top-level exec error may leave
|
|
474
|
+
// the actual Redis outcome unknowable, so treat it as fatal.
|
|
475
|
+
err.fatal = true;
|
|
476
|
+
self.logError('redis', "Transaction commit failed: " + err);
|
|
477
|
+
return callback(err);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Redis can return per-command errors from EXEC while still applying
|
|
481
|
+
// other commands. That is a partial commit, so it must be fatal.
|
|
482
|
+
var cmd_err = null;
|
|
483
|
+
if (results) {
|
|
484
|
+
results.forEach( function(item) {
|
|
485
|
+
if (item && item[0] && !cmd_err) cmd_err = item[0];
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
if (cmd_err) {
|
|
489
|
+
cmd_err.fatal = true;
|
|
490
|
+
self.logError('redis', "Transaction command failed: " + cmd_err);
|
|
491
|
+
return callback(cmd_err);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// commit succeeded, so now it is safe to update the engine LRU cache
|
|
495
|
+
if (self.cache) {
|
|
496
|
+
write_keys.forEach( function(key, idx) {
|
|
497
|
+
if (!self.storage.isBinaryKey(key)) {
|
|
498
|
+
self.cache.set( key, write_values[idx], { date: write_modified[idx] } );
|
|
499
|
+
}
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
delete_keys.forEach( function(key) {
|
|
503
|
+
if (!self.storage.isBinaryKey(key)) {
|
|
504
|
+
// store 'empty' stub in cache
|
|
505
|
+
self.cache.set( key, Buffer.alloc(0), { date: 0 } );
|
|
506
|
+
}
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
self.logDebug(5, "Transaction commit complete: " + trans.id, {
|
|
511
|
+
path: trans.path
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
callback();
|
|
515
|
+
} );
|
|
516
|
+
},
|
|
517
|
+
|
|
518
|
+
unitTestCleanup: function(callback) {
|
|
519
|
+
// cleanup all unit test data
|
|
520
|
+
var self = this;
|
|
521
|
+
this.logDebug(3, "Cleaning up Redis unit test database with FLUSHDB");
|
|
522
|
+
|
|
523
|
+
return this.redis.flushdb( function(err) {
|
|
524
|
+
if (err) {
|
|
525
|
+
self.logError('redis', "Failed to cleanup Redis unit test database: " + err);
|
|
526
|
+
return callback(err);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (self.cache) self.cache.clear();
|
|
530
|
+
callback();
|
|
531
|
+
} );
|
|
532
|
+
},
|
|
533
|
+
|
|
424
534
|
runMaintenance: function(callback) {
|
|
425
535
|
// run daily maintenance
|
|
426
536
|
callback();
|
|
427
537
|
},
|
|
428
538
|
|
|
539
|
+
optimize: function(callback) {
|
|
540
|
+
// Redis does its own memory management, so there is nothing to run here.
|
|
541
|
+
var perf = new Perf();
|
|
542
|
+
perf.setScale(1);
|
|
543
|
+
perf.begin();
|
|
544
|
+
perf.begin('noop');
|
|
545
|
+
perf.end('noop');
|
|
546
|
+
perf.end();
|
|
547
|
+
|
|
548
|
+
process.nextTick( callback, null, {
|
|
549
|
+
engine: this.__name,
|
|
550
|
+
optimized: false,
|
|
551
|
+
perf: perf.metrics(),
|
|
552
|
+
operations: [
|
|
553
|
+
{
|
|
554
|
+
name: 'noop',
|
|
555
|
+
ok: true,
|
|
556
|
+
message: "Redis does not require storage optimization."
|
|
557
|
+
}
|
|
558
|
+
]
|
|
559
|
+
} );
|
|
560
|
+
},
|
|
561
|
+
|
|
429
562
|
shutdown: function(callback) {
|
|
430
563
|
// shutdown storage
|
|
431
564
|
this.logDebug(2, "Shutting down Redis");
|
package/engines/RedisCluster.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 Perf = require("pixl-perf");
|
|
12
13
|
const Cache = require("pixl-cache");
|
|
13
14
|
|
|
14
15
|
module.exports = Class.create({
|
|
@@ -439,6 +440,29 @@ module.exports = Class.create({
|
|
|
439
440
|
callback();
|
|
440
441
|
},
|
|
441
442
|
|
|
443
|
+
optimize: function(callback) {
|
|
444
|
+
// Redis Cluster does its own memory management, so there is nothing to run here.
|
|
445
|
+
var perf = new Perf();
|
|
446
|
+
perf.setScale(1);
|
|
447
|
+
perf.begin();
|
|
448
|
+
perf.begin('noop');
|
|
449
|
+
perf.end('noop');
|
|
450
|
+
perf.end();
|
|
451
|
+
|
|
452
|
+
process.nextTick( callback, null, {
|
|
453
|
+
engine: this.__name,
|
|
454
|
+
optimized: false,
|
|
455
|
+
perf: perf.metrics(),
|
|
456
|
+
operations: [
|
|
457
|
+
{
|
|
458
|
+
name: 'noop',
|
|
459
|
+
ok: true,
|
|
460
|
+
message: "RedisCluster does not require storage optimization."
|
|
461
|
+
}
|
|
462
|
+
]
|
|
463
|
+
} );
|
|
464
|
+
},
|
|
465
|
+
|
|
442
466
|
shutdown: function(callback) {
|
|
443
467
|
// shutdown storage
|
|
444
468
|
this.logDebug(2, "Shutting down Redis");
|