antietcd 1.1.4 → 1.2.1
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 +53 -3
- package/anticli.js +100 -15
- package/anticluster.js +135 -53
- package/antietcd-app.js +20 -4
- package/antietcd.js +193 -47
- package/antilocker.js +212 -0
- package/etctree.js +103 -60
- package/package.json +3 -3
package/antietcd.js
CHANGED
|
@@ -12,12 +12,15 @@ const EventEmitter = require('events');
|
|
|
12
12
|
|
|
13
13
|
const ws = require('ws');
|
|
14
14
|
|
|
15
|
+
const TinyRaft = require('tinyraft');
|
|
16
|
+
|
|
15
17
|
const EtcTree = require('./etctree.js');
|
|
16
18
|
const AntiPersistence = require('./antipersistence.js');
|
|
17
19
|
const AntiCluster = require('./anticluster.js');
|
|
20
|
+
const AntiLocker = require('./antilocker.js');
|
|
18
21
|
const { runCallbacks, de64, b64, RequestError } = require('./common.js');
|
|
19
22
|
|
|
20
|
-
const VERSION = '1.1
|
|
23
|
+
const VERSION = '1.2.1';
|
|
21
24
|
|
|
22
25
|
class AntiEtcd extends EventEmitter
|
|
23
26
|
{
|
|
@@ -27,11 +30,27 @@ class AntiEtcd extends EventEmitter
|
|
|
27
30
|
cfg['merge_watches'] = !('merge_watches' in cfg) || is_true(cfg['merge_watches']);
|
|
28
31
|
cfg['stale_read'] = !('stale_read' in cfg) || is_true(cfg['stale_read']);
|
|
29
32
|
cfg['use_base64'] = !('use_base64' in cfg) || is_true(cfg['use_base64']);
|
|
33
|
+
cfg['use_locks'] = cfg.cluster && (!('use_locks' in cfg) || is_true(cfg['use_locks']));
|
|
34
|
+
cfg['lock_wait_timeout'] = Number(cfg['lock_wait_timeout']);
|
|
35
|
+
if (!cfg['lock_wait_timeout'] || cfg['lock_wait_timeout'] < 1000)
|
|
36
|
+
cfg['lock_wait_timeout'] = 30000;
|
|
37
|
+
if (typeof cfg['logs'] == 'string')
|
|
38
|
+
{
|
|
39
|
+
const logs = {};
|
|
40
|
+
for (const k of cfg['logs'].split(/,/))
|
|
41
|
+
logs[k] = true;
|
|
42
|
+
cfg['logs'] = logs;
|
|
43
|
+
}
|
|
44
|
+
else if (!cfg['logs'])
|
|
45
|
+
cfg['logs'] = {};
|
|
46
|
+
else if (!(cfg['logs'] instanceof Object))
|
|
47
|
+
throw new Error('logs should be object or string with keywords (access,watch,cluster)');
|
|
30
48
|
this.clients = {};
|
|
31
49
|
this.client_id = 1;
|
|
32
|
-
this.etctree = new EtcTree(
|
|
50
|
+
this.etctree = new EtcTree();
|
|
33
51
|
this.persistence = null;
|
|
34
52
|
this.cluster = null;
|
|
53
|
+
this.locker = null;
|
|
35
54
|
this.stored_term = 0;
|
|
36
55
|
this.cfg = cfg;
|
|
37
56
|
this.loading = false;
|
|
@@ -57,6 +76,11 @@ class AntiEtcd extends EventEmitter
|
|
|
57
76
|
{
|
|
58
77
|
this.cluster = new AntiCluster(this);
|
|
59
78
|
}
|
|
79
|
+
if (this.cfg.use_locks)
|
|
80
|
+
{
|
|
81
|
+
this.locker = new AntiLocker(this.cfg);
|
|
82
|
+
this.locker.set_resume_notifications(rev => this.etctree.resume_notifications(rev));
|
|
83
|
+
}
|
|
60
84
|
if (this.cfg.cert)
|
|
61
85
|
{
|
|
62
86
|
this.tls = {
|
|
@@ -103,6 +127,10 @@ class AntiEtcd extends EventEmitter
|
|
|
103
127
|
|
|
104
128
|
async _persistAndReplicate(msg)
|
|
105
129
|
{
|
|
130
|
+
if (this.cfg.use_locks && msg.events.length)
|
|
131
|
+
{
|
|
132
|
+
this.locker.set_txn_locks(msg.events, msg.header.revision);
|
|
133
|
+
}
|
|
106
134
|
let res = [];
|
|
107
135
|
if (this.cluster)
|
|
108
136
|
{
|
|
@@ -144,6 +172,20 @@ class AntiEtcd extends EventEmitter
|
|
|
144
172
|
this.etctree.compact(revision);
|
|
145
173
|
}
|
|
146
174
|
}
|
|
175
|
+
if (this.cfg.use_locks && msg.events.length &&
|
|
176
|
+
(!this.cluster || this.cluster.raft.state == TinyRaft.LEADER))
|
|
177
|
+
{
|
|
178
|
+
// Leader unlocks txn itself, followers wait for feedback from the leader
|
|
179
|
+
this.locker.unlock_txn(msg.header.revision);
|
|
180
|
+
if (this.cluster)
|
|
181
|
+
{
|
|
182
|
+
this.cluster.unlockChange(msg.header.revision).catch(e =>
|
|
183
|
+
{
|
|
184
|
+
if (!(e instanceof RequestError))
|
|
185
|
+
console.error(e);
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
147
189
|
}
|
|
148
190
|
|
|
149
191
|
_handleRequest(req, res)
|
|
@@ -198,7 +240,7 @@ class AntiEtcd extends EventEmitter
|
|
|
198
240
|
try
|
|
199
241
|
{
|
|
200
242
|
// Access log
|
|
201
|
-
if (this.cfg.
|
|
243
|
+
if (this.cfg.logs.access)
|
|
202
244
|
{
|
|
203
245
|
console.log(
|
|
204
246
|
new Date().toISOString()+
|
|
@@ -236,10 +278,10 @@ class AntiEtcd extends EventEmitter
|
|
|
236
278
|
socket,
|
|
237
279
|
alive: true,
|
|
238
280
|
watches: {},
|
|
281
|
+
pinger: null,
|
|
239
282
|
};
|
|
240
283
|
socket.on('pong', () => this.clients[client_id].alive = true);
|
|
241
|
-
|
|
242
|
-
const pinger = setInterval(() =>
|
|
284
|
+
const pinger = () =>
|
|
243
285
|
{
|
|
244
286
|
if (!this.clients[client_id])
|
|
245
287
|
{
|
|
@@ -251,7 +293,18 @@ class AntiEtcd extends EventEmitter
|
|
|
251
293
|
}
|
|
252
294
|
this.clients[client_id].alive = false;
|
|
253
295
|
socket.ping(() => {});
|
|
254
|
-
}
|
|
296
|
+
};
|
|
297
|
+
if (socket.readyState == ws.WebSocket.OPEN)
|
|
298
|
+
{
|
|
299
|
+
this.clients[client_id].pinger = setInterval(pinger, this.cfg.ws_keepalive_interval||30000);
|
|
300
|
+
}
|
|
301
|
+
else
|
|
302
|
+
{
|
|
303
|
+
socket.on('open', () =>
|
|
304
|
+
{
|
|
305
|
+
this.clients[client_id].pinger = setInterval(pinger, this.cfg.ws_keepalive_interval||30000);
|
|
306
|
+
});
|
|
307
|
+
}
|
|
255
308
|
socket.on('message', (msg) =>
|
|
256
309
|
{
|
|
257
310
|
try
|
|
@@ -272,17 +325,30 @@ class AntiEtcd extends EventEmitter
|
|
|
272
325
|
this._handleMessage(client_id, msg, socket);
|
|
273
326
|
}
|
|
274
327
|
});
|
|
275
|
-
|
|
328
|
+
const on_close = () =>
|
|
276
329
|
{
|
|
277
|
-
this.
|
|
278
|
-
|
|
279
|
-
|
|
330
|
+
if (this.clients[client_id])
|
|
331
|
+
{
|
|
332
|
+
this._unsubscribeClient(client_id);
|
|
333
|
+
if (this.clients[client_id].pinger)
|
|
334
|
+
{
|
|
335
|
+
clearInterval(this.clients[client_id].pinger);
|
|
336
|
+
this.clients[client_id].pinger = null;
|
|
337
|
+
}
|
|
338
|
+
delete this.clients[client_id];
|
|
339
|
+
}
|
|
280
340
|
socket.terminate();
|
|
281
341
|
if (reconnect)
|
|
282
342
|
{
|
|
283
343
|
reconnect();
|
|
284
344
|
}
|
|
345
|
+
};
|
|
346
|
+
socket.on('error', e =>
|
|
347
|
+
{
|
|
348
|
+
console.error(e.syscall === 'connect' ? e.message : e);
|
|
349
|
+
on_close();
|
|
285
350
|
});
|
|
351
|
+
socket.on('close', on_close);
|
|
286
352
|
return client_id;
|
|
287
353
|
}
|
|
288
354
|
|
|
@@ -352,13 +418,7 @@ class AntiEtcd extends EventEmitter
|
|
|
352
418
|
}
|
|
353
419
|
if (this.cluster && path !== 'dump' && path != 'maintenance_status')
|
|
354
420
|
{
|
|
355
|
-
const res = await this.cluster.checkRaftState(
|
|
356
|
-
path,
|
|
357
|
-
(data.leaderonly ? AntiCluster.LEADER_ONLY : 0) |
|
|
358
|
-
(data.serializable ? AntiCluster.READ_FROM_FOLLOWER : 0) |
|
|
359
|
-
(data.nowaitquorum ? AntiCluster.NO_WAIT_QUORUM : 0),
|
|
360
|
-
data
|
|
361
|
-
);
|
|
421
|
+
const res = await this.cluster.checkRaftState(path, data);
|
|
362
422
|
if (res)
|
|
363
423
|
{
|
|
364
424
|
return res;
|
|
@@ -416,13 +476,28 @@ class AntiEtcd extends EventEmitter
|
|
|
416
476
|
// public watch API
|
|
417
477
|
async create_watch(params, callback, stream_id)
|
|
418
478
|
{
|
|
419
|
-
|
|
479
|
+
params = this._encodeWatch(params);
|
|
480
|
+
if (this.cfg.use_locks && params.start_revision)
|
|
481
|
+
{
|
|
482
|
+
// watch is also a read, so it also has to follow read lock rules
|
|
483
|
+
const max_ts = Date.now() + this.cfg.lock_wait_timeout;
|
|
484
|
+
let lock_revision = 0;
|
|
485
|
+
while ((lock_revision = this.locker.check_locks(params.key, params.range_end)))
|
|
486
|
+
{
|
|
487
|
+
await this.locker.wait_commit(lock_revision, max_ts);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
const watch = this.etctree.api_create_watch({ ...params, watch_id: null }, (msg) => callback(this._encodeMsg(msg)), stream_id);
|
|
420
491
|
if (!watch.created)
|
|
421
492
|
{
|
|
422
493
|
throw new RequestError(400, 'Requested watch revision is compacted', { compact_revision: watch.compact_revision });
|
|
423
494
|
}
|
|
424
495
|
const watch_id = params.watch_id || watch.watch_id;
|
|
425
496
|
this.api_watches[watch_id] = watch.watch_id;
|
|
497
|
+
if (watch.events)
|
|
498
|
+
{
|
|
499
|
+
callback(this._encodeMsg({ ...watch, watch_id }));
|
|
500
|
+
}
|
|
426
501
|
return watch_id;
|
|
427
502
|
}
|
|
428
503
|
|
|
@@ -440,30 +515,51 @@ class AntiEtcd extends EventEmitter
|
|
|
440
515
|
// internal handlers
|
|
441
516
|
async _handle_kv_txn(data)
|
|
442
517
|
{
|
|
518
|
+
let decoded = data;
|
|
443
519
|
if (this.cfg.use_base64)
|
|
444
520
|
{
|
|
445
|
-
|
|
521
|
+
decoded = {};
|
|
522
|
+
if (data.compare)
|
|
446
523
|
{
|
|
447
|
-
|
|
448
|
-
|
|
524
|
+
decoded.compare = [];
|
|
525
|
+
for (const item of data.compare)
|
|
526
|
+
decoded.compare.push({ ...item, key: de64(item.key) });
|
|
449
527
|
}
|
|
450
|
-
for (const
|
|
528
|
+
for (const key of [ 'success', 'failure' ])
|
|
451
529
|
{
|
|
452
|
-
|
|
530
|
+
if (!data[key])
|
|
531
|
+
continue;
|
|
532
|
+
const actions = [];
|
|
533
|
+
for (const item of data[key])
|
|
453
534
|
{
|
|
454
|
-
const
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
535
|
+
const copy = {};
|
|
536
|
+
let r;
|
|
537
|
+
if ((r = (item.request_range || item.requestRange)))
|
|
538
|
+
copy.request_range = { ...r, key: de64(r.key), range_end: de64(r.range_end) };
|
|
539
|
+
else if ((r = (item.request_delete_range || item.requestDeleteRange)))
|
|
540
|
+
copy.request_delete_range = { ...r, key: de64(r.key), range_end: de64(r.range_end) };
|
|
541
|
+
else if ((r = (item.request_put || item.requestPut)))
|
|
542
|
+
copy.request_put = { ...r, key: de64(r.key), value: de64(r.value) };
|
|
543
|
+
actions.push(copy);
|
|
463
544
|
}
|
|
545
|
+
decoded[key] = actions;
|
|
464
546
|
}
|
|
465
547
|
}
|
|
466
|
-
|
|
548
|
+
if (this.cfg.use_locks)
|
|
549
|
+
{
|
|
550
|
+
const max_ts = Date.now() + this.cfg.lock_wait_timeout;
|
|
551
|
+
let lock_revision = 0;
|
|
552
|
+
while ((lock_revision = this.locker.check_txn_locks(decoded)))
|
|
553
|
+
{
|
|
554
|
+
await this.locker.wait_commit(lock_revision, max_ts);
|
|
555
|
+
const res = await this.cluster.checkRaftState('kv_txn', data);
|
|
556
|
+
if (res)
|
|
557
|
+
{
|
|
558
|
+
return res;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
const result = await this.etctree.api_txn(decoded);
|
|
467
563
|
if (this.cfg.use_base64)
|
|
468
564
|
{
|
|
469
565
|
for (const item of result.responses||[])
|
|
@@ -550,28 +646,23 @@ class AntiEtcd extends EventEmitter
|
|
|
550
646
|
_handleMessage(client_id, msg, socket)
|
|
551
647
|
{
|
|
552
648
|
const client = this.clients[client_id];
|
|
553
|
-
if (this.cfg.
|
|
649
|
+
if (this.cfg.logs.access)
|
|
554
650
|
{
|
|
555
651
|
console.log(new Date().toISOString()+' '+client.addr+' '+(client.raft_node_id || '-')+' -> '+JSON.stringify(msg));
|
|
556
652
|
}
|
|
557
653
|
if (msg.create_request)
|
|
558
654
|
{
|
|
559
|
-
const create_request = msg.create_request;
|
|
655
|
+
const create_request = this._encodeWatch(msg.create_request);
|
|
560
656
|
if (!create_request.watch_id || !client.watches[create_request.watch_id])
|
|
561
657
|
{
|
|
562
|
-
client.send_cb = client.send_cb || (msg => socket.send(JSON.stringify(this._encodeMsg(msg))));
|
|
563
|
-
|
|
564
|
-
this._encodeWatch(create_request), client.send_cb, (this.cfg.merge_watches ? 'C'+client_id : null)
|
|
565
|
-
);
|
|
566
|
-
if (!watch.created)
|
|
658
|
+
client.send_cb = client.send_cb || (msg => socket.send(JSON.stringify(this._encodeMsg(msg, client))));
|
|
659
|
+
if (this.cfg.use_locks && create_request.start_revision)
|
|
567
660
|
{
|
|
568
|
-
|
|
661
|
+
this._lock_and_create_watch(client, create_request);
|
|
569
662
|
}
|
|
570
663
|
else
|
|
571
664
|
{
|
|
572
|
-
|
|
573
|
-
client.watches[create_request.watch_id] = watch.watch_id;
|
|
574
|
-
socket.send(JSON.stringify({ result: { header: { revision: this.etctree.mod_revision }, watch_id: create_request.watch_id, created: true } }));
|
|
665
|
+
this._create_client_watch(client, create_request);
|
|
575
666
|
}
|
|
576
667
|
}
|
|
577
668
|
}
|
|
@@ -599,21 +690,76 @@ class AntiEtcd extends EventEmitter
|
|
|
599
690
|
}
|
|
600
691
|
}
|
|
601
692
|
|
|
693
|
+
async _lock_and_create_watch(client, create_request)
|
|
694
|
+
{
|
|
695
|
+
// watch is also a read, so it also has to follow read lock rules
|
|
696
|
+
try
|
|
697
|
+
{
|
|
698
|
+
const max_ts = Date.now() + this.cfg.lock_wait_timeout;
|
|
699
|
+
let lock_revision = 0;
|
|
700
|
+
while ((lock_revision = this.locker.check_locks(create_request.key, create_request.range_end)))
|
|
701
|
+
{
|
|
702
|
+
await this.locker.wait_commit(lock_revision, max_ts);
|
|
703
|
+
}
|
|
704
|
+
this._create_client_watch(client, create_request);
|
|
705
|
+
}
|
|
706
|
+
catch (e)
|
|
707
|
+
{
|
|
708
|
+
if (!(e instanceof RequestError))
|
|
709
|
+
{
|
|
710
|
+
console.error(e);
|
|
711
|
+
}
|
|
712
|
+
client.send_cb({ result: {
|
|
713
|
+
header: { revision: this.etctree.mod_revision },
|
|
714
|
+
watch_id: create_request.watch_id,
|
|
715
|
+
canceled: true,
|
|
716
|
+
cancel_reason: e.message,
|
|
717
|
+
} });
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
_create_client_watch(client, create_request)
|
|
722
|
+
{
|
|
723
|
+
const watch = this.etctree.api_create_watch(
|
|
724
|
+
{ ...create_request, watch_id: null }, client.send_cb, (this.cfg.merge_watches ? 'C'+client.id : null)
|
|
725
|
+
);
|
|
726
|
+
if (!watch.created)
|
|
727
|
+
{
|
|
728
|
+
client.send_cb({ result: { header: { revision: this.etctree.mod_revision }, watch_id: create_request.watch_id, ...watch } });
|
|
729
|
+
}
|
|
730
|
+
else
|
|
731
|
+
{
|
|
732
|
+
create_request.watch_id = create_request.watch_id || watch.watch_id;
|
|
733
|
+
client.watches[create_request.watch_id] = watch.watch_id;
|
|
734
|
+
client.send_cb({ result: {
|
|
735
|
+
header: { revision: this.etctree.mod_revision },
|
|
736
|
+
watch_id: create_request.watch_id,
|
|
737
|
+
created: true,
|
|
738
|
+
events: watch.events,
|
|
739
|
+
}});
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
602
743
|
_encodeWatch(create_request)
|
|
603
744
|
{
|
|
604
|
-
const req = { ...create_request, watch_id: null };
|
|
605
745
|
if (this.cfg.use_base64)
|
|
606
746
|
{
|
|
747
|
+
const req = { ...create_request };
|
|
607
748
|
if (req.key != null)
|
|
608
749
|
req.key = de64(req.key);
|
|
609
750
|
if (req.range_end != null)
|
|
610
751
|
req.range_end = de64(req.range_end);
|
|
752
|
+
return req;
|
|
611
753
|
}
|
|
612
|
-
return
|
|
754
|
+
return create_request;
|
|
613
755
|
}
|
|
614
756
|
|
|
615
|
-
_encodeMsg(msg)
|
|
757
|
+
_encodeMsg(msg, client)
|
|
616
758
|
{
|
|
759
|
+
if (this.cfg.logs.watch && client)
|
|
760
|
+
{
|
|
761
|
+
console.log(new Date().toISOString()+' '+client.addr+' '+(client.raft_node_id || '-')+' <- '+JSON.stringify(msg));
|
|
762
|
+
}
|
|
617
763
|
if (this.cfg.use_base64 && msg.result && msg.result.events)
|
|
618
764
|
{
|
|
619
765
|
return { ...msg, result: { ...msg.result, events: msg.result.events.map(ev => ({
|
package/antilocker.js
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
// "Row" (key)-level locks for antietcd
|
|
2
|
+
// (c) Vitaliy Filippov, 2024+
|
|
3
|
+
// License: Mozilla Public License 2.0 or Vitastor Network Public License 1.1
|
|
4
|
+
|
|
5
|
+
const { RequestError } = require('./common.js');
|
|
6
|
+
|
|
7
|
+
// How does it work:
|
|
8
|
+
// - Leader sets key-level locks, persists and replicates the change, then removes locks
|
|
9
|
+
// - Followers set locks, persist and reply to the leader, wait for the feedback, then remove locks
|
|
10
|
+
// - This is done from etctree by calling regular 'replicate watcher' method so etctree is agnostic of it
|
|
11
|
+
// - Both leader and followers check for locks with check_txn_locks() and then wait for them
|
|
12
|
+
// using wait_commit() before executing the transaction, and only proceed when check_txn_locks() returns 0
|
|
13
|
+
// - A node calls break_locks() if their Raft state changes
|
|
14
|
+
class AntiLocker
|
|
15
|
+
{
|
|
16
|
+
constructor(cfg)
|
|
17
|
+
{
|
|
18
|
+
cfg = cfg || {};
|
|
19
|
+
this.cfg = cfg;
|
|
20
|
+
this.cfg.lock_wait_interval = Number(this.cfg.lock_wait_interval);
|
|
21
|
+
if (!this.cfg.lock_wait_interval || this.cfg.lock_wait_interval < 50)
|
|
22
|
+
this.cfg.lock_wait_interval = 3000;
|
|
23
|
+
this.locks = [];
|
|
24
|
+
this.on_unlock = [];
|
|
25
|
+
this.txn_locks = {};
|
|
26
|
+
this.lock_timeout_timer = this.cfg.use_locks ? setInterval(() => this._timeout_locks(), this.cfg.lock_wait_interval) : null;
|
|
27
|
+
this.resume_notifications = null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
destroy()
|
|
31
|
+
{
|
|
32
|
+
if (this.lock_timeout_timer)
|
|
33
|
+
{
|
|
34
|
+
clearInterval(this.lock_timeout_timer);
|
|
35
|
+
this.lock_timeout_timer = null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Put (txn) => etctree.resume_notifications(txn) here
|
|
40
|
+
set_resume_notifications(resume_cb)
|
|
41
|
+
{
|
|
42
|
+
this.resume_notifications = resume_cb;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
_find_lock(key)
|
|
46
|
+
{
|
|
47
|
+
let min = 0, max = this.locks.length;
|
|
48
|
+
while (max-min > 1)
|
|
49
|
+
{
|
|
50
|
+
let mid = Math.floor((min+max)/2);
|
|
51
|
+
if (this.locks[mid].key > key)
|
|
52
|
+
max = mid;
|
|
53
|
+
else
|
|
54
|
+
min = mid;
|
|
55
|
+
}
|
|
56
|
+
return min;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
check_locks(key, range_end)
|
|
60
|
+
{
|
|
61
|
+
const pos = this._find_lock(key);
|
|
62
|
+
if (pos < this.locks.length && this.locks[pos].key <= (key || range_end))
|
|
63
|
+
{
|
|
64
|
+
return this.locks[pos].revision;
|
|
65
|
+
}
|
|
66
|
+
return 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
check_txn_locks(txn)
|
|
70
|
+
{
|
|
71
|
+
let lock_txn;
|
|
72
|
+
if (txn.compare)
|
|
73
|
+
{
|
|
74
|
+
for (const req of txn.compare)
|
|
75
|
+
{
|
|
76
|
+
lock_txn = this.check_locks(req.key);
|
|
77
|
+
if (lock_txn)
|
|
78
|
+
return lock_txn;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
for (const actions of [ 'success', 'failure' ])
|
|
82
|
+
{
|
|
83
|
+
if (!txn[actions])
|
|
84
|
+
continue;
|
|
85
|
+
let r, lock_txn;
|
|
86
|
+
for (const req of txn[actions])
|
|
87
|
+
{
|
|
88
|
+
if ((r = (req.request_range || req.requestRange)))
|
|
89
|
+
{
|
|
90
|
+
lock_txn = this.check_locks(r.key, r.range_end);
|
|
91
|
+
if (lock_txn)
|
|
92
|
+
return lock_txn;
|
|
93
|
+
}
|
|
94
|
+
else if ((r = (req.request_put || req.requestPut)))
|
|
95
|
+
{
|
|
96
|
+
lock_txn = this.check_locks(r.key);
|
|
97
|
+
if (lock_txn)
|
|
98
|
+
return lock_txn;
|
|
99
|
+
}
|
|
100
|
+
else if ((r = (req.request_delete_range || req.requestDeleteRange)))
|
|
101
|
+
{
|
|
102
|
+
lock_txn = this.check_locks(r.key, r.range_end);
|
|
103
|
+
if (lock_txn)
|
|
104
|
+
return lock_txn;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return 0;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
set_txn_locks(notifications, mod_revision)
|
|
112
|
+
{
|
|
113
|
+
if (this.txn_locks[mod_revision])
|
|
114
|
+
{
|
|
115
|
+
throw new Error('BUG: locks already set for revision '+mod_revision);
|
|
116
|
+
}
|
|
117
|
+
const res = [];
|
|
118
|
+
for (const event of notifications)
|
|
119
|
+
{
|
|
120
|
+
const pos = this._find_lock(event.key);
|
|
121
|
+
const lock = { key: event.key, revision: mod_revision };
|
|
122
|
+
res.push(lock);
|
|
123
|
+
this.locks.splice(pos, 0, lock);
|
|
124
|
+
}
|
|
125
|
+
this.txn_locks[mod_revision] = res;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
wait_commit(mod_revision, expire_ts)
|
|
129
|
+
{
|
|
130
|
+
// Postpone transaction
|
|
131
|
+
return new Promise((ok, fail) => this._set_on_unlock(mod_revision, expire_ts, ok, fail));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
break_locks()
|
|
135
|
+
{
|
|
136
|
+
this.txn_locks = {};
|
|
137
|
+
this.locks = [];
|
|
138
|
+
const waits = this.on_unlock;
|
|
139
|
+
this.on_unlock = [];
|
|
140
|
+
for (const wait of waits)
|
|
141
|
+
{
|
|
142
|
+
const cb = wait.fail;
|
|
143
|
+
cb(new RequestError(408, 'Lock wait aborted, please retry request'));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
_set_on_unlock(lock_rev, expire_ts, ok, fail)
|
|
148
|
+
{
|
|
149
|
+
let min = 0, max = this.on_unlock.length;
|
|
150
|
+
while (max-min > 1)
|
|
151
|
+
{
|
|
152
|
+
let mid = Math.floor((min+max)/2);
|
|
153
|
+
if (this.on_unlock[mid].revision > lock_rev)
|
|
154
|
+
max = mid;
|
|
155
|
+
else
|
|
156
|
+
min = mid;
|
|
157
|
+
}
|
|
158
|
+
this.on_unlock.splice(min, 0, { revision: lock_rev, expire_ts, ok, fail });
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
_timeout_locks()
|
|
162
|
+
{
|
|
163
|
+
const now = Date.now();
|
|
164
|
+
const cancel = [];
|
|
165
|
+
for (let i = 0; i < this.on_unlock.length; i++)
|
|
166
|
+
{
|
|
167
|
+
if (now > this.on_unlock[i].expire_ts)
|
|
168
|
+
{
|
|
169
|
+
cancel.push(this.on_unlock[i]);
|
|
170
|
+
this.on_unlock.splice(i, 1);
|
|
171
|
+
i--;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
for (const wait of cancel)
|
|
175
|
+
{
|
|
176
|
+
const cb = wait.fail;
|
|
177
|
+
cb(new RequestError(408, 'Lock wait timeout, please retry request'));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
unlock_txn(lock_rev)
|
|
182
|
+
{
|
|
183
|
+
if (!this.txn_locks[lock_rev])
|
|
184
|
+
{
|
|
185
|
+
throw new Error('BUG: revision '+lock_rev+' not locked');
|
|
186
|
+
}
|
|
187
|
+
const txn_set = new Set();
|
|
188
|
+
for (const lock of this.txn_locks[lock_rev])
|
|
189
|
+
{
|
|
190
|
+
txn_set.add(lock);
|
|
191
|
+
}
|
|
192
|
+
delete this.txn_locks[lock_rev];
|
|
193
|
+
this.locks = this.locks.filter(l => !txn_set.has(l));
|
|
194
|
+
let i = 0;
|
|
195
|
+
while (i < this.on_unlock.length && this.on_unlock[i].revision <= lock_rev)
|
|
196
|
+
{
|
|
197
|
+
i++;
|
|
198
|
+
}
|
|
199
|
+
const waits = this.on_unlock.splice(0, i);
|
|
200
|
+
for (const wait of waits)
|
|
201
|
+
{
|
|
202
|
+
const cb = wait.ok;
|
|
203
|
+
cb();
|
|
204
|
+
}
|
|
205
|
+
if (this.resume_notifications)
|
|
206
|
+
{
|
|
207
|
+
this.resume_notifications(lock_rev);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
module.exports = AntiLocker;
|