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/README.md +35 -4
- package/docs/API.md +58 -0
- package/docs/Indexer.md +62 -0
- package/docs/Transactions.md +102 -60
- package/engines/Filesystem.js +59 -27
- package/engines/Hybrid.js +104 -2
- package/engines/Postgres.js +291 -23
- package/engines/Redis.js +262 -28
- package/engines/RedisCluster.js +152 -28
- package/engines/S3.js +53 -25
- package/engines/SQLite.js +253 -21
- 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-33.db.gz +0 -0
- 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/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) {
|
|
@@ -204,13 +212,22 @@ module.exports = Class.create({
|
|
|
204
212
|
if (this.cache && this.cache.has(key)) {
|
|
205
213
|
var item = this.cache.getMeta(key);
|
|
206
214
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
215
|
+
if (item.value.length == 0) {
|
|
216
|
+
process.nextTick( function() {
|
|
217
|
+
var err = new Error("Failed to head key: " + key + ": Not found");
|
|
218
|
+
err.code = "NoSuchKey";
|
|
219
|
+
callback( err );
|
|
212
220
|
} );
|
|
213
|
-
}
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
process.nextTick( function() {
|
|
224
|
+
self.logDebug(9, "Cached head complete: " + key);
|
|
225
|
+
callback( null, {
|
|
226
|
+
mod: item.date,
|
|
227
|
+
len: item.value.length
|
|
228
|
+
} );
|
|
229
|
+
} );
|
|
230
|
+
}
|
|
214
231
|
return;
|
|
215
232
|
} // cache
|
|
216
233
|
|
|
@@ -227,6 +244,12 @@ module.exports = Class.create({
|
|
|
227
244
|
if (!row) {
|
|
228
245
|
var err = new Error("Failed to head key: " + key + ": Not found");
|
|
229
246
|
err.code = "NoSuchKey";
|
|
247
|
+
|
|
248
|
+
if (self.cache && !self.storage.isBinaryKey(key)) {
|
|
249
|
+
// store 'empty' stub in cache
|
|
250
|
+
self.cache.set( key, Buffer.alloc(0), { date: 0 } );
|
|
251
|
+
}
|
|
252
|
+
|
|
230
253
|
return callback(err, null);
|
|
231
254
|
}
|
|
232
255
|
callback(null, { mod: parseInt(row.modified, 10), len: row.len });
|
|
@@ -242,19 +265,29 @@ module.exports = Class.create({
|
|
|
242
265
|
|
|
243
266
|
// check cache first
|
|
244
267
|
if (this.cache && !is_binary && this.cache.has(key)) {
|
|
245
|
-
var
|
|
268
|
+
var item = this.cache.getMeta(key);
|
|
246
269
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
callback(
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
270
|
+
if (item.value.length == 0) {
|
|
271
|
+
process.nextTick( function() {
|
|
272
|
+
var err = new Error("Failed to fetch key: " + key + ": Not found");
|
|
273
|
+
err.code = "NoSuchKey";
|
|
274
|
+
callback( err );
|
|
275
|
+
} );
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
process.nextTick( function() {
|
|
279
|
+
var data = null;
|
|
280
|
+
try { data = JSON.parse( item.value.toString() ); }
|
|
281
|
+
catch (e) {
|
|
282
|
+
self.logError('pg', "Failed to parse JSON record: " + key + ": " + e);
|
|
283
|
+
callback( e, null );
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
self.logDebug(9, "Cached JSON fetch complete: " + key, self.debugLevel(10) ? data : null);
|
|
287
|
+
|
|
288
|
+
callback( null, data );
|
|
289
|
+
} );
|
|
290
|
+
}
|
|
258
291
|
return;
|
|
259
292
|
} // cache
|
|
260
293
|
|
|
@@ -273,6 +306,12 @@ module.exports = Class.create({
|
|
|
273
306
|
if (!row) {
|
|
274
307
|
var err = new Error("Failed to fetch key: " + key + ": Not found");
|
|
275
308
|
err.code = "NoSuchKey";
|
|
309
|
+
|
|
310
|
+
if (self.cache && !is_binary) {
|
|
311
|
+
// store 'empty' stub in cache
|
|
312
|
+
self.cache.set( key, Buffer.alloc(0), { date: 0 } );
|
|
313
|
+
}
|
|
314
|
+
|
|
276
315
|
return callback(err, null);
|
|
277
316
|
}
|
|
278
317
|
if (is_binary) {
|
|
@@ -412,21 +451,250 @@ module.exports = Class.create({
|
|
|
412
451
|
if (!res.rowCount) {
|
|
413
452
|
var err = new Error("Failed to delete object: " + key + ": Not found");
|
|
414
453
|
err.code = "NoSuchKey";
|
|
454
|
+
|
|
455
|
+
if (self.cache && !self.storage.isBinaryKey(key)) {
|
|
456
|
+
// store 'empty' stub in cache
|
|
457
|
+
self.cache.set( key, Buffer.alloc(0), { date: 0 } );
|
|
458
|
+
}
|
|
459
|
+
|
|
415
460
|
return callback(err);
|
|
416
461
|
}
|
|
417
462
|
self.logDebug(9, "Delete complete: " + key);
|
|
418
|
-
|
|
419
|
-
|
|
463
|
+
|
|
464
|
+
// possibly "delete" from LRU cache as well
|
|
465
|
+
if (self.cache && !self.storage.isBinaryKey(key)) {
|
|
466
|
+
// store 'empty' stub in cache
|
|
467
|
+
self.cache.set( key, Buffer.alloc(0), { date: 0 } );
|
|
420
468
|
}
|
|
421
469
|
callback();
|
|
422
470
|
} ); // query
|
|
423
471
|
},
|
|
424
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
|
+
|
|
425
651
|
runMaintenance: function(callback) {
|
|
426
652
|
// run daily maintenance
|
|
427
653
|
callback();
|
|
428
654
|
},
|
|
429
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
|
+
|
|
430
698
|
shutdown: function(callback) {
|
|
431
699
|
// shutdown storage
|
|
432
700
|
var self = this;
|