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/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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
|
332
|
+
var item = this.cache.getMeta(orig_key);
|
|
319
333
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
callback(
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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.
|
|
582
|
-
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
|
248
|
+
var item = this.cache.getMeta(key);
|
|
234
249
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
callback(
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
406
|
-
|
|
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.
|
|
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
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
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);
|