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,733 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const assert = require('assert').strict;
|
|
4
|
+
const ueberdb = require('../index');
|
|
5
|
+
const util = require('util');
|
|
6
|
+
|
|
7
|
+
// Gate is a normal Promise that resolves when its open() method is called.
|
|
8
|
+
class Gate extends Promise {
|
|
9
|
+
constructor(executor = null) {
|
|
10
|
+
let open;
|
|
11
|
+
super((resolve, reject) => {
|
|
12
|
+
open = resolve;
|
|
13
|
+
if (executor != null) executor(resolve, reject);
|
|
14
|
+
});
|
|
15
|
+
this.open = open;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const diffMetrics = (before, after) => {
|
|
20
|
+
const diff = {};
|
|
21
|
+
assert.equal(Object.keys(before).length, Object.keys(after).length);
|
|
22
|
+
for (const [k, bv] of Object.entries(before)) {
|
|
23
|
+
assert(bv != null);
|
|
24
|
+
const av = after[k];
|
|
25
|
+
assert(av != null);
|
|
26
|
+
if (av - bv > 0) diff[k] = av - bv;
|
|
27
|
+
}
|
|
28
|
+
return diff;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const assertMetricsDelta = (before, after, wantDelta) => {
|
|
32
|
+
wantDelta = {...wantDelta};
|
|
33
|
+
for (const [k, v] of Object.entries(wantDelta)) {
|
|
34
|
+
if (v === 0) delete wantDelta[k];
|
|
35
|
+
}
|
|
36
|
+
assert.deepEqual(diffMetrics(before, after), wantDelta);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
describe(__filename, function () {
|
|
40
|
+
let db;
|
|
41
|
+
let key;
|
|
42
|
+
let mock;
|
|
43
|
+
|
|
44
|
+
before(async function () {
|
|
45
|
+
const settings = {};
|
|
46
|
+
const udb = new ueberdb.Database('mock', settings);
|
|
47
|
+
mock = settings.mock;
|
|
48
|
+
db = {metrics: udb.metrics};
|
|
49
|
+
const fns = ['init', 'close', 'get', 'getSub', 'findKeys', 'flush', 'remove', 'set', 'setSub'];
|
|
50
|
+
for (const fn of fns) db[fn] = util.promisify(udb[fn].bind(udb));
|
|
51
|
+
mock.once('init', (cb) => cb());
|
|
52
|
+
await db.init();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
after(async function () {
|
|
56
|
+
mock.once('close', (cb) => cb());
|
|
57
|
+
await db.close();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
beforeEach(async function () {
|
|
61
|
+
key = this.currentTest.fullTitle(); // Use test title to avoid collisions with other tests.
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
afterEach(async function () {
|
|
65
|
+
mock.removeAllListeners();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('reads', function () {
|
|
69
|
+
const tcs = [
|
|
70
|
+
{name: 'get', f: (key) => db.get(key)},
|
|
71
|
+
{name: 'getSub', f: (key) => db.getSub(key, ['s'])},
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
for (const tc of tcs) {
|
|
75
|
+
describe(tc.name, function () {
|
|
76
|
+
const subtcs = [
|
|
77
|
+
{
|
|
78
|
+
name: 'cache miss',
|
|
79
|
+
val: '{"s": "v"}',
|
|
80
|
+
wantMetrics: {
|
|
81
|
+
lockReleases: 1,
|
|
82
|
+
readsFinished: 1,
|
|
83
|
+
readsFromDbFinished: 1,
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: 'cache hit',
|
|
88
|
+
cacheHit: true,
|
|
89
|
+
val: '{"s": "v"}',
|
|
90
|
+
wantMetrics: {
|
|
91
|
+
lockAcquires: 1,
|
|
92
|
+
lockReleases: 1,
|
|
93
|
+
reads: 1,
|
|
94
|
+
readsFinished: 1,
|
|
95
|
+
readsFromCache: 1,
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: 'read error',
|
|
100
|
+
err: new Error('test'),
|
|
101
|
+
wantMetrics: {
|
|
102
|
+
lockReleases: 1,
|
|
103
|
+
readsFailed: 1,
|
|
104
|
+
readsFinished: 1,
|
|
105
|
+
readsFromDbFailed: 1,
|
|
106
|
+
readsFromDbFinished: 1,
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: 'json error',
|
|
111
|
+
val: 'ignore me -- this is intentionally invalid json',
|
|
112
|
+
wantJsonErr: true,
|
|
113
|
+
wantMetrics: {
|
|
114
|
+
lockReleases: 1,
|
|
115
|
+
readsFailed: 1,
|
|
116
|
+
readsFinished: 1,
|
|
117
|
+
readsFromDbFinished: 1,
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
for (const subtc of subtcs) {
|
|
123
|
+
it(subtc.name, async function () {
|
|
124
|
+
if (subtc.cacheHit) {
|
|
125
|
+
mock.once('get', (key, cb) => { cb(null, subtc.val); });
|
|
126
|
+
await tc.f(key);
|
|
127
|
+
}
|
|
128
|
+
let finishDbRead;
|
|
129
|
+
const dbReadStarted = new Promise((resolve) => {
|
|
130
|
+
mock.once('get', (key, cb) => {
|
|
131
|
+
assert(!subtc.cacheHit, 'value should have been cached');
|
|
132
|
+
resolve();
|
|
133
|
+
new Promise((resolve) => { finishDbRead = resolve; })
|
|
134
|
+
.then(() => cb(subtc.err, subtc.val));
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
let before = {...db.metrics};
|
|
138
|
+
let readFinished = tc.f(key);
|
|
139
|
+
if (!subtc.cacheHit) {
|
|
140
|
+
await dbReadStarted;
|
|
141
|
+
assertMetricsDelta(before, db.metrics, {
|
|
142
|
+
lockAcquires: 1,
|
|
143
|
+
reads: 1,
|
|
144
|
+
readsFromDb: 1,
|
|
145
|
+
});
|
|
146
|
+
before = {...db.metrics};
|
|
147
|
+
finishDbRead();
|
|
148
|
+
}
|
|
149
|
+
if (subtc.err) readFinished = assert.rejects(readFinished, subtc.err);
|
|
150
|
+
if (subtc.wantJsonErr) readFinished = assert.rejects(readFinished, {message: /JSON/});
|
|
151
|
+
await readFinished;
|
|
152
|
+
assertMetricsDelta(before, db.metrics, subtc.wantMetrics);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
it('read of in-progress write', async function () {
|
|
157
|
+
let finishWrite;
|
|
158
|
+
const writeStarted = new Promise((resolve) => {
|
|
159
|
+
mock.once('set', (key, val, cb) => {
|
|
160
|
+
resolve();
|
|
161
|
+
new Promise((resolve) => { finishWrite = resolve; }).then(() => cb());
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
const writeFinished = db.set(key, {s: 'v'});
|
|
165
|
+
const flushed = db.flush(); // Speed up the tests.
|
|
166
|
+
await writeStarted;
|
|
167
|
+
mock.once('get', (key, cb) => { assert.fail('value should be cached'); });
|
|
168
|
+
const before = {...db.metrics};
|
|
169
|
+
await tc.f(key);
|
|
170
|
+
assertMetricsDelta(before, db.metrics, {
|
|
171
|
+
lockAcquires: 1,
|
|
172
|
+
lockReleases: 1,
|
|
173
|
+
reads: 1,
|
|
174
|
+
readsFinished: 1,
|
|
175
|
+
readsFromCache: 1,
|
|
176
|
+
});
|
|
177
|
+
finishWrite();
|
|
178
|
+
await writeFinished;
|
|
179
|
+
await flushed;
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe('writes', function () {
|
|
186
|
+
const tcs = [
|
|
187
|
+
{
|
|
188
|
+
name: 'remove ok',
|
|
189
|
+
action: async () => await db.remove(key),
|
|
190
|
+
wantOps: [
|
|
191
|
+
{
|
|
192
|
+
wantFns: ['remove'],
|
|
193
|
+
wantMetricsDelta: {
|
|
194
|
+
lockAcquires: 1,
|
|
195
|
+
lockReleases: 1,
|
|
196
|
+
writes: 1,
|
|
197
|
+
writesToDb: 1,
|
|
198
|
+
},
|
|
199
|
+
cbArgs: [[null]],
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
wantErr: null,
|
|
203
|
+
wantMetricsDelta: {
|
|
204
|
+
writesFinished: 1,
|
|
205
|
+
writesToDbFinished: 1,
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
name: 'remove error',
|
|
210
|
+
action: async () => await db.remove(key),
|
|
211
|
+
wantOps: [
|
|
212
|
+
{
|
|
213
|
+
wantFns: ['remove'],
|
|
214
|
+
wantMetricsDelta: {
|
|
215
|
+
lockAcquires: 1,
|
|
216
|
+
lockReleases: 1,
|
|
217
|
+
writes: 1,
|
|
218
|
+
writesToDb: 1,
|
|
219
|
+
},
|
|
220
|
+
cbArgs: [[new Error('test')]],
|
|
221
|
+
},
|
|
222
|
+
],
|
|
223
|
+
wantErr: {message: 'test'},
|
|
224
|
+
wantMetricsDelta: {
|
|
225
|
+
writesFailed: 1,
|
|
226
|
+
writesFinished: 1,
|
|
227
|
+
writesToDbFailed: 1,
|
|
228
|
+
writesToDbFinished: 1,
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
name: 'set ok',
|
|
233
|
+
action: async () => await db.set(key, 'v'),
|
|
234
|
+
wantOps: [
|
|
235
|
+
{
|
|
236
|
+
wantFns: ['set'],
|
|
237
|
+
wantMetricsDelta: {
|
|
238
|
+
lockAcquires: 1,
|
|
239
|
+
lockReleases: 1,
|
|
240
|
+
writes: 1,
|
|
241
|
+
writesToDb: 1,
|
|
242
|
+
},
|
|
243
|
+
cbArgs: [[null]],
|
|
244
|
+
},
|
|
245
|
+
],
|
|
246
|
+
wantErr: null,
|
|
247
|
+
wantMetricsDelta: {
|
|
248
|
+
writesFinished: 1,
|
|
249
|
+
writesToDbFinished: 1,
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
name: 'set db error',
|
|
254
|
+
action: async () => await db.set(key, 'v'),
|
|
255
|
+
wantOps: [
|
|
256
|
+
{
|
|
257
|
+
wantFns: ['set'],
|
|
258
|
+
wantMetricsDelta: {
|
|
259
|
+
lockAcquires: 1,
|
|
260
|
+
lockReleases: 1,
|
|
261
|
+
writes: 1,
|
|
262
|
+
writesToDb: 1,
|
|
263
|
+
},
|
|
264
|
+
cbArgs: [[new Error('test')]],
|
|
265
|
+
},
|
|
266
|
+
],
|
|
267
|
+
wantErr: {message: 'test'},
|
|
268
|
+
wantMetricsDelta: {
|
|
269
|
+
writesFailed: 1,
|
|
270
|
+
writesFinished: 1,
|
|
271
|
+
writesToDbFailed: 1,
|
|
272
|
+
writesToDbFinished: 1,
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
name: 'set json error',
|
|
277
|
+
action: async () => await db.set(key, BigInt(1)), // BigInts are not JSONable.
|
|
278
|
+
wantOps: [],
|
|
279
|
+
wantErr: {name: 'TypeError'},
|
|
280
|
+
wantMetricsDelta: {
|
|
281
|
+
lockAcquires: 1,
|
|
282
|
+
lockReleases: 1,
|
|
283
|
+
writes: 1,
|
|
284
|
+
writesFailed: 1,
|
|
285
|
+
writesFinished: 1,
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
name: 'setSub ok',
|
|
290
|
+
action: async () => await db.setSub(key, ['s'], 'v2'),
|
|
291
|
+
wantOps: [
|
|
292
|
+
{
|
|
293
|
+
wantFns: ['get'],
|
|
294
|
+
wantMetricsDelta: {
|
|
295
|
+
lockAcquires: 1,
|
|
296
|
+
reads: 1,
|
|
297
|
+
readsFromDb: 1,
|
|
298
|
+
},
|
|
299
|
+
cbArgs: [[null, '{"s": "v1"}']],
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
wantFns: ['set'],
|
|
303
|
+
wantMetricsDelta: {
|
|
304
|
+
lockReleases: 1,
|
|
305
|
+
readsFinished: 1,
|
|
306
|
+
readsFromDbFinished: 1,
|
|
307
|
+
writes: 1,
|
|
308
|
+
writesToDb: 1,
|
|
309
|
+
},
|
|
310
|
+
cbArgs: [[null]],
|
|
311
|
+
},
|
|
312
|
+
],
|
|
313
|
+
wantErr: null,
|
|
314
|
+
wantMetricsDelta: {
|
|
315
|
+
writesFinished: 1,
|
|
316
|
+
writesToDbFinished: 1,
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
name: 'setSub db write error',
|
|
321
|
+
action: async () => await db.setSub(key, ['s'], 'v2'),
|
|
322
|
+
wantOps: [
|
|
323
|
+
{
|
|
324
|
+
wantFns: ['get'],
|
|
325
|
+
wantMetricsDelta: {
|
|
326
|
+
lockAcquires: 1,
|
|
327
|
+
reads: 1,
|
|
328
|
+
readsFromDb: 1,
|
|
329
|
+
},
|
|
330
|
+
cbArgs: [[null, '{"s": "v1"}']],
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
wantFns: ['set'],
|
|
334
|
+
wantMetricsDelta: {
|
|
335
|
+
lockReleases: 1,
|
|
336
|
+
readsFinished: 1,
|
|
337
|
+
readsFromDbFinished: 1,
|
|
338
|
+
writes: 1,
|
|
339
|
+
writesToDb: 1,
|
|
340
|
+
},
|
|
341
|
+
cbArgs: [[new Error('test')]],
|
|
342
|
+
},
|
|
343
|
+
],
|
|
344
|
+
wantErr: {message: 'test'},
|
|
345
|
+
wantMetricsDelta: {
|
|
346
|
+
writesFailed: 1,
|
|
347
|
+
writesFinished: 1,
|
|
348
|
+
writesToDbFailed: 1,
|
|
349
|
+
writesToDbFinished: 1,
|
|
350
|
+
},
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
name: 'setSub db read error',
|
|
354
|
+
action: async () => await db.setSub(key, ['s'], 'v2'),
|
|
355
|
+
wantOps: [
|
|
356
|
+
{
|
|
357
|
+
wantFns: ['get'],
|
|
358
|
+
wantMetricsDelta: {
|
|
359
|
+
lockAcquires: 1,
|
|
360
|
+
reads: 1,
|
|
361
|
+
readsFromDb: 1,
|
|
362
|
+
},
|
|
363
|
+
cbArgs: [[new Error('test')]],
|
|
364
|
+
},
|
|
365
|
+
],
|
|
366
|
+
wantErr: {message: 'test'},
|
|
367
|
+
wantMetricsDelta: {
|
|
368
|
+
lockReleases: 1,
|
|
369
|
+
readsFailed: 1,
|
|
370
|
+
readsFinished: 1,
|
|
371
|
+
readsFromDbFailed: 1,
|
|
372
|
+
readsFromDbFinished: 1,
|
|
373
|
+
writes: 1,
|
|
374
|
+
writesFailed: 1,
|
|
375
|
+
writesFinished: 1,
|
|
376
|
+
},
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
name: 'setSub json read error',
|
|
380
|
+
action: async () => await db.setSub(key, ['s'], 'v2'),
|
|
381
|
+
wantOps: [
|
|
382
|
+
{
|
|
383
|
+
wantFns: ['get'],
|
|
384
|
+
wantMetricsDelta: {
|
|
385
|
+
lockAcquires: 1,
|
|
386
|
+
reads: 1,
|
|
387
|
+
readsFromDb: 1,
|
|
388
|
+
},
|
|
389
|
+
cbArgs: [[null, 'ignore me -- this is intentionally invalid json']],
|
|
390
|
+
},
|
|
391
|
+
],
|
|
392
|
+
wantErr: {name: 'SyntaxError'},
|
|
393
|
+
wantMetricsDelta: {
|
|
394
|
+
lockReleases: 1,
|
|
395
|
+
readsFailed: 1,
|
|
396
|
+
readsFinished: 1,
|
|
397
|
+
readsFromDbFinished: 1,
|
|
398
|
+
writes: 1,
|
|
399
|
+
writesFailed: 1,
|
|
400
|
+
writesFinished: 1,
|
|
401
|
+
},
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
name: 'setSub update non-object error',
|
|
405
|
+
action: async () => await db.setSub(key, ['s'], 'v2'),
|
|
406
|
+
wantOps: [
|
|
407
|
+
{
|
|
408
|
+
wantFns: ['get'],
|
|
409
|
+
wantMetricsDelta: {
|
|
410
|
+
lockAcquires: 1,
|
|
411
|
+
reads: 1,
|
|
412
|
+
readsFromDb: 1,
|
|
413
|
+
},
|
|
414
|
+
cbArgs: [[null, '"foo"']],
|
|
415
|
+
},
|
|
416
|
+
],
|
|
417
|
+
wantErr: {message: /non-object/},
|
|
418
|
+
wantMetricsDelta: {
|
|
419
|
+
lockReleases: 1,
|
|
420
|
+
readsFinished: 1,
|
|
421
|
+
readsFromDbFinished: 1,
|
|
422
|
+
writes: 1,
|
|
423
|
+
writesFailed: 1,
|
|
424
|
+
writesFinished: 1,
|
|
425
|
+
},
|
|
426
|
+
},
|
|
427
|
+
{
|
|
428
|
+
name: 'setSub json write error',
|
|
429
|
+
action: async () => await db.setSub(key, ['s'], BigInt(1)), // BigInts are not JSONable.
|
|
430
|
+
wantOps: [
|
|
431
|
+
{
|
|
432
|
+
wantFns: ['get'],
|
|
433
|
+
wantMetricsDelta: {
|
|
434
|
+
lockAcquires: 1,
|
|
435
|
+
reads: 1,
|
|
436
|
+
readsFromDb: 1,
|
|
437
|
+
},
|
|
438
|
+
cbArgs: [[null, '{"s": "v1"}']],
|
|
439
|
+
},
|
|
440
|
+
],
|
|
441
|
+
wantErr: {name: 'TypeError'},
|
|
442
|
+
wantMetricsDelta: {
|
|
443
|
+
lockReleases: 1,
|
|
444
|
+
readsFinished: 1,
|
|
445
|
+
readsFromDbFinished: 1,
|
|
446
|
+
writes: 1,
|
|
447
|
+
writesFailed: 1,
|
|
448
|
+
writesFinished: 1,
|
|
449
|
+
},
|
|
450
|
+
},
|
|
451
|
+
{
|
|
452
|
+
name: 'doBulk ok',
|
|
453
|
+
action: async () => await Promise.all([db.set(key, 'v'), db.set(`${key} second op`, 'v')]),
|
|
454
|
+
wantOps: [
|
|
455
|
+
{
|
|
456
|
+
wantFns: ['doBulk'],
|
|
457
|
+
wantMetricsDelta: {
|
|
458
|
+
lockAcquires: 2,
|
|
459
|
+
lockReleases: 2,
|
|
460
|
+
writes: 2,
|
|
461
|
+
writesToDb: 2,
|
|
462
|
+
},
|
|
463
|
+
cbArgs: [[null]],
|
|
464
|
+
},
|
|
465
|
+
],
|
|
466
|
+
wantErr: null,
|
|
467
|
+
wantMetricsDelta: {
|
|
468
|
+
writesFinished: 2,
|
|
469
|
+
writesToDbFinished: 2,
|
|
470
|
+
},
|
|
471
|
+
},
|
|
472
|
+
{
|
|
473
|
+
name: 'doBulk error, all retries ok',
|
|
474
|
+
action: async () => await Promise.all([db.set(key, 'v'), db.set(`${key} second op`, 'v')]),
|
|
475
|
+
wantOps: [
|
|
476
|
+
{
|
|
477
|
+
wantFns: ['doBulk'],
|
|
478
|
+
wantMetricsDelta: {
|
|
479
|
+
lockAcquires: 2,
|
|
480
|
+
lockReleases: 2,
|
|
481
|
+
writes: 2,
|
|
482
|
+
writesToDb: 2,
|
|
483
|
+
},
|
|
484
|
+
cbArgs: [[new Error('injected doBulk error')]],
|
|
485
|
+
},
|
|
486
|
+
{
|
|
487
|
+
wantFns: ['set', 'set'],
|
|
488
|
+
wantMetricsDelta: {
|
|
489
|
+
writesToDbRetried: 2,
|
|
490
|
+
},
|
|
491
|
+
cbArgs: [[null], [null]],
|
|
492
|
+
},
|
|
493
|
+
],
|
|
494
|
+
wantErr: null,
|
|
495
|
+
wantMetricsDelta: {
|
|
496
|
+
writesFinished: 2,
|
|
497
|
+
writesToDbFinished: 2,
|
|
498
|
+
},
|
|
499
|
+
},
|
|
500
|
+
{
|
|
501
|
+
name: 'doBulk error, one of the retries fails',
|
|
502
|
+
action: async () => await Promise.all([db.set(key, 'v'), db.set(`${key} second op`, 'v')]),
|
|
503
|
+
wantOps: [
|
|
504
|
+
{
|
|
505
|
+
wantFns: ['doBulk'],
|
|
506
|
+
wantMetricsDelta: {
|
|
507
|
+
lockAcquires: 2,
|
|
508
|
+
lockReleases: 2,
|
|
509
|
+
writes: 2,
|
|
510
|
+
writesToDb: 2,
|
|
511
|
+
},
|
|
512
|
+
cbArgs: [[new Error('injected doBulk error')]],
|
|
513
|
+
},
|
|
514
|
+
{
|
|
515
|
+
wantFns: ['set', 'set'],
|
|
516
|
+
wantMetricsDelta: {
|
|
517
|
+
writesToDbRetried: 2,
|
|
518
|
+
},
|
|
519
|
+
cbArgs: [[new Error('test')], [null]],
|
|
520
|
+
},
|
|
521
|
+
],
|
|
522
|
+
wantErr: {message: 'test'},
|
|
523
|
+
wantMetricsDelta: {
|
|
524
|
+
writesFailed: 1,
|
|
525
|
+
writesFinished: 2,
|
|
526
|
+
writesToDbFailed: 1,
|
|
527
|
+
writesToDbFinished: 2,
|
|
528
|
+
},
|
|
529
|
+
},
|
|
530
|
+
{
|
|
531
|
+
name: 'doBulk error, all retries fail',
|
|
532
|
+
action: async () => await Promise.all([db.set(key, 'v'), db.set(`${key} second op`, 'v')]),
|
|
533
|
+
wantOps: [
|
|
534
|
+
{
|
|
535
|
+
wantFns: ['doBulk'],
|
|
536
|
+
wantMetricsDelta: {
|
|
537
|
+
lockAcquires: 2,
|
|
538
|
+
lockReleases: 2,
|
|
539
|
+
writes: 2,
|
|
540
|
+
writesToDb: 2,
|
|
541
|
+
},
|
|
542
|
+
cbArgs: [[new Error('injected doBulk error')]],
|
|
543
|
+
},
|
|
544
|
+
{
|
|
545
|
+
wantFns: ['set', 'set'],
|
|
546
|
+
wantMetricsDelta: {
|
|
547
|
+
writesToDbRetried: 2,
|
|
548
|
+
},
|
|
549
|
+
cbArgs: [[new Error('test1')], [new Error('test2')]],
|
|
550
|
+
},
|
|
551
|
+
],
|
|
552
|
+
wantErr: (err) => ['test1', 'test2'].includes(err.message),
|
|
553
|
+
wantMetricsDelta: {
|
|
554
|
+
writesFailed: 2,
|
|
555
|
+
writesFinished: 2,
|
|
556
|
+
writesToDbFailed: 2,
|
|
557
|
+
writesToDbFinished: 2,
|
|
558
|
+
},
|
|
559
|
+
},
|
|
560
|
+
{
|
|
561
|
+
name: 'obsoleted ok',
|
|
562
|
+
action: async () => await Promise.all([db.set(key, 'v'), db.set(key, 'v2')]),
|
|
563
|
+
wantOps: [
|
|
564
|
+
{
|
|
565
|
+
wantFns: ['set'],
|
|
566
|
+
wantMetricsDelta: {
|
|
567
|
+
lockAcquires: 2,
|
|
568
|
+
lockAwaits: 1,
|
|
569
|
+
lockReleases: 2,
|
|
570
|
+
writes: 2,
|
|
571
|
+
writesObsoleted: 1,
|
|
572
|
+
writesToDb: 1,
|
|
573
|
+
},
|
|
574
|
+
cbArgs: [[null]],
|
|
575
|
+
},
|
|
576
|
+
],
|
|
577
|
+
wantErr: null,
|
|
578
|
+
wantMetricsDelta: {
|
|
579
|
+
writesFinished: 2,
|
|
580
|
+
writesToDbFinished: 1,
|
|
581
|
+
},
|
|
582
|
+
},
|
|
583
|
+
{
|
|
584
|
+
name: 'obsoleted error',
|
|
585
|
+
action: async () => await Promise.all([db.set(key, 'v'), db.set(key, 'v2')]),
|
|
586
|
+
wantOps: [
|
|
587
|
+
{
|
|
588
|
+
wantFns: ['set'],
|
|
589
|
+
wantMetricsDelta: {
|
|
590
|
+
lockAcquires: 2,
|
|
591
|
+
lockAwaits: 1,
|
|
592
|
+
lockReleases: 2,
|
|
593
|
+
writes: 2,
|
|
594
|
+
writesObsoleted: 1,
|
|
595
|
+
writesToDb: 1,
|
|
596
|
+
},
|
|
597
|
+
cbArgs: [[new Error('test')]],
|
|
598
|
+
},
|
|
599
|
+
],
|
|
600
|
+
wantErr: {message: 'test'},
|
|
601
|
+
wantMetricsDelta: {
|
|
602
|
+
writesFailed: 2,
|
|
603
|
+
writesFinished: 2,
|
|
604
|
+
writesToDbFailed: 1,
|
|
605
|
+
writesToDbFinished: 1,
|
|
606
|
+
},
|
|
607
|
+
},
|
|
608
|
+
];
|
|
609
|
+
|
|
610
|
+
for (const tc of tcs) {
|
|
611
|
+
it(tc.name, async function () {
|
|
612
|
+
const opStarts = [];
|
|
613
|
+
for (const fn of ['doBulk', 'get', 'remove', 'set']) {
|
|
614
|
+
mock.on(fn, (...args) => {
|
|
615
|
+
const opStart = opStarts.shift();
|
|
616
|
+
const cb = args.pop();
|
|
617
|
+
opStart.open([fn, cb]);
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
let before = {...db.metrics};
|
|
621
|
+
let actionDone;
|
|
622
|
+
// advance() triggers the next database operation(s), either by starting tc.action (if
|
|
623
|
+
// tc.action has not yet been started) or completing the previous operation(s) (if tc.action
|
|
624
|
+
// has been started).
|
|
625
|
+
let advance = () => { actionDone = tc.action(); };
|
|
626
|
+
for (const ops of tc.wantOps) {
|
|
627
|
+
// Provide a way for the mock database to tell us that a mocked database method has been
|
|
628
|
+
// called. The number of expected parallel operations for this iteration is
|
|
629
|
+
// ops.wantFns.length, so that is the number of Gates that are added to opStarts. Each
|
|
630
|
+
// Gate resolves to [fn, cb] where fn is the name of the mocked database method and cb is
|
|
631
|
+
// the mocked database method's callback.
|
|
632
|
+
for (let i = 0; i < ops.wantFns.length; ++i) opStarts.push(new Gate());
|
|
633
|
+
|
|
634
|
+
// Trigger the call(s) to the mock database method(s). This is scheduled to run in the
|
|
635
|
+
// future to ensure that advance() does not empty the opStarts array until after the
|
|
636
|
+
// Promise.all() call below has a chance to see all of the Promises in opStarts.
|
|
637
|
+
setImmediate(advance);
|
|
638
|
+
|
|
639
|
+
// Wait until the expected number of parallel database method calls have started.
|
|
640
|
+
const gotOps = await Promise.all(opStarts);
|
|
641
|
+
|
|
642
|
+
assertMetricsDelta(before, db.metrics, ops.wantMetricsDelta);
|
|
643
|
+
before = {...db.metrics};
|
|
644
|
+
|
|
645
|
+
const advanceFns = [];
|
|
646
|
+
for (const [gotFn, cb] of gotOps) {
|
|
647
|
+
const i = ops.wantFns.indexOf(gotFn);
|
|
648
|
+
assert(i >= 0, `unexpected mock database method call: ${gotFn}`);
|
|
649
|
+
ops.wantFns.splice(i, 1);
|
|
650
|
+
const [cbArgs] = ops.cbArgs.splice(i, 1);
|
|
651
|
+
advanceFns.push(() => cb(...cbArgs));
|
|
652
|
+
}
|
|
653
|
+
assert.equal(ops.wantFns.length, 0, `missing call(s): ${ops.wantFns.join(', ')}`);
|
|
654
|
+
advance = () => advanceFns.forEach((f) => f());
|
|
655
|
+
}
|
|
656
|
+
advance();
|
|
657
|
+
await (tc.wantErr ? assert.rejects(actionDone, tc.wantErr) : actionDone);
|
|
658
|
+
assertMetricsDelta(before, db.metrics, tc.wantMetricsDelta);
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
describe('lock contention', function () {
|
|
664
|
+
const tcs = [
|
|
665
|
+
{
|
|
666
|
+
name: 'get',
|
|
667
|
+
f: (key) => db.get(key),
|
|
668
|
+
wantMetrics: {lockAwaits: 1},
|
|
669
|
+
},
|
|
670
|
+
{
|
|
671
|
+
name: 'getSub',
|
|
672
|
+
fn: 'get',
|
|
673
|
+
f: (key) => db.getSub(key, ['s']),
|
|
674
|
+
wantMetrics: {lockAwaits: 1},
|
|
675
|
+
},
|
|
676
|
+
{
|
|
677
|
+
name: 'remove',
|
|
678
|
+
f: (key) => db.remove(key),
|
|
679
|
+
wantMetrics: {lockAwaits: 1},
|
|
680
|
+
},
|
|
681
|
+
{
|
|
682
|
+
name: 'set',
|
|
683
|
+
f: (key) => db.set(key, 'v'),
|
|
684
|
+
wantMetrics: {lockAwaits: 1},
|
|
685
|
+
},
|
|
686
|
+
{
|
|
687
|
+
name: 'setSub',
|
|
688
|
+
fn: 'set',
|
|
689
|
+
f: (key) => db.setSub(key, ['s'], 'v'),
|
|
690
|
+
wantMetrics: {lockAwaits: 1},
|
|
691
|
+
},
|
|
692
|
+
{
|
|
693
|
+
name: 'doBulk',
|
|
694
|
+
f: (key) => Promise.all([
|
|
695
|
+
db.set(key, 'v'),
|
|
696
|
+
db.set(`${key} second op`, 'v'),
|
|
697
|
+
]),
|
|
698
|
+
wantMetrics: {lockAcquires: 1, lockAwaits: 1},
|
|
699
|
+
},
|
|
700
|
+
];
|
|
701
|
+
|
|
702
|
+
for (const tc of tcs) {
|
|
703
|
+
if (tc.fn == null) tc.fn = tc.name;
|
|
704
|
+
|
|
705
|
+
it(tc.name, async function () {
|
|
706
|
+
let finishRead;
|
|
707
|
+
const readStarted = new Promise((resolve) => {
|
|
708
|
+
mock.once('get', (key, cb) => {
|
|
709
|
+
resolve();
|
|
710
|
+
const val = '{"s": "v"}';
|
|
711
|
+
new Promise((resolve) => { finishRead = resolve; }).then(() => cb(null, val));
|
|
712
|
+
});
|
|
713
|
+
});
|
|
714
|
+
// Note: All contention tests should be with get() to ensure that all functions lock using
|
|
715
|
+
// the record's key.
|
|
716
|
+
const getP = db.get(key);
|
|
717
|
+
await readStarted;
|
|
718
|
+
mock.once(tc.fn, (...args) => {
|
|
719
|
+
assert(tc.fn !== 'get', 'value should have been cached');
|
|
720
|
+
args.pop()();
|
|
721
|
+
});
|
|
722
|
+
const before = {...db.metrics};
|
|
723
|
+
const opFinished = tc.f(key);
|
|
724
|
+
const flushed = db.flush(); // Speed up tests.
|
|
725
|
+
assertMetricsDelta(before, db.metrics, tc.wantMetrics);
|
|
726
|
+
finishRead();
|
|
727
|
+
await getP;
|
|
728
|
+
await opFinished;
|
|
729
|
+
await flushed;
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
});
|
|
733
|
+
});
|