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/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] [--ca ca.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,6 +100,7 @@ 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);
101
104
  this.tls = {};
102
105
  if (this.options.cert && this.options.key)
103
106
  {
@@ -124,6 +127,10 @@ class AntiEtcdCli
124
127
  {
125
128
  await this.load();
126
129
  }
130
+ else if (cmd[0] == 'watch')
131
+ {
132
+ await this.watch(cmd.slice(1));
133
+ }
127
134
  // wait until output is fully flushed
128
135
  await new Promise(ok => process.stdout.write('', ok));
129
136
  await new Promise(ok => process.stderr.write('', ok));
@@ -171,7 +178,9 @@ class AntiEtcdCli
171
178
  {
172
179
  keys = keys.map(k => k.replace(/\/+$/, ''));
173
180
  }
174
- 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) } })) };
175
184
  const res = await this.request('/v3/kv/txn', txn);
176
185
  if (this.options.notemp)
177
186
  {
@@ -197,11 +206,11 @@ class AntiEtcdCli
197
206
  {
198
207
  if (!this.options.print_value_only)
199
208
  {
200
- process.stdout.write(de64(kv.key)+'\n');
209
+ process.stdout.write(this.de64(kv.key)+'\n');
201
210
  }
202
211
  if (!this.options.keys_only)
203
212
  {
204
- process.stdout.write(de64(kv.value)+'\n');
213
+ process.stdout.write(this.de64(kv.value)+'\n');
205
214
  }
206
215
  }
207
216
  }
@@ -214,7 +223,7 @@ class AntiEtcdCli
214
223
  {
215
224
  value = await new Promise((ok, no) => fs.readFile(0, { encoding: 'utf-8' }, (err, res) => err ? no(err) : ok(res)));
216
225
  }
217
- 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) });
218
227
  if (res.header)
219
228
  {
220
229
  process.stdout.write('OK\n');
@@ -227,7 +236,9 @@ class AntiEtcdCli
227
236
  {
228
237
  keys = keys.map(k => k.replace(/\/+$/, ''));
229
238
  }
230
- 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) } })) };
231
242
  const res = await this.request('/v3/kv/txn', txn);
232
243
  for (const r of res.responses||[])
233
244
  {
@@ -238,6 +249,75 @@ class AntiEtcdCli
238
249
  }
239
250
  }
240
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
+
241
321
  async request(path, body)
242
322
  {
243
323
  for (const url of this.options.endpoints)
@@ -270,6 +350,16 @@ class AntiEtcdCli
270
350
  }
271
351
  process.exit(1);
272
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
+ }
273
363
  }
274
364
 
275
365
  function POST(url, options, body, timeout)
@@ -323,14 +413,9 @@ function POST(url, options, body, timeout)
323
413
  });
324
414
  }
325
415
 
326
- function b64(str)
327
- {
328
- return Buffer.from(str).toString('base64');
329
- }
330
-
331
- function de64(str)
416
+ function is_true(s)
332
417
  {
333
- return Buffer.from(str, 'base64').toString();
418
+ return s === true || s === 1 || s === '1' || s === 'yes' || s === 'true' || s === 'on';
334
419
  }
335
420
 
336
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;
package/antietcd-app.js CHANGED
@@ -43,11 +43,19 @@ HTTP:
43
43
  Require TLS client certificates signed by <ca> or by default CA to connect.
44
44
  --ws_keepalive_interval 30000
45
45
  Client websocket ping (keepalive) interval in milliseconds
46
+ --use_base64 1
47
+ Use base64 encoding of keys and values, like in etcd (enabled by default).
48
+
49
+ Consistency:
50
+
51
+ --use_locks 1
52
+ Enable lock-based transaction isolation to prevent G1a anomaly (aborted read).
46
53
  --merge_watches 1
47
54
  Antietcd merges all watcher events into a single websocket message to provide
48
55
  more ordering/transaction guarantees. Set to 0 to disable this behaviour.
49
- --use_base64 1
50
- Use base64 encoding of keys and values, like in etcd (enabled by default).
56
+ --stale_read 1
57
+ Allow to serve reads from replicas (potentially stale). Specify 0 to always
58
+ contact leader on every read.
51
59
 
52
60
  Persistence:
53
61
 
@@ -76,10 +84,10 @@ Clustering:
76
84
  Raft leader heartbeat timeout
77
85
  --wait_quorum_timeout 30000
78
86
  Timeout for requests to wait for quorum to come up
87
+ --leadership_timeout 3000
88
+ TinyRaft leadership timeout
79
89
  --leader_priority <number>
80
90
  Raft leader priority for this node (optional)
81
- --stale_read 1
82
- Allow to serve reads from followers. Specify 0 to disallow
83
91
  --reconnect_interval 1000
84
92
  Unavailable peer connection retry interval
85
93
  --dump_timeout 5000
@@ -92,6 +100,14 @@ Clustering:
92
100
  Timeout for replicating requests from leader to follower in milliseconds
93
101
  --compact_timeout 1000
94
102
  Timeout for compaction requests from leader to follower in milliseconds
103
+
104
+ Logging:
105
+
106
+ --logs keyword1,keyword2,...
107
+ Enable logs by keywords:
108
+ access: log all requests and incoming websocket messages.
109
+ watch: log outgoing websocket watch messages.
110
+ cluster: log clustering (TinyRaft) events.
95
111
  `;
96
112
 
97
113
  function parse()