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/Redis.js
CHANGED
|
@@ -9,6 +9,8 @@ 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");
|
|
13
|
+
const Cache = require("pixl-cache");
|
|
12
14
|
|
|
13
15
|
module.exports = Class.create({
|
|
14
16
|
|
|
@@ -47,6 +49,23 @@ module.exports = Class.create({
|
|
|
47
49
|
this.keyTemplate = (r_config.keyTemplate || '').replace(/^\//, '').replace(/\/$/, '');
|
|
48
50
|
delete r_config.keyTemplate;
|
|
49
51
|
|
|
52
|
+
// optional LRU cache
|
|
53
|
+
this.cache = null;
|
|
54
|
+
var cache_opts = r_config.cache;
|
|
55
|
+
if (cache_opts && cache_opts.enabled) {
|
|
56
|
+
this.logDebug(3, "Setting up LRU cache", cache_opts);
|
|
57
|
+
this.cache = new Cache( Tools.copyHashRemoveKeys(cache_opts, { enabled: 1 }) );
|
|
58
|
+
this.cache.on('expire', function(item, reason) {
|
|
59
|
+
self.logDebug(9, "Expiring LRU cache object: " + item.key + " due to: " + reason, {
|
|
60
|
+
key: item.key,
|
|
61
|
+
reason: reason,
|
|
62
|
+
totalCount: self.cache.count,
|
|
63
|
+
totalBytes: self.cache.bytes
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
delete r_config.cache;
|
|
68
|
+
|
|
50
69
|
if (!r_config.username.length) delete r_config.username;
|
|
51
70
|
if (!r_config.password.length) delete r_config.password;
|
|
52
71
|
|
|
@@ -93,13 +112,14 @@ module.exports = Class.create({
|
|
|
93
112
|
// store key+value in Redis
|
|
94
113
|
var self = this;
|
|
95
114
|
key = this.prepKey(key);
|
|
115
|
+
var is_binary = this.storage.isBinaryKey(key);
|
|
96
116
|
|
|
97
|
-
if (
|
|
117
|
+
if (is_binary) {
|
|
98
118
|
this.logDebug(9, "Storing Redis Binary Object: " + key, '' + value.length + ' bytes');
|
|
99
119
|
}
|
|
100
120
|
else {
|
|
101
121
|
this.logDebug(9, "Storing Redis JSON Object: " + key, this.debugLevel(10) ? value : null);
|
|
102
|
-
value = JSON.stringify( value );
|
|
122
|
+
value = Buffer.from( JSON.stringify( value ) );
|
|
103
123
|
}
|
|
104
124
|
|
|
105
125
|
this.redis.set( key, value, function(err) {
|
|
@@ -109,6 +129,11 @@ module.exports = Class.create({
|
|
|
109
129
|
}
|
|
110
130
|
else self.logDebug(9, "Store complete: " + key);
|
|
111
131
|
|
|
132
|
+
// possibly cache in LRU
|
|
133
|
+
if (!err && self.cache && !is_binary) {
|
|
134
|
+
self.cache.set( key, value, { date: Tools.timeNow(true) } );
|
|
135
|
+
}
|
|
136
|
+
|
|
112
137
|
if (callback) callback(err);
|
|
113
138
|
} );
|
|
114
139
|
},
|
|
@@ -138,7 +163,30 @@ module.exports = Class.create({
|
|
|
138
163
|
// The Redis API has no way to head / ping an object.
|
|
139
164
|
// So, we have to do this the RAM-hard way...
|
|
140
165
|
|
|
141
|
-
|
|
166
|
+
// check cache first
|
|
167
|
+
if (this.cache && this.cache.has(key)) {
|
|
168
|
+
var item = this.cache.getMeta(key);
|
|
169
|
+
|
|
170
|
+
if (item.value.length == 0) {
|
|
171
|
+
process.nextTick( function() {
|
|
172
|
+
var err = new Error("Failed to head key: " + key + ": Not found");
|
|
173
|
+
err.code = "NoSuchKey";
|
|
174
|
+
callback( err );
|
|
175
|
+
} );
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
process.nextTick( function() {
|
|
179
|
+
self.logDebug(9, "Cached head complete: " + key);
|
|
180
|
+
callback( null, {
|
|
181
|
+
mod: item.date,
|
|
182
|
+
len: item.value.length
|
|
183
|
+
} );
|
|
184
|
+
} );
|
|
185
|
+
}
|
|
186
|
+
return;
|
|
187
|
+
} // cache
|
|
188
|
+
|
|
189
|
+
this.redis.getBuffer( key, function(err, data) {
|
|
142
190
|
if (err) {
|
|
143
191
|
// an actual error
|
|
144
192
|
err.message = "Failed to head key: " + key + ": " + err;
|
|
@@ -150,7 +198,12 @@ module.exports = Class.create({
|
|
|
150
198
|
// always use "NoSuchKey" in error code
|
|
151
199
|
var err = new Error("Failed to head key: " + key + ": Not found");
|
|
152
200
|
err.code = "NoSuchKey";
|
|
153
|
-
|
|
201
|
+
|
|
202
|
+
if (self.cache && !self.storage.isBinaryKey(key)) {
|
|
203
|
+
// store 'empty' stub in cache
|
|
204
|
+
self.cache.set( key, Buffer.alloc(0), { date: 0 } );
|
|
205
|
+
}
|
|
206
|
+
|
|
154
207
|
callback( err, null );
|
|
155
208
|
}
|
|
156
209
|
else {
|
|
@@ -163,44 +216,79 @@ module.exports = Class.create({
|
|
|
163
216
|
// fetch Redis value given key
|
|
164
217
|
var self = this;
|
|
165
218
|
key = this.prepKey(key);
|
|
219
|
+
var is_binary = this.storage.isBinaryKey(key);
|
|
166
220
|
|
|
167
221
|
this.logDebug(9, "Fetching Redis Object: " + key);
|
|
168
222
|
|
|
169
|
-
|
|
170
|
-
this.
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
self.logError('redis', ''+err);
|
|
176
|
-
callback( err, null );
|
|
177
|
-
}
|
|
178
|
-
else {
|
|
179
|
-
// record not found
|
|
180
|
-
// always use "NoSuchKey" in error code
|
|
223
|
+
// check cache first
|
|
224
|
+
if (this.cache && !is_binary && this.cache.has(key)) {
|
|
225
|
+
var item = this.cache.getMeta(key);
|
|
226
|
+
|
|
227
|
+
if (item.value.length == 0) {
|
|
228
|
+
process.nextTick( function() {
|
|
181
229
|
var err = new Error("Failed to fetch key: " + key + ": Not found");
|
|
182
230
|
err.code = "NoSuchKey";
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
}
|
|
231
|
+
callback( err );
|
|
232
|
+
} );
|
|
186
233
|
}
|
|
187
234
|
else {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
else {
|
|
192
|
-
try { result = JSON.parse( result.toString() ); }
|
|
235
|
+
process.nextTick( function() {
|
|
236
|
+
var data = null;
|
|
237
|
+
try { data = JSON.parse( item.value.toString() ); }
|
|
193
238
|
catch (err) {
|
|
194
239
|
self.logError('redis', "Failed to parse JSON record: " + key + ": " + err);
|
|
195
240
|
callback( err, null );
|
|
196
241
|
return;
|
|
197
242
|
}
|
|
198
|
-
self.logDebug(9, "JSON fetch complete: " + key, self.debugLevel(10) ?
|
|
243
|
+
self.logDebug(9, "Cached JSON fetch complete: " + key, self.debugLevel(10) ? data : null);
|
|
244
|
+
|
|
245
|
+
callback( null, data );
|
|
246
|
+
} );
|
|
247
|
+
}
|
|
248
|
+
return;
|
|
249
|
+
} // cache
|
|
250
|
+
|
|
251
|
+
this.redis.getBuffer( key, function(err, result) {
|
|
252
|
+
if (err) {
|
|
253
|
+
// an actual error
|
|
254
|
+
err.message = "Failed to fetch key: " + key + ": " + err;
|
|
255
|
+
self.logError('redis', ''+err);
|
|
256
|
+
return callback( err, null );
|
|
257
|
+
}
|
|
258
|
+
else if (!result || !result.length) {
|
|
259
|
+
// record not found
|
|
260
|
+
// always use "NoSuchKey" in error code
|
|
261
|
+
var err = new Error("Failed to fetch key: " + key + ": Not found");
|
|
262
|
+
err.code = "NoSuchKey";
|
|
263
|
+
|
|
264
|
+
if (self.cache && !is_binary) {
|
|
265
|
+
// store 'empty' stub in cache
|
|
266
|
+
self.cache.set( key, Buffer.alloc(0), { date: 0 } );
|
|
199
267
|
}
|
|
200
268
|
|
|
201
|
-
callback(
|
|
269
|
+
return callback( err, null );
|
|
202
270
|
}
|
|
203
|
-
|
|
271
|
+
|
|
272
|
+
if (is_binary) {
|
|
273
|
+
self.logDebug(9, "Binary fetch complete: " + key, '' + result.length + ' bytes');
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
// possibly cache in LRU
|
|
277
|
+
if (self.cache) {
|
|
278
|
+
self.cache.set( key, result, { date: Tools.timeNow(true) } );
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
try { result = JSON.parse( result.toString() ); }
|
|
282
|
+
catch (err) {
|
|
283
|
+
self.logError('redis', "Failed to parse JSON record: " + key + ": " + err);
|
|
284
|
+
callback( err, null );
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
self.logDebug(9, "JSON fetch complete: " + key, self.debugLevel(10) ? result : null);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
callback( null, result );
|
|
291
|
+
} ); // redis.getBuffer
|
|
204
292
|
},
|
|
205
293
|
|
|
206
294
|
getBuffer: function(key, callback) {
|
|
@@ -303,6 +391,7 @@ module.exports = Class.create({
|
|
|
303
391
|
// delete Redis key given key
|
|
304
392
|
var self = this;
|
|
305
393
|
key = this.prepKey(key);
|
|
394
|
+
var is_binary = this.storage.isBinaryKey(key);
|
|
306
395
|
|
|
307
396
|
this.logDebug(9, "Deleting Redis Object: " + key);
|
|
308
397
|
|
|
@@ -310,21 +399,166 @@ module.exports = Class.create({
|
|
|
310
399
|
if (!err && !deleted) {
|
|
311
400
|
err = new Error("Failed to fetch key: " + key + ": Not found");
|
|
312
401
|
err.code = "NoSuchKey";
|
|
402
|
+
|
|
403
|
+
if (self.cache && !is_binary) {
|
|
404
|
+
// store 'empty' stub in cache
|
|
405
|
+
self.cache.set( key, Buffer.alloc(0), { date: 0 } );
|
|
406
|
+
}
|
|
313
407
|
}
|
|
314
408
|
if (err) {
|
|
315
409
|
self.logError('redis', "Failed to delete object: " + key + ": " + err);
|
|
316
410
|
}
|
|
317
|
-
else
|
|
411
|
+
else {
|
|
412
|
+
self.logDebug(9, "Delete complete: " + key);
|
|
413
|
+
|
|
414
|
+
// possibly "delete" from LRU cache as well
|
|
415
|
+
if (self.cache && !is_binary) {
|
|
416
|
+
// store 'empty' stub in cache
|
|
417
|
+
self.cache.set( key, Buffer.alloc(0), { date: 0 } );
|
|
418
|
+
}
|
|
419
|
+
}
|
|
318
420
|
|
|
319
421
|
callback(err);
|
|
320
422
|
} );
|
|
321
423
|
},
|
|
322
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
|
+
|
|
323
534
|
runMaintenance: function(callback) {
|
|
324
535
|
// run daily maintenance
|
|
325
536
|
callback();
|
|
326
537
|
},
|
|
327
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
|
+
|
|
328
562
|
shutdown: function(callback) {
|
|
329
563
|
// shutdown storage
|
|
330
564
|
this.logDebug(2, "Shutting down Redis");
|
package/engines/RedisCluster.js
CHANGED
|
@@ -9,6 +9,8 @@ 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");
|
|
13
|
+
const Cache = require("pixl-cache");
|
|
12
14
|
|
|
13
15
|
module.exports = Class.create({
|
|
14
16
|
|
|
@@ -49,6 +51,22 @@ module.exports = Class.create({
|
|
|
49
51
|
|
|
50
52
|
this.keyTemplate = (r_config.keyTemplate || '').replace(/^\//, '').replace(/\/$/, '');
|
|
51
53
|
|
|
54
|
+
// optional LRU cache
|
|
55
|
+
this.cache = null;
|
|
56
|
+
var cache_opts = r_config.cache;
|
|
57
|
+
if (cache_opts && cache_opts.enabled) {
|
|
58
|
+
this.logDebug(3, "Setting up LRU cache", cache_opts);
|
|
59
|
+
this.cache = new Cache( Tools.copyHashRemoveKeys(cache_opts, { enabled: 1 }) );
|
|
60
|
+
this.cache.on('expire', function(item, reason) {
|
|
61
|
+
self.logDebug(9, "Expiring LRU cache object: " + item.key + " due to: " + reason, {
|
|
62
|
+
key: item.key,
|
|
63
|
+
reason: reason,
|
|
64
|
+
totalCount: self.cache.count,
|
|
65
|
+
totalBytes: self.cache.bytes
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
52
70
|
r_config.clusterOpts.clusterRetryStrategy = function(attempts) {
|
|
53
71
|
if (attempts > r_config.connectRetries) return false;
|
|
54
72
|
return attempts;
|
|
@@ -107,13 +125,14 @@ module.exports = Class.create({
|
|
|
107
125
|
// store key+value in Redis
|
|
108
126
|
var self = this;
|
|
109
127
|
key = this.prepKey(key);
|
|
128
|
+
var is_binary = this.storage.isBinaryKey(key);
|
|
110
129
|
|
|
111
|
-
if (
|
|
130
|
+
if (is_binary) {
|
|
112
131
|
this.logDebug(9, "Storing Redis Binary Object: " + key, '' + value.length + ' bytes');
|
|
113
132
|
}
|
|
114
133
|
else {
|
|
115
134
|
this.logDebug(9, "Storing Redis JSON Object: " + key, this.debugLevel(10) ? value : null);
|
|
116
|
-
value = JSON.stringify( value );
|
|
135
|
+
value = Buffer.from( JSON.stringify( value ) );
|
|
117
136
|
}
|
|
118
137
|
|
|
119
138
|
this.redis.set( key, value, function(err) {
|
|
@@ -123,6 +142,11 @@ module.exports = Class.create({
|
|
|
123
142
|
}
|
|
124
143
|
else self.logDebug(9, "Store complete: " + key);
|
|
125
144
|
|
|
145
|
+
// possibly cache in LRU
|
|
146
|
+
if (!err && self.cache && !is_binary) {
|
|
147
|
+
self.cache.set( key, value, { date: Tools.timeNow(true) } );
|
|
148
|
+
}
|
|
149
|
+
|
|
126
150
|
if (callback) callback(err);
|
|
127
151
|
} );
|
|
128
152
|
},
|
|
@@ -152,7 +176,30 @@ module.exports = Class.create({
|
|
|
152
176
|
// The Redis API has no way to head / ping an object.
|
|
153
177
|
// So, we have to do this the RAM-hard way...
|
|
154
178
|
|
|
155
|
-
|
|
179
|
+
// check cache first
|
|
180
|
+
if (this.cache && this.cache.has(key)) {
|
|
181
|
+
var item = this.cache.getMeta(key);
|
|
182
|
+
|
|
183
|
+
if (item.value.length == 0) {
|
|
184
|
+
process.nextTick( function() {
|
|
185
|
+
var err = new Error("Failed to head key: " + key + ": Not found");
|
|
186
|
+
err.code = "NoSuchKey";
|
|
187
|
+
callback( err );
|
|
188
|
+
} );
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
process.nextTick( function() {
|
|
192
|
+
self.logDebug(9, "Cached head complete: " + key);
|
|
193
|
+
callback( null, {
|
|
194
|
+
mod: item.date,
|
|
195
|
+
len: item.value.length
|
|
196
|
+
} );
|
|
197
|
+
} );
|
|
198
|
+
}
|
|
199
|
+
return;
|
|
200
|
+
} // cache
|
|
201
|
+
|
|
202
|
+
this.redis.getBuffer( key, function(err, data) {
|
|
156
203
|
if (err) {
|
|
157
204
|
// an actual error
|
|
158
205
|
err.message = "Failed to head key: " + key + ": " + err;
|
|
@@ -164,7 +211,12 @@ module.exports = Class.create({
|
|
|
164
211
|
// always use "NoSuchKey" in error code
|
|
165
212
|
var err = new Error("Failed to head key: " + key + ": Not found");
|
|
166
213
|
err.code = "NoSuchKey";
|
|
167
|
-
|
|
214
|
+
|
|
215
|
+
if (self.cache && !self.storage.isBinaryKey(key)) {
|
|
216
|
+
// store 'empty' stub in cache
|
|
217
|
+
self.cache.set( key, Buffer.alloc(0), { date: 0 } );
|
|
218
|
+
}
|
|
219
|
+
|
|
168
220
|
callback( err, null );
|
|
169
221
|
}
|
|
170
222
|
else {
|
|
@@ -177,44 +229,79 @@ module.exports = Class.create({
|
|
|
177
229
|
// fetch Redis value given key
|
|
178
230
|
var self = this;
|
|
179
231
|
key = this.prepKey(key);
|
|
232
|
+
var is_binary = this.storage.isBinaryKey(key);
|
|
180
233
|
|
|
181
234
|
this.logDebug(9, "Fetching Redis Object: " + key);
|
|
182
235
|
|
|
183
|
-
|
|
184
|
-
this.
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
self.logError('redis', ''+err);
|
|
190
|
-
callback( err, null );
|
|
191
|
-
}
|
|
192
|
-
else {
|
|
193
|
-
// record not found
|
|
194
|
-
// always use "NoSuchKey" in error code
|
|
236
|
+
// check cache first
|
|
237
|
+
if (this.cache && !is_binary && this.cache.has(key)) {
|
|
238
|
+
var item = this.cache.getMeta(key);
|
|
239
|
+
|
|
240
|
+
if (item.value.length == 0) {
|
|
241
|
+
process.nextTick( function() {
|
|
195
242
|
var err = new Error("Failed to fetch key: " + key + ": Not found");
|
|
196
243
|
err.code = "NoSuchKey";
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
}
|
|
244
|
+
callback( err );
|
|
245
|
+
} );
|
|
200
246
|
}
|
|
201
247
|
else {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
else {
|
|
206
|
-
try { result = JSON.parse( result.toString() ); }
|
|
248
|
+
process.nextTick( function() {
|
|
249
|
+
var data = null;
|
|
250
|
+
try { data = JSON.parse( item.value.toString() ); }
|
|
207
251
|
catch (err) {
|
|
208
252
|
self.logError('redis', "Failed to parse JSON record: " + key + ": " + err);
|
|
209
253
|
callback( err, null );
|
|
210
254
|
return;
|
|
211
255
|
}
|
|
212
|
-
self.logDebug(9, "JSON fetch complete: " + key, self.debugLevel(10) ?
|
|
256
|
+
self.logDebug(9, "Cached JSON fetch complete: " + key, self.debugLevel(10) ? data : null);
|
|
257
|
+
|
|
258
|
+
callback( null, data );
|
|
259
|
+
} );
|
|
260
|
+
}
|
|
261
|
+
return;
|
|
262
|
+
} // cache
|
|
263
|
+
|
|
264
|
+
this.redis.getBuffer( key, function(err, result) {
|
|
265
|
+
if (err) {
|
|
266
|
+
// an actual error
|
|
267
|
+
err.message = "Failed to fetch key: " + key + ": " + err;
|
|
268
|
+
self.logError('redis', ''+err);
|
|
269
|
+
return callback( err, null );
|
|
270
|
+
}
|
|
271
|
+
else if (!result || !result.length) {
|
|
272
|
+
// record not found
|
|
273
|
+
// always use "NoSuchKey" in error code
|
|
274
|
+
var err = new Error("Failed to fetch key: " + key + ": Not found");
|
|
275
|
+
err.code = "NoSuchKey";
|
|
276
|
+
|
|
277
|
+
if (self.cache && !is_binary) {
|
|
278
|
+
// store 'empty' stub in cache
|
|
279
|
+
self.cache.set( key, Buffer.alloc(0), { date: 0 } );
|
|
213
280
|
}
|
|
214
281
|
|
|
215
|
-
callback(
|
|
282
|
+
return callback( err, null );
|
|
216
283
|
}
|
|
217
|
-
|
|
284
|
+
|
|
285
|
+
if (is_binary) {
|
|
286
|
+
self.logDebug(9, "Binary fetch complete: " + key, '' + result.length + ' bytes');
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
// possibly cache in LRU
|
|
290
|
+
if (self.cache) {
|
|
291
|
+
self.cache.set( key, result, { date: Tools.timeNow(true) } );
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
try { result = JSON.parse( result.toString() ); }
|
|
295
|
+
catch (err) {
|
|
296
|
+
self.logError('redis', "Failed to parse JSON record: " + key + ": " + err);
|
|
297
|
+
callback( err, null );
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
self.logDebug(9, "JSON fetch complete: " + key, self.debugLevel(10) ? result : null);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
callback( null, result );
|
|
304
|
+
} ); // redis.getBuffer
|
|
218
305
|
},
|
|
219
306
|
|
|
220
307
|
getBuffer: function(key, callback) {
|
|
@@ -317,6 +404,7 @@ module.exports = Class.create({
|
|
|
317
404
|
// delete Redis key given key
|
|
318
405
|
var self = this;
|
|
319
406
|
key = this.prepKey(key);
|
|
407
|
+
var is_binary = this.storage.isBinaryKey(key);
|
|
320
408
|
|
|
321
409
|
this.logDebug(9, "Deleting Redis Object: " + key);
|
|
322
410
|
|
|
@@ -324,11 +412,24 @@ module.exports = Class.create({
|
|
|
324
412
|
if (!err && !deleted) {
|
|
325
413
|
err = new Error("Failed to fetch key: " + key + ": Not found");
|
|
326
414
|
err.code = "NoSuchKey";
|
|
415
|
+
|
|
416
|
+
if (self.cache && !is_binary) {
|
|
417
|
+
// store 'empty' stub in cache
|
|
418
|
+
self.cache.set( key, Buffer.alloc(0), { date: 0 } );
|
|
419
|
+
}
|
|
327
420
|
}
|
|
328
421
|
if (err) {
|
|
329
422
|
self.logError('redis', "Failed to delete object: " + key + ": " + err);
|
|
330
423
|
}
|
|
331
|
-
else
|
|
424
|
+
else {
|
|
425
|
+
self.logDebug(9, "Delete complete: " + key);
|
|
426
|
+
|
|
427
|
+
// possibly "delete" from LRU cache as well
|
|
428
|
+
if (self.cache && !is_binary) {
|
|
429
|
+
// store 'empty' stub in cache
|
|
430
|
+
self.cache.set( key, Buffer.alloc(0), { date: 0 } );
|
|
431
|
+
}
|
|
432
|
+
}
|
|
332
433
|
|
|
333
434
|
callback(err);
|
|
334
435
|
} );
|
|
@@ -339,6 +440,29 @@ module.exports = Class.create({
|
|
|
339
440
|
callback();
|
|
340
441
|
},
|
|
341
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
|
+
|
|
342
466
|
shutdown: function(callback) {
|
|
343
467
|
// shutdown storage
|
|
344
468
|
this.logDebug(2, "Shutting down Redis");
|