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.
- package/.github/workflows/npmpublish.yml +103 -0
- package/.travis.yml +46 -0
- package/CHANGELOG.md +167 -0
- package/CONTRIBUTING.md +103 -0
- package/LICENSE +202 -0
- package/README.md +356 -0
- package/SECURITY.md +5 -0
- package/databases/cassandra_db.js +250 -0
- package/databases/couch_db.js +201 -0
- package/databases/dirty_db.js +80 -0
- package/databases/dirty_git_db.js +78 -0
- package/databases/elasticsearch_db.js +288 -0
- package/databases/mock_db.js +42 -0
- package/databases/mongodb_db.js +136 -0
- package/databases/mssql_db.js +218 -0
- package/databases/mysql_db.js +178 -0
- package/databases/postgres_db.js +198 -0
- package/databases/postgrespool_db.js +11 -0
- package/databases/redis_db.js +128 -0
- package/databases/rethink_db.js +98 -0
- package/databases/sqlite_db.js +158 -0
- package/index.js +191 -0
- package/lib/AbstractDatabase.js +32 -0
- package/lib/CacheAndBufferLayer.js +610 -0
- package/package.json +122 -0
- package/test/lib/databases.js +62 -0
- package/test/lib/mysql.sql +84 -0
- package/test/test.js +312 -0
- package/test/test_bulk.js +71 -0
- package/test/test_lru.js +145 -0
- package/test/test_metrics.js +733 -0
- package/test/test_mysql.js +68 -0
- package/test/test_postgres.js +17 -0
|
@@ -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
|
+
};
|