antietcd 1.1.4 → 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/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
- this._notify(notifications);
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: this.mod_revision }, leases: [ { id, ttl: req.TTL, expires } ] });
381
+ await this.replicate({ header: { revision: next_revision }, leases: [ { id, ttl: req.TTL, expires } ] });
370
382
  }
371
- return { header: { revision: this.mod_revision }, ID: id, TTL: req.TTL };
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: this.mod_revision }, leases: [ { id, ttl, expires: lease.expires } ] });
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: this.mod_revision }, ID: id, TTL: ''+ttl } };
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, this.mod_revision);
454
- if (this.replicate)
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
- if (this.replicate)
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 && this.compact_revision && this.compact_revision > 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
- if (req.start_revision && req.start_revision <= this.mod_revision)
563
+ let events = undefined;
564
+ if (req.start_revision)
571
565
  {
572
566
  // Send initial changes
573
- const imm = setImmediate(() =>
574
- {
575
- this.active_immediate = this.active_immediate.filter(i => i !== imm);
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
- _notify(notifications)
637
+ async _notify_and_replicate(next_revision, notifications, leases)
642
638
  {
643
- if (!notifications.length)
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
- const next_revision = this.mod_revision + 1;
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
- if (this.replicate && notifications.length)
680
- {
681
- // First replicate the change and then notify watchers about it
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.1.4",
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
- "antietcd.js",
24
+ "antilocker.js",
25
25
  "antipersistence.js",
26
26
  "common.js",
27
27
  "etctree.js",