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/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.4';
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(true);
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.log_level > 1)
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
- socket.on('error', e => console.error(e.syscall === 'connect' ? e.message : e));
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
- }, this.cfg.ws_keepalive_interval||30000);
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
- socket.on('close', () =>
328
+ const on_close = () =>
276
329
  {
277
- this._unsubscribeClient(client_id);
278
- clearInterval(pinger);
279
- delete this.clients[client_id];
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
- const watch = this.etctree.api_create_watch(this._encodeWatch(params), (msg) => callback(this._encodeMsg(msg)), stream_id);
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
- for (const item of data.compare||[])
521
+ decoded = {};
522
+ if (data.compare)
446
523
  {
447
- if (item.key != null)
448
- item.key = de64(item.key);
524
+ decoded.compare = [];
525
+ for (const item of data.compare)
526
+ decoded.compare.push({ ...item, key: de64(item.key) });
449
527
  }
450
- for (const items of [ data.success, data.failure ])
528
+ for (const key of [ 'success', 'failure' ])
451
529
  {
452
- for (const item of items||[])
530
+ if (!data[key])
531
+ continue;
532
+ const actions = [];
533
+ for (const item of data[key])
453
534
  {
454
- const req = item.request_range || item.requestRange ||
455
- item.request_put || item.requestPut ||
456
- item.request_delete_range || item.requestDeleteRange;
457
- if (req.key != null)
458
- req.key = de64(req.key);
459
- if (req.range_end != null)
460
- req.range_end = de64(req.range_end);
461
- if (req.value != null)
462
- req.value = de64(req.value);
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
- const result = await this.etctree.api_txn(data);
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.access_log)
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
- const watch = this.etctree.api_create_watch(
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
- socket.send(JSON.stringify({ result: { header: { revision: this.etctree.mod_revision }, watch_id: create_request.watch_id, ...watch } }));
661
+ this._lock_and_create_watch(client, create_request);
569
662
  }
570
663
  else
571
664
  {
572
- create_request.watch_id = create_request.watch_id || watch.watch_id;
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 req;
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;