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 CHANGED
@@ -5,6 +5,8 @@ Simplistic miniature etcd replacement based on [TinyRaft](https://git.yourcmc.ru
5
5
  - Embeddable
6
6
  - REST API only, gRPC is shit and will never be supported
7
7
  - [TinyRaft](https://git.yourcmc.ru/vitalif/tinyraft/)-based leader election
8
+ - [Strict serializable](https://jepsen.io/consistency/models/strict-serializable) transaction isolation,
9
+ confirmed by [Jepsen tests](https://git.yourcmc.ru/vitalif/antietcd/src/branch/master/jepsen)
8
10
  - Websocket-based cluster communication
9
11
  - Supports a limited subset of etcd REST APIs
10
12
  - With optional persistence
@@ -52,6 +54,7 @@ node_modules/.bin/anticli [OPTIONS] put <key> [<value>]
52
54
  node_modules/.bin/anticli [OPTIONS] get <key> [-p|--prefix] [-v|--print-value-only] [-k|--keys-only] [--no-temp]
53
55
  node_modules/.bin/anticli [OPTIONS] del <key> [-p|--prefix]
54
56
  node_modules/.bin/anticli [OPTIONS] load [--with-lease] < dump.json
57
+ node_modules/.bin/anticli [OPTIONS] watch <key> [key...] [-p|--prefix]
55
58
  ```
56
59
 
57
60
  For `put`, if `<value>` is not specified, it will be read from STDIN.
@@ -69,9 +72,15 @@ Options:
69
72
  <dt>--key &lt;key&gt;</dt>
70
73
  <dd>Use TLS with this key file (PEM format)</dd>
71
74
 
75
+ <dt>--ca &lt;cert&gt;</dt>
76
+ <dd>Use this TLS CA certificate to verify server certificate</dd>
77
+
72
78
  <dt>--timeout 1000</dt>
73
79
  <dd>Specify request timeout in milliseconds</dd>
74
80
 
81
+ <dt>--base64 1</dt>
82
+ <dd>Use base64 encoding of keys and values, like in etcd (enabled by default)</dd>
83
+
75
84
  <dt>--json or --write-out=json</dt>
76
85
  <dd>Print raw response in JSON</dd>
77
86
 
@@ -110,6 +119,22 @@ Specify &lt;ca&gt; = &lt;cert&gt; if your certificate is self-signed.</dd>
110
119
 
111
120
  </dl>
112
121
 
122
+ ### Consistency
123
+
124
+ <dl>
125
+
126
+ <dt>--use_locks 1</dt>
127
+ <dd>Enable lock-based transaction isolation to prevent <a href="https://jepsen.io/consistency/phenomena/g1a">G1a</a>.</dd>
128
+
129
+ <dt>--merge_watches 1</dt>
130
+ <dd>Antietcd merges all watcher events for a single transaction into a single websocket message to provide
131
+ more ordering/transaction guarantees. Set to 0 to disable this behaviour.</dd>
132
+
133
+ <dt>--stale_read 1</dt>
134
+ <dd>Allow to serve reads from replicas (potentially stale). Specify 0 to always contact leader on every read.</dd>
135
+
136
+ </dl>
137
+
113
138
  ### Persistence
114
139
 
115
140
  <dl>
@@ -152,12 +177,12 @@ for every change and returning a new value or undefined to skip persistence.</dd
152
177
  <dt>--wait_quorum_timeout 30000</dt>
153
178
  <dd>Timeout for requests to wait for quorum to come up</dd>
154
179
 
180
+ <dt>--leadership_timeout 3000</dt>
181
+ <dd>TinyRaft leadership timeout</dd>
182
+
155
183
  <dt>--leader_priority &lt;number&gt;</dt>
156
184
  <dd>Raft leader priority for this node (optional)</dd>
157
185
 
158
- <dt>--stale_read 1</dt>
159
- <dd>Allow to serve reads from followers. Specify 0 to disallow</dd>
160
-
161
186
  <dt>--reconnect_interval 1000</dt>
162
187
  <dd>Unavailable peer connection retry interval</dd>
163
188
 
@@ -178,6 +203,21 @@ for every change and returning a new value or undefined to skip persistence.</dd
178
203
 
179
204
  </dl>
180
205
 
206
+ ### Logging
207
+
208
+ <dl>
209
+
210
+ <dt>--logs keyword1,keyword2,...</dt>
211
+ <dd>
212
+ Enable logs by keywords:
213
+
214
+ - access: log all requests and incoming websocket messages.
215
+ - watch: log outgoing websocket watch messages.
216
+ -cluster: log clustering (TinyRaft) events.
217
+ </dd>
218
+
219
+ </dl>
220
+
181
221
  ## Embedded Usage
182
222
 
183
223
  ```js
@@ -264,6 +304,15 @@ function example_filter(cfg)
264
304
  module.exports = example_filter;
265
305
  ```
266
306
 
307
+ ## Consistency Guarantees
308
+
309
+ When `--stale_read` is enabled, Antietcd provides [SERIALIZABLE](https://jepsen.io/consistency/models/serializable) transaction isolation level.
310
+
311
+ When it's disabled, the level is [STRICT SERIALIZABLE](https://jepsen.io/consistency/models/strict-serializable).
312
+
313
+ Reconnected watchers don't receive all historical events individually, but they are guaranteed to receive
314
+ a consistent snapshot of keys in interest.
315
+
267
316
  ## Supported etcd APIs
268
317
 
269
318
  NOTE: `key`, `value` and `range_end` are always encoded in base64, like in original etcd.
@@ -527,6 +576,7 @@ type ServerMessage = {
527
576
  - 400 for invalid requests
528
577
  - 404 for unsupported API / URL not found
529
578
  - 405 for non-POST request method
579
+ - 408 for lock wait timeouts
530
580
  - 501 for unsupported API feature - non-directory range queries and so on
531
581
  - 502 for server is stopping
532
582
  - 503 for quorum-related errors - quorum not available and so on
package/anticli.js CHANGED
@@ -8,6 +8,7 @@ const fs = require('fs');
8
8
  const fsp = require('fs').promises;
9
9
  const http = require('http');
10
10
  const https = require('https');
11
+ const WebSocket = require('ws');
11
12
 
12
13
  const help_text = `CLI for AntiEtcd
13
14
  (c) Vitaliy Filippov, 2024
@@ -18,12 +19,13 @@ Usage:
18
19
  anticli.js [OPTIONS] put <key> [<value>]
19
20
  anticli.js [OPTIONS] get <key> [-p|--prefix] [-v|--print-value-only] [-k|--keys-only] [--no-temp]
20
21
  anticli.js [OPTIONS] del <key> [-p|--prefix]
22
+ anticli.js [OPTIONS] watch <key> [key...] [-p|--prefix]
21
23
  anticli.js [OPTIONS] load [--with-lease] < dump.json
22
24
 
23
25
  Options:
24
26
 
25
27
  [--endpoints|-e http://node1:2379,http://node2:2379,http://node3:2379]
26
- [--cert cert.pem] [--key key.pem] [--timeout 1000] [--json]
28
+ [--cert cert.pem] [--key key.pem] [--ca ca.pem] [--timeout 1000] [--base64 1] [--json]
27
29
  `;
28
30
 
29
31
  class AntiEtcdCli
@@ -83,9 +85,9 @@ class AntiEtcdCli
83
85
  cmd.push(arg);
84
86
  }
85
87
  }
86
- if (!cmd.length || cmd[0] != 'get' && cmd[0] != 'put' && cmd[0] != 'del' && cmd[0] != 'load')
88
+ if (!cmd.length || cmd[0] != 'get' && cmd[0] != 'put' && cmd[0] != 'del' && cmd[0] != 'load' && cmd[0] != 'watch')
87
89
  {
88
- process.stderr.write('Supported commands: get, put, del, load. Use --help to see details\n');
90
+ process.stderr.write('Supported commands: get, put, del, load, watch. Use --help to see details\n');
89
91
  process.exit(1);
90
92
  }
91
93
  return [ cmd, options ];
@@ -98,12 +100,16 @@ class AntiEtcdCli
98
100
  {
99
101
  this.options.endpoints = [ 'http://localhost:2379' ];
100
102
  }
103
+ this.options.base64 = (this.options.base64 == null) || is_true(this.options.base64);
104
+ this.tls = {};
101
105
  if (this.options.cert && this.options.key)
102
106
  {
103
- this.tls = {
104
- key: await fsp.readFile(this.options.key),
105
- cert: await fsp.readFile(this.options.cert),
106
- };
107
+ this.tls.key = await fsp.readFile(this.options.key);
108
+ this.tls.cert = await fsp.readFile(this.options.cert);
109
+ }
110
+ if (this.options.ca)
111
+ {
112
+ this.tls.ca = await fsp.readFile(this.options.ca);
107
113
  }
108
114
  if (cmd[0] == 'get')
109
115
  {
@@ -121,6 +127,10 @@ class AntiEtcdCli
121
127
  {
122
128
  await this.load();
123
129
  }
130
+ else if (cmd[0] == 'watch')
131
+ {
132
+ await this.watch(cmd.slice(1));
133
+ }
124
134
  // wait until output is fully flushed
125
135
  await new Promise(ok => process.stdout.write('', ok));
126
136
  await new Promise(ok => process.stderr.write('', ok));
@@ -168,7 +178,9 @@ class AntiEtcdCli
168
178
  {
169
179
  keys = keys.map(k => k.replace(/\/+$/, ''));
170
180
  }
171
- const txn = { success: keys.map(key => ({ request_range: this.options.prefix ? { key: b64(key+'/'), range_end: b64(key+'0') } : { key: b64(key) } })) };
181
+ const txn = { success: keys.map(key => ({ request_range: this.options.prefix
182
+ ? { key: this.b64(key+'/'), range_end: this.b64(key+'0') }
183
+ : { key: this.b64(key) } })) };
172
184
  const res = await this.request('/v3/kv/txn', txn);
173
185
  if (this.options.notemp)
174
186
  {
@@ -194,11 +206,11 @@ class AntiEtcdCli
194
206
  {
195
207
  if (!this.options.print_value_only)
196
208
  {
197
- process.stdout.write(de64(kv.key)+'\n');
209
+ process.stdout.write(this.de64(kv.key)+'\n');
198
210
  }
199
211
  if (!this.options.keys_only)
200
212
  {
201
- process.stdout.write(de64(kv.value)+'\n');
213
+ process.stdout.write(this.de64(kv.value)+'\n');
202
214
  }
203
215
  }
204
216
  }
@@ -211,7 +223,7 @@ class AntiEtcdCli
211
223
  {
212
224
  value = await new Promise((ok, no) => fs.readFile(0, { encoding: 'utf-8' }, (err, res) => err ? no(err) : ok(res)));
213
225
  }
214
- const res = await this.request('/v3/kv/put', { key: b64(key), value: b64(value) });
226
+ const res = await this.request('/v3/kv/put', { key: this.b64(key), value: this.b64(value) });
215
227
  if (res.header)
216
228
  {
217
229
  process.stdout.write('OK\n');
@@ -224,7 +236,9 @@ class AntiEtcdCli
224
236
  {
225
237
  keys = keys.map(k => k.replace(/\/+$/, ''));
226
238
  }
227
- const txn = { success: keys.map(key => ({ request_delete_range: this.options.prefix ? { key: b64(key+'/'), range_end: b64(key+'0') } : { key: b64(key) } })) };
239
+ const txn = { success: keys.map(key => ({ request_delete_range: this.options.prefix
240
+ ? { key: this.b64(key+'/'), range_end: this.b64(key+'0') }
241
+ : { key: this.b64(key) } })) };
228
242
  const res = await this.request('/v3/kv/txn', txn);
229
243
  for (const r of res.responses||[])
230
244
  {
@@ -235,6 +249,75 @@ class AntiEtcdCli
235
249
  }
236
250
  }
237
251
 
252
+ async watch(keys)
253
+ {
254
+ const prefix = this.options.prefix || !keys.length;
255
+ keys = keys.length ? keys : [''];
256
+ for (const url of this.options.endpoints)
257
+ {
258
+ const ws = new WebSocket(url.replace(/^http/, 'ws').replace(/\/+$/, '')+'/watch', this.tls||{});
259
+ let closed = false;
260
+ let started = false;
261
+ let close_cb = null;
262
+ const on_error = (e) =>
263
+ {
264
+ console.log(e);
265
+ closed = true;
266
+ if (close_cb)
267
+ close_cb();
268
+ };
269
+ ws.on('error', on_error);
270
+ ws.on('message', (msg) =>
271
+ {
272
+ try
273
+ {
274
+ msg = JSON.parse(msg);
275
+ }
276
+ catch (e)
277
+ {
278
+ console.error(JSON.stringify('Bad JSON received from websocket: '+msg));
279
+ return;
280
+ }
281
+ if (this.options.base64 && msg.result && msg.result.events)
282
+ {
283
+ for (const ev of msg.result.events)
284
+ {
285
+ if (ev.kv)
286
+ {
287
+ ev.kv.key = this.de64(ev.kv.key);
288
+ ev.kv.value = this.de64(ev.kv.value);
289
+ }
290
+ }
291
+ }
292
+ console.log(JSON.stringify(msg));
293
+ });
294
+ ws.on('open', () =>
295
+ {
296
+ started = true;
297
+ for (let i = 0; i < keys.length; i++)
298
+ {
299
+ const k = keys[i];
300
+ ws.send(JSON.stringify({
301
+ create_request: {
302
+ key: this.b64(prefix ? k+'/' : k),
303
+ range_end: prefix ? k+'0' : undefined,
304
+ watch_id: i+1,
305
+ progress_notify: true,
306
+ },
307
+ }));
308
+ }
309
+ });
310
+ if (closed)
311
+ continue;
312
+ else
313
+ {
314
+ await new Promise(ok => close_cb = ok);
315
+ if (!started)
316
+ return;
317
+ }
318
+ }
319
+ }
320
+
238
321
  async request(path, body)
239
322
  {
240
323
  for (const url of this.options.endpoints)
@@ -267,6 +350,16 @@ class AntiEtcdCli
267
350
  }
268
351
  process.exit(1);
269
352
  }
353
+
354
+ b64(str)
355
+ {
356
+ return this.options.base64 ? Buffer.from(str).toString('base64') : str;
357
+ }
358
+
359
+ de64(str)
360
+ {
361
+ return this.options.base64 ? Buffer.from(str, 'base64').toString() : str;
362
+ }
270
363
  }
271
364
 
272
365
  function POST(url, options, body, timeout)
@@ -281,7 +374,7 @@ function POST(url, options, body, timeout)
281
374
  req = null;
282
375
  ok({ error: 'timeout' });
283
376
  }, timeout) : null;
284
- let req = (url.substr(0, 6).toLowerCase() == 'https://' ? https : http).request(url, { method: 'POST', headers: {
377
+ let req = (url.substr(0, 8).toLowerCase() == 'https://' ? https : http).request(url, { method: 'POST', headers: {
285
378
  'Content-Type': 'application/json',
286
379
  'Content-Length': body_text.length,
287
380
  }, timeout, ...options }, (res) =>
@@ -320,14 +413,9 @@ function POST(url, options, body, timeout)
320
413
  });
321
414
  }
322
415
 
323
- function b64(str)
324
- {
325
- return Buffer.from(str).toString('base64');
326
- }
327
-
328
- function de64(str)
416
+ function is_true(s)
329
417
  {
330
- return Buffer.from(str, 'base64').toString();
418
+ return s === true || s === 1 || s === '1' || s === 'yes' || s === 'true' || s === 'on';
331
419
  }
332
420
 
333
421
  new AntiEtcdCli().run(...AntiEtcdCli.parse(process.argv)).catch(console.error);
package/anticluster.js CHANGED
@@ -9,10 +9,6 @@ const { runCallbacks, RequestError } = require('./common.js');
9
9
 
10
10
  const LEADER_MISMATCH = 'raft leader/term mismatch';
11
11
 
12
- const LEADER_ONLY = 1;
13
- const NO_WAIT_QUORUM = 2;
14
- const READ_FROM_FOLLOWER = 4;
15
-
16
12
  class AntiCluster
17
13
  {
18
14
  constructor(antietcd)
@@ -38,6 +34,7 @@ class AntiCluster
38
34
  nodeId: this.cfg.node_id,
39
35
  heartbeatTimeout: this.cfg.heartbeat_timeout,
40
36
  electionTimeout: this.cfg.election_timeout,
37
+ leadershipTimeout: this.cfg.leadership_timeout||(((this.cfg.heartbeat_timeout||1000) + (this.cfg.election_timeout||5000))/2),
41
38
  leaderPriority: this.cfg.leader_priority||undefined,
42
39
  initialTerm: this.antietcd.stored_term,
43
40
  send: (to, msg) => this._sendRaftMessage(to, msg),
@@ -86,7 +83,7 @@ class AntiCluster
86
83
 
87
84
  async replicateChange(msg)
88
85
  {
89
- if (this.raft.state !== TinyRaft.LEADER)
86
+ if (this.raft.state !== TinyRaft.LEADER || !this.synced)
90
87
  {
91
88
  return;
92
89
  }
@@ -104,9 +101,14 @@ class AntiCluster
104
101
  }
105
102
  }
106
103
 
104
+ unlockChange(revision)
105
+ {
106
+ return this._requestFollowers({ unlock: { revision } }, this.cfg.replication_timeout||1000);
107
+ }
108
+
107
109
  _log(msg)
108
110
  {
109
- if (this.cfg.log_level > 0)
111
+ if (this.cfg.logs.cluster)
110
112
  {
111
113
  console.log(msg);
112
114
  }
@@ -186,7 +188,6 @@ class AntiCluster
186
188
  {
187
189
  // (Re)sync with the new set of followers
188
190
  this._resync(event.followers);
189
- this.antietcd.etctree.resume_leases();
190
191
  }
191
192
  else
192
193
  {
@@ -233,7 +234,7 @@ class AntiCluster
233
234
  );
234
235
  }
235
236
  this.resync_state.dumps[client.raft_node_id] = res.error ? null : res;
236
- this._continueResync();
237
+ return this._continueResync();
237
238
  }
238
239
  });
239
240
  }
@@ -246,10 +247,10 @@ class AntiCluster
246
247
  delete this.resync_state.dumps[f];
247
248
  }
248
249
  }
249
- this._continueResync();
250
+ this._continueResync().catch(console.error);
250
251
  }
251
252
 
252
- _continueResync()
253
+ async _continueResync()
253
254
  {
254
255
  if (!this.resync_state ||
255
256
  Object.values(this.resync_state.dumps).filter(d => !d).length > 0)
@@ -286,31 +287,37 @@ class AntiCluster
286
287
  this._log(update_only ? 'Updating database from node '+with_max[i]+' state' : 'Copying node '+with_max[i]+' state');
287
288
  this.antietcd.etctree.load(this.resync_state.dumps[with_max[i]], update_only);
288
289
  }
290
+ if (this.antietcd.persistence)
291
+ {
292
+ await this.antietcd.persistence.persist();
293
+ }
294
+ this.antietcd.stored_term = this.raft.term;
295
+ if (this.cfg.use_locks)
296
+ {
297
+ this.antietcd.etctree.resync_notifications();
298
+ this.antietcd.locker.break_locks();
299
+ }
289
300
  let wait = 0;
290
301
  const load_request = { term: this.raft.term, load: this.antietcd.etctree.dump() };
291
302
  for (const follower in this.resync_state.dumps)
292
303
  {
293
304
  if (follower != this.cfg.node_id)
294
305
  {
295
- const dump = this.resync_state.dumps[follower];
296
- if (dump.term <= max_term)
306
+ const client = this._getPeer(follower);
307
+ if (!client)
297
308
  {
298
- const client = this._getPeer(follower);
299
- if (!client)
300
- {
301
- this._log('Lost peer connection during resync - restarting election');
302
- this.raft.start();
303
- return;
304
- }
305
- this._log('Copying state to '+follower);
306
- const loadstate = this.resync_state.loads[follower] = {};
307
- wait++;
308
- this._peerRequest(client, load_request, this.cfg.load_timeout||5000).then(res =>
309
- {
310
- loadstate.result = res;
311
- this._finishResync();
312
- });
309
+ this._log('Lost peer connection during resync - restarting election');
310
+ this.raft.start();
311
+ return;
313
312
  }
313
+ this._log('Copying state to '+follower);
314
+ const loadstate = this.resync_state.loads[follower] = {};
315
+ wait++;
316
+ this._peerRequest(client, load_request, this.cfg.load_timeout||5000).then(res =>
317
+ {
318
+ loadstate.result = res;
319
+ this._finishResync();
320
+ });
314
321
  }
315
322
  }
316
323
  if (!wait)
@@ -328,7 +335,7 @@ class AntiCluster
328
335
  return;
329
336
  }
330
337
  // All current peers have copied the database, we can proceed
331
- this.antietcd.stored_term = this.raft.term;
338
+ this.antietcd.etctree.resume_leases();
332
339
  this.synced = true;
333
340
  runCallbacks(this, 'wait_sync', []);
334
341
  this._log(
@@ -348,38 +355,48 @@ class AntiCluster
348
355
  return path != 'kv_range';
349
356
  }
350
357
 
351
- async checkRaftState(path, leaderonly, data)
358
+ async checkRaftState(path, data)
352
359
  {
353
360
  if (!this.raft)
354
361
  {
355
362
  return null;
356
363
  }
357
- if (leaderonly == LEADER_ONLY && this.raft.state != TinyRaft.LEADER)
364
+ if (data.leaderonly && this.raft.state != TinyRaft.LEADER)
358
365
  {
359
366
  throw new RequestError(503, 'Not leader');
360
367
  }
361
- if (leaderonly == NO_WAIT_QUORUM && this.raft.state == TinyRaft.CANDIDATE)
362
- {
363
- throw new RequestError(503, 'Quorum not available');
364
- }
365
- if (!this.synced)
368
+ if (!data.nowaitquorum)
366
369
  {
367
- // Wait for quorum / initial sync with timeout
368
- await new Promise((ok, no) =>
370
+ if (this.raft.state == TinyRaft.CANDIDATE)
369
371
  {
370
- this.wait_sync.push(ok);
371
- setTimeout(() =>
372
+ throw new RequestError(503, 'Quorum not available');
373
+ }
374
+ if (!this.synced)
375
+ {
376
+ // Wait for quorum / initial sync with timeout
377
+ await new Promise((ok, no) =>
372
378
  {
373
- this.wait_sync = this.wait_sync.filter(cb => cb != ok);
374
- no(new RequestError(503, 'Quorum not available'));
375
- }, this.cfg.wait_quorum_timeout||30000);
376
- });
379
+ this.wait_sync.push(ok);
380
+ setTimeout(() =>
381
+ {
382
+ this.wait_sync = this.wait_sync.filter(cb => cb != ok);
383
+ no(new RequestError(503, 'Quorum not available'));
384
+ }, this.cfg.wait_quorum_timeout||30000);
385
+ });
386
+ }
377
387
  }
378
- if (this.raft.state == TinyRaft.FOLLOWER &&
379
- (this._isWrite(path, data) || !this.cfg.stale_read && !(leaderonly & READ_FROM_FOLLOWER)))
388
+ if (this.raft.state == TinyRaft.FOLLOWER)
380
389
  {
381
- // Forward to leader
382
- return await this._forwardToLeader(path, data);
390
+ if (this._isWrite(path, data))
391
+ {
392
+ // Forward to leader
393
+ return await this._forwardToLeader(path, data);
394
+ }
395
+ else if (!this.cfg.stale_read && !data.serializable)
396
+ {
397
+ // Just check that the leader is still here and replicating to us
398
+ return await this._checkLeader();
399
+ }
383
400
  }
384
401
  return null;
385
402
  }
@@ -391,7 +408,27 @@ class AntiCluster
391
408
  {
392
409
  throw new RequestError(503, 'Leader is unavailable');
393
410
  }
394
- return await this._peerRequest(client, { handler, request: data }, this.cfg.forward_timeout||1000);
411
+ const res = await this._peerRequest(client, { handler, request: data }, this.cfg.forward_timeout||1000);
412
+ if (res.error)
413
+ {
414
+ throw new RequestError(503, res.error);
415
+ }
416
+ return res;
417
+ }
418
+
419
+ async _checkLeader()
420
+ {
421
+ const client = this._getPeer(this.raft.leader);
422
+ if (!client)
423
+ {
424
+ throw new RequestError(503, 'Leader is unavailable');
425
+ }
426
+ const res = await this._peerRequest(client, { check_leader: true, term: this.raft.term }, this.cfg.forward_timeout||1000);
427
+ if (res.error)
428
+ {
429
+ throw new RequestError(503, res.error);
430
+ }
431
+ return res;
395
432
  }
396
433
 
397
434
  handleWsMsg(client, msg)
@@ -409,6 +446,10 @@ class AntiCluster
409
446
  msg.identify.node_id != this.cfg.node_id)
410
447
  {
411
448
  client.raft_node_id = msg.identify.node_id;
449
+ if (!this.cluster_connections[client.raft_node_id])
450
+ {
451
+ this.cluster_connections[client.raft_node_id] = client.id;
452
+ }
412
453
  this._log('Got a connection from '+client.raft_node_id);
413
454
  }
414
455
  }
@@ -432,6 +473,14 @@ class AntiCluster
432
473
  {
433
474
  this._handleCompactMsg(client, msg);
434
475
  }
476
+ else if (msg.unlock)
477
+ {
478
+ this._handleUnlockMsg(client, msg);
479
+ }
480
+ else if (msg.check_leader)
481
+ {
482
+ this._handleCheckLeaderMsg(client, msg);
483
+ }
435
484
  }
436
485
 
437
486
  async _handleRequestMsg(client, msg)
@@ -443,7 +492,10 @@ class AntiCluster
443
492
  }
444
493
  catch (e)
445
494
  {
446
- console.error(e);
495
+ if (!(e instanceof RequestError))
496
+ {
497
+ console.error(e);
498
+ }
447
499
  client.socket.send(JSON.stringify({ request_id: msg.request_id, reply: { error: e.message } }));
448
500
  }
449
501
  }
@@ -460,6 +512,11 @@ class AntiCluster
460
512
  }
461
513
  this.antietcd.stored_term = msg.term;
462
514
  this.synced = true;
515
+ if (this.cfg.use_locks)
516
+ {
517
+ this.antietcd.etctree.resync_notifications();
518
+ this.antietcd.locker.break_locks();
519
+ }
463
520
  runCallbacks(this, 'wait_sync', []);
464
521
  this._log(
465
522
  'Synchronized with leader, new term is '+this.raft.term+
@@ -502,6 +559,35 @@ class AntiCluster
502
559
  }
503
560
  }
504
561
 
562
+ _handleUnlockMsg(client, msg)
563
+ {
564
+ if (client.raft_node_id && this.raft.state == TinyRaft.FOLLOWER &&
565
+ this.raft.leader === client.raft_node_id && this.raft.term == msg.term)
566
+ {
567
+ if (this.cfg.use_locks)
568
+ {
569
+ this.antietcd.locker.unlock_txn(msg.unlock.revision);
570
+ }
571
+ client.socket.send(JSON.stringify({ request_id: msg.request_id, reply: {} }));
572
+ }
573
+ else
574
+ {
575
+ client.socket.send(JSON.stringify({ request_id: msg.request_id, reply: { error: LEADER_MISMATCH } }));
576
+ }
577
+ }
578
+
579
+ _handleCheckLeaderMsg(client, msg)
580
+ {
581
+ if (this.raft.state == TinyRaft.LEADER && this.raft.term == msg.term)
582
+ {
583
+ client.socket.send(JSON.stringify({ request_id: msg.request_id, reply: {} }));
584
+ }
585
+ else
586
+ {
587
+ client.socket.send(JSON.stringify({ request_id: msg.request_id, reply: { error: LEADER_MISMATCH } }));
588
+ }
589
+ }
590
+
505
591
  _getPeer(to)
506
592
  {
507
593
  if (to == this.cfg.node_id)
@@ -531,8 +617,4 @@ class AntiCluster
531
617
  }
532
618
  }
533
619
 
534
- AntiCluster.LEADER_ONLY = LEADER_ONLY;
535
- AntiCluster.NO_WAIT_QUORUM = NO_WAIT_QUORUM;
536
- AntiCluster.READ_FROM_FOLLOWER = READ_FROM_FOLLOWER;
537
-
538
620
  module.exports = AntiCluster;