antietcd 1.1.3 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -3
- package/anticli.js +108 -20
- 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 +5 -5
package/etctree.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Simple in-memory etcd-like key/value store
|
|
2
|
-
// (c) Vitaliy Filippov, 2024
|
|
2
|
+
// (c) Vitaliy Filippov, 2024+
|
|
3
3
|
// License: Mozilla Public License 2.0 or Vitastor Network Public License 1.1
|
|
4
4
|
|
|
5
5
|
const crypto = require('crypto');
|
|
@@ -19,8 +19,9 @@ const { RequestError } = require('./common.js');
|
|
|
19
19
|
|
|
20
20
|
class EtcTree
|
|
21
21
|
{
|
|
22
|
-
constructor()
|
|
22
|
+
constructor(cfg)
|
|
23
23
|
{
|
|
24
|
+
this.cfg = cfg || {};
|
|
24
25
|
this.state = {};
|
|
25
26
|
this.leases = {};
|
|
26
27
|
this.watchers = {};
|
|
@@ -30,6 +31,7 @@ class EtcTree
|
|
|
30
31
|
this.replicate = null;
|
|
31
32
|
this.paused = false;
|
|
32
33
|
this.active_immediate = [];
|
|
34
|
+
this.notify_queue = [];
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
destroy()
|
|
@@ -97,13 +99,13 @@ class EtcTree
|
|
|
97
99
|
|
|
98
100
|
_key_parts(key)
|
|
99
101
|
{
|
|
100
|
-
const parts = key.replace(/\/\/+/g, '/').replace(/\/$/g, ''); // trim beginning?
|
|
102
|
+
const parts = (''+key).replace(/\/\/+/g, '/').replace(/\/$/g, ''); // trim beginning?
|
|
101
103
|
return parts === '' ? [] : parts.split('/');
|
|
102
104
|
}
|
|
103
105
|
|
|
104
106
|
_get_range(req)
|
|
105
107
|
{
|
|
106
|
-
const key = req.key;
|
|
108
|
+
const key = ''+req.key;
|
|
107
109
|
const end = req.range_end;
|
|
108
110
|
if (end != null && (key !== '' && end !== '') && (key[key.length-1] != '/' || end[end.length-1] != '0' ||
|
|
109
111
|
end.substr(0, end.length-1) !== key.substr(0, key.length-1)))
|
|
@@ -237,7 +239,17 @@ class EtcTree
|
|
|
237
239
|
}
|
|
238
240
|
// Then find and apply the difference in data
|
|
239
241
|
this._restore_diff(update_only, this.state, snapshot.state, null, this.state.watchers || [], notifications);
|
|
240
|
-
|
|
242
|
+
if (notifications.length)
|
|
243
|
+
{
|
|
244
|
+
if (!this.cfg.use_locks)
|
|
245
|
+
{
|
|
246
|
+
this._notify(notifications);
|
|
247
|
+
}
|
|
248
|
+
else
|
|
249
|
+
{
|
|
250
|
+
this.notify_queue.push({ revision: this.mod_revision, notifications });
|
|
251
|
+
}
|
|
252
|
+
}
|
|
241
253
|
}
|
|
242
254
|
|
|
243
255
|
_restore_diff(update_only, cur_old, cur_new, prefix, watchers, notifications)
|
|
@@ -362,13 +374,13 @@ class EtcTree
|
|
|
362
374
|
}
|
|
363
375
|
const expires = Date.now() + req.TTL*1000;
|
|
364
376
|
this.leases[id] = { ttl: req.TTL, expires, timer_id: null, keys: {} };
|
|
365
|
-
this.mod_revision
|
|
377
|
+
const next_revision = ++this.mod_revision;
|
|
366
378
|
this._set_expire(id);
|
|
367
379
|
if (this.replicate)
|
|
368
380
|
{
|
|
369
|
-
await this.replicate({ header: { revision:
|
|
381
|
+
await this.replicate({ header: { revision: next_revision }, leases: [ { id, ttl: req.TTL, expires } ] });
|
|
370
382
|
}
|
|
371
|
-
return { header: { revision:
|
|
383
|
+
return { header: { revision: next_revision }, ID: id, TTL: req.TTL };
|
|
372
384
|
}
|
|
373
385
|
|
|
374
386
|
async api_keepalive_lease(req)
|
|
@@ -386,14 +398,14 @@ class EtcTree
|
|
|
386
398
|
}
|
|
387
399
|
const ttl = this.leases[id].ttl;
|
|
388
400
|
lease.expires = Date.now() + ttl*1000;
|
|
389
|
-
this.mod_revision
|
|
401
|
+
const next_revision = ++this.mod_revision;
|
|
390
402
|
this._set_expire(id);
|
|
391
403
|
if (this.replicate)
|
|
392
404
|
{
|
|
393
|
-
await this.replicate({ header: { revision:
|
|
405
|
+
await this.replicate({ header: { revision: next_revision }, leases: [ { id, ttl, expires: lease.expires } ] });
|
|
394
406
|
}
|
|
395
407
|
// extra wrapping in { result: ... }
|
|
396
|
-
return { result: { header: { revision:
|
|
408
|
+
return { result: { header: { revision: next_revision }, ID: id, TTL: ''+ttl } };
|
|
397
409
|
}
|
|
398
410
|
|
|
399
411
|
load_lease(lease)
|
|
@@ -449,26 +461,10 @@ class EtcTree
|
|
|
449
461
|
return null;
|
|
450
462
|
throw new RequestError(400, 'unknown lease');
|
|
451
463
|
}
|
|
452
|
-
this.mod_revision
|
|
453
|
-
this._sync_revoke_lease(req.ID, notifications,
|
|
454
|
-
|
|
455
|
-
{
|
|
456
|
-
await this.notify_replicator(notifications, [ { id: req.ID } ]);
|
|
457
|
-
}
|
|
458
|
-
this._notify(notifications);
|
|
459
|
-
return { header: { revision: this.mod_revision } };
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
async notify_replicator(notifications, leases)
|
|
463
|
-
{
|
|
464
|
-
// First replicate the change and then notify watchers about it
|
|
465
|
-
const all_changes = {};
|
|
466
|
-
for (const chg of notifications)
|
|
467
|
-
{
|
|
468
|
-
all_changes[chg.key] = { ...chg };
|
|
469
|
-
delete all_changes[chg.key].watchers;
|
|
470
|
-
}
|
|
471
|
-
await this.replicate({ header: { revision: this.mod_revision }, events: Object.values(all_changes), leases });
|
|
464
|
+
const next_revision = ++this.mod_revision;
|
|
465
|
+
this._sync_revoke_lease(req.ID, notifications, next_revision);
|
|
466
|
+
await this._notify_and_replicate(next_revision, notifications, [ { id: req.ID } ]);
|
|
467
|
+
return { header: { revision: next_revision } };
|
|
472
468
|
}
|
|
473
469
|
|
|
474
470
|
async apply_replication(msg)
|
|
@@ -503,11 +499,7 @@ class EtcTree
|
|
|
503
499
|
}
|
|
504
500
|
}
|
|
505
501
|
}
|
|
506
|
-
|
|
507
|
-
{
|
|
508
|
-
await this.notify_replicator(notifications, msg.leases);
|
|
509
|
-
}
|
|
510
|
-
this._notify(notifications);
|
|
502
|
+
await this._notify_and_replicate(msg.header.revision, notifications, msg.leases);
|
|
511
503
|
}
|
|
512
504
|
|
|
513
505
|
// forget deletions before compact_revision
|
|
@@ -533,7 +525,8 @@ class EtcTree
|
|
|
533
525
|
api_create_watch(req, send, stream_id)
|
|
534
526
|
{
|
|
535
527
|
const { parts, all } = this._get_range(req);
|
|
536
|
-
if (req.start_revision &&
|
|
528
|
+
if (req.start_revision && req.start_revision > 1 &&
|
|
529
|
+
this.compact_revision && this.compact_revision > req.start_revision)
|
|
537
530
|
{
|
|
538
531
|
// Deletions up to this.compact_revision are forgotten
|
|
539
532
|
return { canceled: true, cancel_reason: 'Revisions up to '+this.compact_revision+' are compacted', compact_revision: this.compact_revision };
|
|
@@ -567,23 +560,18 @@ class EtcTree
|
|
|
567
560
|
cur.key_watchers = cur.key_watchers || [];
|
|
568
561
|
cur.key_watchers.push(watch_id);
|
|
569
562
|
}
|
|
570
|
-
|
|
563
|
+
let events = undefined;
|
|
564
|
+
if (req.start_revision)
|
|
571
565
|
{
|
|
572
566
|
// Send initial changes
|
|
573
|
-
|
|
574
|
-
{
|
|
575
|
-
|
|
576
|
-
const events = [];
|
|
577
|
-
const { cur } = this._get_subtree([], false, false);
|
|
578
|
-
this._get_modified(events, cur, null, req.start_revision);
|
|
579
|
-
send({ result: { header: { revision: this.mod_revision }, events } });
|
|
580
|
-
});
|
|
581
|
-
this.active_immediate.push(imm);
|
|
567
|
+
events = [];
|
|
568
|
+
const { cur } = this._get_subtree([], false, false);
|
|
569
|
+
this._get_modified(events, cur, null, req.start_revision);
|
|
582
570
|
}
|
|
583
|
-
return { header: { revision: this.mod_revision }, watch_id, created: true };
|
|
571
|
+
return { header: { revision: this.mod_revision }, watch_id, created: true, events };
|
|
584
572
|
}
|
|
585
573
|
|
|
586
|
-
_get_modified(events, cur, prefix, min_rev)
|
|
574
|
+
_get_modified(events, cur, prefix, min_rev, with_watchers)
|
|
587
575
|
{
|
|
588
576
|
if (cur.mod_revision >= min_rev)
|
|
589
577
|
{
|
|
@@ -595,6 +583,10 @@ class EtcTree
|
|
|
595
583
|
mod_revision: cur.mod_revision,
|
|
596
584
|
},
|
|
597
585
|
};
|
|
586
|
+
if (with_watchers)
|
|
587
|
+
{
|
|
588
|
+
ev.watchers = [ ...with_watchers, ...(cur.watchers||[]), ...(cur.key_watchers||[]) ];
|
|
589
|
+
}
|
|
598
590
|
if (cur.lease)
|
|
599
591
|
{
|
|
600
592
|
ev.kv.lease = cur.lease;
|
|
@@ -603,9 +595,13 @@ class EtcTree
|
|
|
603
595
|
}
|
|
604
596
|
if (cur.children)
|
|
605
597
|
{
|
|
598
|
+
if (with_watchers)
|
|
599
|
+
{
|
|
600
|
+
with_watchers = [ ...with_watchers, ...(cur.watchers||[]) ];
|
|
601
|
+
}
|
|
606
602
|
for (const k in cur.children)
|
|
607
603
|
{
|
|
608
|
-
this._get_modified(events, cur.children[k], prefix === null ? k : prefix+'/'+k, min_rev);
|
|
604
|
+
this._get_modified(events, cur.children[k], prefix === null ? k : prefix+'/'+k, min_rev, with_watchers);
|
|
609
605
|
}
|
|
610
606
|
}
|
|
611
607
|
}
|
|
@@ -638,12 +634,63 @@ class EtcTree
|
|
|
638
634
|
return { canceled: true };
|
|
639
635
|
}
|
|
640
636
|
|
|
641
|
-
|
|
637
|
+
async _notify_and_replicate(next_revision, notifications, leases)
|
|
642
638
|
{
|
|
643
|
-
if (
|
|
639
|
+
if (notifications.length)
|
|
640
|
+
{
|
|
641
|
+
// First schedule notifications, then replicate. Replication may fail.
|
|
642
|
+
// And we don't want to miss events in that case.
|
|
643
|
+
if (!this.cfg.use_locks)
|
|
644
|
+
{
|
|
645
|
+
this._notify(notifications);
|
|
646
|
+
}
|
|
647
|
+
else
|
|
648
|
+
{
|
|
649
|
+
this.notify_queue.push({ revision: next_revision, notifications });
|
|
650
|
+
}
|
|
651
|
+
if (this.replicate)
|
|
652
|
+
{
|
|
653
|
+
const all_changes = {};
|
|
654
|
+
for (const chg of notifications)
|
|
655
|
+
{
|
|
656
|
+
all_changes[chg.key] = { ...chg };
|
|
657
|
+
delete all_changes[chg.key].watchers;
|
|
658
|
+
}
|
|
659
|
+
await this.replicate({ header: { revision: next_revision }, events: Object.values(all_changes), leases });
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
resume_notifications(max_revision)
|
|
665
|
+
{
|
|
666
|
+
let i = 0;
|
|
667
|
+
while (i < this.notify_queue.length &&
|
|
668
|
+
this.notify_queue[i].revision <= max_revision)
|
|
669
|
+
{
|
|
670
|
+
this._notify(this.notify_queue[i].notifications);
|
|
671
|
+
i++;
|
|
672
|
+
}
|
|
673
|
+
this.notify_queue.splice(0, i);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
resync_notifications()
|
|
677
|
+
{
|
|
678
|
+
if (!this.notify_queue.length)
|
|
644
679
|
{
|
|
645
680
|
return;
|
|
646
681
|
}
|
|
682
|
+
const first_unnotified_revision = this.notify_queue
|
|
683
|
+
.flatMap(msg => msg.notifications.map(m => m.revision))
|
|
684
|
+
.reduce((a, c) => (a < c ? a : c));
|
|
685
|
+
const events = [];
|
|
686
|
+
const { cur } = this._get_subtree([], false, false);
|
|
687
|
+
this._get_modified(events, cur, null, first_unnotified_revision, []);
|
|
688
|
+
const notifications = events.map(n => ({ ...n.kv, watchers: n.watchers }));
|
|
689
|
+
this._notify(notifications);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
_notify(notifications)
|
|
693
|
+
{
|
|
647
694
|
const by_watcher = {};
|
|
648
695
|
for (const notif of notifications)
|
|
649
696
|
{
|
|
@@ -671,18 +718,14 @@ class EtcTree
|
|
|
671
718
|
const failed = (compare || []).filter(chk => !this._check(chk)).length > 0;
|
|
672
719
|
const responses = [];
|
|
673
720
|
const notifications = [];
|
|
674
|
-
|
|
721
|
+
let next_revision = this.mod_revision + 1;
|
|
675
722
|
for (const req of (failed ? failure : success) || [])
|
|
676
723
|
{
|
|
677
724
|
responses.push(this._txn_action(req, next_revision, notifications));
|
|
678
725
|
}
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
await this.notify_replicator(notifications);
|
|
683
|
-
}
|
|
684
|
-
this._notify(notifications);
|
|
685
|
-
return { header: { revision: this.mod_revision }, succeeded: !failed, responses };
|
|
726
|
+
next_revision = this.mod_revision;
|
|
727
|
+
await this._notify_and_replicate(next_revision, notifications);
|
|
728
|
+
return { header: { revision: next_revision }, succeeded: !failed, responses };
|
|
686
729
|
}
|
|
687
730
|
|
|
688
731
|
_txn_action(req, cur_revision, notifications)
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "antietcd",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Simplistic etcd replacement based on TinyRaft",
|
|
5
5
|
"main": "antietcd.js",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"lint": "eslint common.js anticli.js antipersistence.js anticluster.js antietcd.js etctree.js etctree.spec.js",
|
|
7
|
+
"lint": "eslint common.js anticli.js antipersistence.js anticluster.js antietcd.js antilocker.js etctree.js etctree.spec.js",
|
|
8
8
|
"test": "node etctree.spec.js"
|
|
9
9
|
},
|
|
10
10
|
"repository": {
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"antietcd-app.js",
|
|
22
22
|
"antietcd.js",
|
|
23
23
|
"antietcd.d.ts",
|
|
24
|
-
"
|
|
24
|
+
"antilocker.js",
|
|
25
25
|
"antipersistence.js",
|
|
26
26
|
"common.js",
|
|
27
27
|
"etctree.js",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"ws": "^8.17.0"
|
|
50
50
|
},
|
|
51
51
|
"bin": {
|
|
52
|
-
"antietcd": "
|
|
53
|
-
"anticli": "
|
|
52
|
+
"antietcd": "antietcd-app.js",
|
|
53
|
+
"anticli": "anticli.js"
|
|
54
54
|
}
|
|
55
55
|
}
|