ueberdb2 1.4.16

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.
@@ -0,0 +1,610 @@
1
+ 'use strict';
2
+ /**
3
+ * 2011 Peter 'Pita' Martischka
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS-IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+
18
+ /**
19
+ * This module is made for the case, you want to use a SQL-Based Databse or a KeyValue Database that
20
+ * can only save strings(and no objects), as a JSON KeyValue Store.
21
+ *
22
+ * The idea of the dbWrapper is to provide following features:
23
+ *
24
+ * * automatic JSON serialize/deserialize to abstract that away from the database driver and the
25
+ * module user.
26
+ * * cache reads. A amount of KeyValues are hold in the memory, so that reading is faster.
27
+ * * Buffer DB Writings. Sets and deletes should be buffered to make them in a setted interval
28
+ * with a bulk. This reduces the overhead of database transactions and makes the database
29
+ * faster. But there is also a danger to loose data integrity, to keep that, we should provide a
30
+ * flush function.
31
+ *
32
+ * All Features can be disabled or configured. The Wrapper provides default settings that can be
33
+ * overwriden by the driver and by the module user.
34
+ */
35
+
36
+ const util = require('util');
37
+
38
+ /**
39
+ * Cache with Least Recently Used eviction policy.
40
+ */
41
+ class LRU {
42
+ /**
43
+ * @param evictable Optional predicate that dictates whether it is permissable to evict the entry
44
+ * if it is old and the cache is over capacity. The predicate is passed two arguments (key,
45
+ * value). If no predicate is provided, all entries are evictable. Warning: Non-evictable
46
+ * entries can cause the cache to go over capacity. If the number of non-evictable entries is
47
+ * greater than or equal to the capacity, all new evictable entries will be evicted
48
+ * immediately.
49
+ */
50
+ constructor(capacity, evictable = (k, v) => true) {
51
+ this._capacity = capacity;
52
+ this._evictable = evictable;
53
+ this._cache = new Map();
54
+ }
55
+
56
+ /**
57
+ * The entries accessed via this iterator are not considered to have been "used" (for purposes of
58
+ * determining least recently used).
59
+ */
60
+ [Symbol.iterator]() {
61
+ return this._cache.entries();
62
+ }
63
+
64
+ /**
65
+ * @param isUse Optional boolean indicating whether this get() should be considered a "use" of the
66
+ * entry (for determining least recently used). Defaults to true.
67
+ * @returns undefined if there is no entry matching the given key.
68
+ */
69
+ get(k, isUse = true) {
70
+ if (!this._cache.has(k)) return;
71
+ const v = this._cache.get(k);
72
+ if (isUse) {
73
+ // Mark this entry as the most recently used entry.
74
+ this._cache.delete(k);
75
+ this._cache.set(k, v);
76
+ }
77
+ return v;
78
+ }
79
+
80
+ /**
81
+ * Adds or updates an entry in the cache. This marks the entry as the most recently used entry.
82
+ */
83
+ set(k, v) {
84
+ this._cache.delete(k); // Make sure this entry is marked as the most recently used entry.
85
+ this._cache.set(k, v);
86
+ this.evictOld();
87
+ }
88
+
89
+ /**
90
+ * Evicts the oldest evictable entries until the number of entries is equal to or less than the
91
+ * cache's capacity. This method is automatically called by set(). Call this if you need to evict
92
+ * newly evictable entries before the next call to set().
93
+ */
94
+ evictOld() {
95
+ // ES Map objects iterate in insertion order, so the first items are the ones that have been
96
+ // accessed least recently.
97
+ for (const [k, v] of this._cache.entries()) {
98
+ if (this._cache.size <= this._capacity) break;
99
+ if (!this._evictable(k, v)) continue;
100
+ this._cache.delete(k);
101
+ }
102
+ }
103
+ }
104
+
105
+ // Same as Promise but with a `done` property set to a Node-style callback that resolves/rejects the
106
+ // Promise.
107
+ class SelfContainedPromise extends Promise {
108
+ constructor(executor = null) {
109
+ let done;
110
+ super((resolve, reject) => {
111
+ done = (err, val) => err != null ? reject(err) : resolve(val);
112
+ if (executor != null) executor(resolve, reject);
113
+ });
114
+ this.done = done;
115
+ }
116
+ }
117
+
118
+ const defaultSettings =
119
+ {
120
+ // Maximum number of operations that can be passed to the wrapped database's doBulk() method.
121
+ // Falsy means no limit. EXPERIMENTAL.
122
+ bulkLimit: 0,
123
+ // the number of elements that should be cached. To Disable cache just set it to zero
124
+ cache: 10000,
125
+ // the interval in ms the wrapper writes to the database. To Disable interval writes just set it
126
+ // to zero
127
+ writeInterval: 100,
128
+ // a flag if the data sould be serialized/deserialized to json
129
+ json: true,
130
+ // use utf8mb4 as default
131
+ charset: 'utf8mb4',
132
+ };
133
+
134
+ exports.Database = class {
135
+ /**
136
+ * @param wrappedDB The Database that should be wrapped
137
+ * @param settings (optional) The settings that should be applied to the wrapper
138
+ */
139
+ constructor(wrappedDB, settings, logger) {
140
+ // wrappedDB.isAsync is a temporary boolean that will go away once we have migrated all of the
141
+ // database drivers from callback-based methods to async methods.
142
+ if (wrappedDB.isAsync) {
143
+ this.wrappedDB = wrappedDB;
144
+ } else {
145
+ this.wrappedDB = {};
146
+ for (const fn of ['close', 'doBulk', 'findKeys', 'get', 'init', 'remove', 'set']) {
147
+ const f = wrappedDB[fn];
148
+ if (typeof f !== 'function') continue;
149
+ this.wrappedDB[fn] = util.promisify(f.bind(wrappedDB));
150
+ }
151
+ }
152
+ this.logger = logger;
153
+
154
+ this.settings = Object.freeze({
155
+ ...defaultSettings,
156
+ ...(wrappedDB.settings || {}),
157
+ ...(settings || {}),
158
+ });
159
+
160
+ // The key is the database key. The value is an object with the following properties:
161
+ // - value: The entry's value.
162
+ // - dirty: If the value has not yet been written, this is a Promise that will resolve once
163
+ // the write to the underlying database returns. If the value has been written this is null.
164
+ // - writingInProgress: Boolean that if true indicates that the value has been sent to the
165
+ // underlying database and we are awaiting commit.
166
+ this.buffer = new LRU(this.settings.cache, (k, v) => !v.dirty && !v.writingInProgress);
167
+
168
+ // Maps database key to a Promise that is resolved when the record is unlocked.
169
+ this._locks = new Map();
170
+
171
+ this.metrics = {
172
+ // Count of times a database operation had to wait for the release of a record lock.
173
+ lockAwaits: 0,
174
+ // Count of times a record was locked.
175
+ lockAcquires: 0,
176
+ // Count of times a record was unlocked.
177
+ lockReleases: 0,
178
+
179
+ // Count of read operations (number of times `get()`, `getSub()`, and `setSub()` were called).
180
+ // This minus `readsFinished` is the number of currently pending read operations.
181
+ reads: 0,
182
+ // Count of times a read operation failed, including JSON parsing errors. This divided by
183
+ // `readsFinished` is the overall read error rate.
184
+ readsFailed: 0,
185
+ // Count of completed (successful or failed) read operations.
186
+ readsFinished: 0,
187
+ // Count of read operations that were satisfied from in-memory state (including the write
188
+ // buffer).
189
+ readsFromCache: 0,
190
+ // Count of times the database was queried for a value. This minus `readsFromDbFinished` is
191
+ // the number of in-progress reads.
192
+ readsFromDb: 0,
193
+ // Count of times the database failed to return a value. This does not include JSON parsing
194
+ // errors.
195
+ readsFromDbFailed: 0,
196
+ // Count of completed (successful or failed) value reads from the database. This plus
197
+ // `readsFromCache` equals `readsFinished`.
198
+ readsFromDbFinished: 0,
199
+
200
+ // Count of write operations (number of times `remove()`, `set()`, or `setSub()` was called)
201
+ // regardless of whether the value actually changed. This minus `writesFinished` is the
202
+ // current number of pending write operations.
203
+ writes: 0,
204
+ // Count of times a write operation failed, including JSON serialization errors. This divided
205
+ // by `writesFinished` is the overall write error rate.
206
+ writesFailed: 0,
207
+ // Count of completed (successful or failed) write operations.
208
+ writesFinished: 0,
209
+ // Count of times a pending write operation was not sent to the underlying database because a
210
+ // call to `remove()`, `set()`, or `setSub()` superseded the write, rendering it unnecessary.
211
+ writesObsoleted: 0,
212
+ // Count of times a value was sent to the underlying database, including record deletes but
213
+ // excluding retries. This minus `writesToDbFinished` is the number of in-progress writes.
214
+ writesToDb: 0,
215
+ // Count of times ueberDB failed to write a change to the underlying database, including
216
+ // failed record deletes. This does not include JSON serialization errors or write errors that
217
+ // later succeeded thanks to a retry by ueberDB.
218
+ writesToDbFailed: 0,
219
+ // Count of completed (successful or failed) value writes to the database. This plus
220
+ // `writesObsoleted` equals `writesFinished`.
221
+ writesToDbFinished: 0,
222
+ // Count of times a write operation was retried.
223
+ writesToDbRetried: 0,
224
+ };
225
+
226
+ // start the write Interval
227
+ this.flushInterval = this.settings.writeInterval > 0
228
+ ? setInterval(() => this.flush(), this.settings.writeInterval) : null;
229
+ }
230
+
231
+ async _lock(key) {
232
+ while (true) {
233
+ const l = this._locks.get(key);
234
+ if (l == null) break;
235
+ ++this.metrics.lockAwaits;
236
+ await l;
237
+ }
238
+ ++this.metrics.lockAcquires;
239
+ this._locks.set(key, new SelfContainedPromise());
240
+ }
241
+
242
+ async _unlock(key) {
243
+ ++this.metrics.lockReleases;
244
+ this._locks.get(key).done();
245
+ this._locks.delete(key);
246
+ }
247
+
248
+ /**
249
+ * wraps the init function of the original DB
250
+ */
251
+ async init() {
252
+ await this.wrappedDB.init();
253
+ }
254
+
255
+ /**
256
+ * wraps the close function of the original DB
257
+ */
258
+ async close() {
259
+ clearInterval(this.flushInterval);
260
+ await this.flush();
261
+ await this.wrappedDB.close();
262
+ this.wrappedDB = null;
263
+ }
264
+
265
+ /**
266
+ * Gets the value trough the wrapper.
267
+ */
268
+ async get(key) {
269
+ await this._lock(key);
270
+ try {
271
+ return await this._getLocked(key);
272
+ } finally {
273
+ this._unlock(key);
274
+ }
275
+ }
276
+
277
+ async _getLocked(key) {
278
+ ++this.metrics.reads;
279
+ try {
280
+ const entry = this.buffer.get(key);
281
+ if (entry != null) {
282
+ ++this.metrics.readsFromCache;
283
+ if (this.logger.isDebugEnabled()) {
284
+ this.logger.debug(`GET - ${key} - ${JSON.stringify(entry.value)} - ` +
285
+ `from ${entry.dirty ? 'dirty buffer' : 'cache'}`);
286
+ }
287
+ return entry.value;
288
+ }
289
+
290
+ // get it direct
291
+ let value;
292
+ ++this.metrics.readsFromDb;
293
+ try {
294
+ value = await this.wrappedDB.get(key);
295
+ } catch (err) {
296
+ ++this.metrics.readsFromDbFailed;
297
+ throw err;
298
+ } finally {
299
+ ++this.metrics.readsFromDbFinished;
300
+ }
301
+ if (this.settings.json) {
302
+ try {
303
+ value = JSON.parse(value);
304
+ } catch (err) {
305
+ this.logger.error(`JSON-PROBLEM:${value}`);
306
+ throw err;
307
+ }
308
+ }
309
+
310
+ // cache the value if caching is enabled
311
+ if (this.settings.cache > 0) {
312
+ this.buffer.set(key, {
313
+ value,
314
+ dirty: null,
315
+ writingInProgress: false,
316
+ });
317
+ }
318
+
319
+ if (this.logger.isDebugEnabled()) {
320
+ this.logger.debug(`GET - ${key} - ${JSON.stringify(value)} - from database `);
321
+ }
322
+
323
+ return value;
324
+ } catch (err) {
325
+ ++this.metrics.readsFailed;
326
+ throw err;
327
+ } finally {
328
+ ++this.metrics.readsFinished;
329
+ }
330
+ }
331
+
332
+ /**
333
+ * Find keys function searches the db sets for matching entries and
334
+ * returns the key entries via callback.
335
+ */
336
+ async findKeys(key, notKey) {
337
+ const bufferKey = `${key}-${notKey}`;
338
+ const keyValues = await this.wrappedDB.findKeys(key, notKey);
339
+ if (this.logger.isDebugEnabled()) {
340
+ this.logger.debug(`GET - ${bufferKey} - ${JSON.stringify(keyValues)} - from database `);
341
+ }
342
+ return keyValues;
343
+ }
344
+
345
+ /**
346
+ * Remove a record from the database
347
+ */
348
+ async remove(key) {
349
+ if (this.logger.isDebugEnabled()) this.logger.debug(`DELETE - ${key} - from database `);
350
+ await this.set(key, null);
351
+ }
352
+
353
+ /**
354
+ * Sets the value trough the wrapper
355
+ */
356
+ async set(key, value) {
357
+ await this._lock(key);
358
+ const p = this._setLocked(key, value);
359
+ this._unlock(key);
360
+ await p;
361
+ }
362
+
363
+ // Implementation of the `set()` method. The record must already be locked before calling this. It
364
+ // is safe to unlock the record before the returned Promise resolves.
365
+ async _setLocked(key, value) {
366
+ // IMPORTANT: This function MUST NOT use the `await` keyword before the entry in `this.buffer`
367
+ // is added/updated. Using `await` causes execution to return to the caller, and the caller must
368
+ // be able to immediately unlock the record to avoid unnecessary blocking while the value is
369
+ // committed to the underlying database. If `await` is used before the entry is updated then the
370
+ // record will be unlocked prematurely, possibly resulting in inconsistent state.
371
+ ++this.metrics.writes;
372
+ try {
373
+ let entry = this.buffer.get(key);
374
+ // If there is a write of a different value for the same key already in progress then don't
375
+ // update the existing entry object -- create a new entry object instead and replace the old
376
+ // one in this.buffer. (If the existing entry was updated instead, then entry.dirty would
377
+ // resolve when the old value is committed, not the new value.)
378
+ if (!entry || entry.writingInProgress) entry = {};
379
+ else if (entry.dirty) ++this.metrics.writesObsoleted;
380
+ entry.value = value;
381
+ // Always mark as dirty even if the value did not change. This simplifies the implementation:
382
+ // this function doesn't need to perform deep comparisons, and setSub() doesn't need to
383
+ // perform a deep copy of the object returned from get().
384
+ if (!entry.dirty) entry.dirty = new SelfContainedPromise();
385
+ // buffer.set() is called even if the value is unchanged so that the cache entry is marked as
386
+ // most recently used.
387
+ this.buffer.set(key, entry);
388
+ const buffered = this.settings.writeInterval > 0;
389
+ if (this.logger.isDebugEnabled()) {
390
+ this.logger.debug(
391
+ `SET - ${key} - ${JSON.stringify(value)} - to ${buffered ? 'buffer' : 'database'}`);
392
+ }
393
+ // Write it immediately if write buffering is disabled. If write buffering is enabled,
394
+ // this.flush() will eventually take care of it.
395
+ if (!buffered) this._write([[key, entry]]); // await is unnecessary.
396
+ await entry.dirty;
397
+ } catch (err) {
398
+ ++this.metrics.writesFailed;
399
+ throw err;
400
+ } finally {
401
+ ++this.metrics.writesFinished;
402
+ }
403
+ }
404
+
405
+ /**
406
+ * Sets a subvalue
407
+ */
408
+ async setSub(key, sub, value) {
409
+ if (this.logger.isDebugEnabled()) {
410
+ this.logger.debug(`SETSUB - ${key}${JSON.stringify(sub)} - ${JSON.stringify(value)}`);
411
+ }
412
+ let p;
413
+ await this._lock(key);
414
+ try {
415
+ let base;
416
+ try {
417
+ const fullValue = await this._getLocked(key);
418
+ base = {fullValue};
419
+ // Emulate a pointer to the property that should be set to `value`.
420
+ const ptr = {obj: base, prop: 'fullValue'};
421
+ for (let i = 0; i < sub.length; i++) {
422
+ let o = ptr.obj[ptr.prop];
423
+ if (o == null) ptr.obj[ptr.prop] = o = {};
424
+ // If o is a primitive (string, number, etc.), then setting `o.foo` has no effect because
425
+ // ECMAScript automatically wraps primitives in a temporary wrapper object.
426
+ if (typeof o !== 'object') {
427
+ throw new TypeError(
428
+ `Cannot set property ${JSON.stringify(sub[i])} on non-object ` +
429
+ `${JSON.stringify(o)} (key: ${JSON.stringify(key)} ` +
430
+ `value in db: ${JSON.stringify(fullValue)} ` +
431
+ `sub: ${JSON.stringify(sub.slice(0, i + 1))})`);
432
+ }
433
+ ptr.obj = ptr.obj[ptr.prop];
434
+ ptr.prop = sub[i];
435
+ }
436
+ ptr.obj[ptr.prop] = value;
437
+ } catch (err) {
438
+ // this._setLocked() will not be called but it should still count as a write failure.
439
+ ++this.metrics.writes;
440
+ ++this.metrics.writesFailed;
441
+ ++this.metrics.writesFinished;
442
+ throw err;
443
+ }
444
+ p = this._setLocked(key, base.fullValue);
445
+ } finally {
446
+ this._unlock(key);
447
+ }
448
+ await p;
449
+ }
450
+
451
+ /**
452
+ * Returns a sub value of the object
453
+ * @param sub is a array, for example if you want to access object.test.bla, the array is ["test",
454
+ * "bla"]
455
+ */
456
+ async getSub(key, sub) {
457
+ await this._lock(key);
458
+ try {
459
+ // get the full value
460
+ const value = await this._getLocked(key);
461
+
462
+ // everything is correct, navigate to the subvalue and return it
463
+ let subvalue = value;
464
+
465
+ for (let i = 0; i < sub.length; i++) {
466
+ // test if the subvalue exist
467
+ if (subvalue != null && subvalue[sub[i]] !== undefined) {
468
+ subvalue = subvalue[sub[i]];
469
+ } else {
470
+ // the subvalue doesn't exist, break the loop and return null
471
+ subvalue = null;
472
+ break;
473
+ }
474
+ }
475
+
476
+ if (this.logger.isDebugEnabled()) {
477
+ this.logger.debug(`GETSUB - ${key}${JSON.stringify(sub)} - ${JSON.stringify(subvalue)}`);
478
+ }
479
+ return subvalue;
480
+ } finally {
481
+ this._unlock(key);
482
+ }
483
+ }
484
+
485
+ /**
486
+ * Writes all dirty values to the database
487
+ */
488
+ async flush() {
489
+ if (this._flushDone == null) {
490
+ this._flushDone = (async () => {
491
+ while (true) {
492
+ const dirtyEntries = [];
493
+ for (const entry of this.buffer) {
494
+ if (entry[1].dirty && !entry[1].writingInProgress) {
495
+ dirtyEntries.push(entry);
496
+ if (this.settings.bulkLimit && dirtyEntries.length >= this.settings.bulkLimit) break;
497
+ }
498
+ }
499
+ if (dirtyEntries.length === 0) return;
500
+ await this._write(dirtyEntries);
501
+ }
502
+ })();
503
+ }
504
+ await this._flushDone;
505
+ this._flushDone = null;
506
+ }
507
+
508
+ async _write(dirtyEntries) {
509
+ const markDone = (entry, err) => {
510
+ if (entry.writingInProgress) {
511
+ entry.writingInProgress = false;
512
+ if (err != null) ++this.metrics.writesToDbFailed;
513
+ ++this.metrics.writesToDbFinished;
514
+ }
515
+ // If err != null then the entry is still technically dirty, but the responsibility is on the
516
+ // user to retry failures so from ueberDB's perspective the entry is no longer dirty.
517
+ entry.dirty.done(err);
518
+ entry.dirty = null;
519
+ };
520
+ const ops = [];
521
+ const entries = [];
522
+ for (const [key, entry] of dirtyEntries) {
523
+ let value = entry.value;
524
+ try {
525
+ value = this.settings.json && value != null ? JSON.stringify(value) : clone(value);
526
+ } catch (err) {
527
+ markDone(entry, err);
528
+ continue;
529
+ }
530
+ entry.writingInProgress = true;
531
+ ops.push({type: value == null ? 'remove' : 'set', key, value});
532
+ entries.push(entry);
533
+ }
534
+ if (ops.length === 0) return;
535
+ this.metrics.writesToDb += ops.length;
536
+ const writeOneOp = async (op, entry) => {
537
+ let writeErr = null;
538
+ try {
539
+ switch (op.type) {
540
+ case 'remove':
541
+ await this.wrappedDB.remove(op.key);
542
+ break;
543
+ case 'set':
544
+ await this.wrappedDB.set(op.key, op.value);
545
+ break;
546
+ default:
547
+ throw new Error(`unsupported operation type: ${op.type}`);
548
+ }
549
+ } catch (err) {
550
+ writeErr = err || new Error(err);
551
+ }
552
+ markDone(entry, writeErr);
553
+ };
554
+ if (ops.length === 1) {
555
+ await writeOneOp(ops[0], entries[0]);
556
+ } else {
557
+ let success = false;
558
+ try {
559
+ await this.wrappedDB.doBulk(ops);
560
+ success = true;
561
+ } catch (err) {
562
+ this.logger.error(
563
+ `Bulk write of ${ops.length} ops failed, retrying individually: ${err.stack || err}`);
564
+ this.metrics.writesToDbRetried += ops.length;
565
+ await Promise.all(ops.map(async (op, i) => await writeOneOp(op, entries[i])));
566
+ }
567
+ if (success) entries.forEach((entry) => markDone(entry, null));
568
+ }
569
+ // At this point we could call db.buffer.evictOld() to ensure that the number of entries in
570
+ // this.buffer is at or below capacity, but if we haven't run out of memory by this point then
571
+ // it should be safe to continue using the memory until the next call to this.buffer.set()
572
+ // evicts the old entries. This saves some CPU cycles at the expense of memory.
573
+ }
574
+ };
575
+
576
+ const clone = (obj) => {
577
+ // Handle the 3 simple types, and null or undefined
578
+ if (null == obj || 'object' !== typeof obj) return obj;
579
+
580
+ // Handle Date
581
+ if (obj instanceof Date) {
582
+ const copy = new Date();
583
+ copy.setTime(obj.getTime());
584
+ return copy;
585
+ }
586
+
587
+ // Handle Array
588
+ if (obj instanceof Array) {
589
+ const copy = [];
590
+ for (let i = 0, len = obj.length; i < len; ++i) {
591
+ copy[i] = clone(obj[i]);
592
+ }
593
+ return copy;
594
+ }
595
+
596
+ // Handle Object
597
+ if (obj instanceof Object) {
598
+ const copy = {};
599
+ for (const attr in obj) {
600
+ if (Object.prototype.hasOwnProperty.call(obj, attr)) copy[attr] = clone(obj[attr]);
601
+ }
602
+ return copy;
603
+ }
604
+
605
+ throw new Error("Unable to copy obj! Its type isn't supported.");
606
+ };
607
+
608
+ exports.exportedForTesting = {
609
+ LRU,
610
+ };