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/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 <key></dt>
|
|
70
73
|
<dd>Use TLS with this key file (PEM format)</dd>
|
|
71
74
|
|
|
75
|
+
<dt>--ca <cert></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 <ca> = <cert> 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 <number></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
|
-
|
|
105
|
-
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
324
|
-
{
|
|
325
|
-
return Buffer.from(str).toString('base64');
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
function de64(str)
|
|
416
|
+
function is_true(s)
|
|
329
417
|
{
|
|
330
|
-
return
|
|
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.
|
|
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
|
|
296
|
-
if (
|
|
306
|
+
const client = this._getPeer(follower);
|
|
307
|
+
if (!client)
|
|
297
308
|
{
|
|
298
|
-
|
|
299
|
-
|
|
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.
|
|
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,
|
|
358
|
+
async checkRaftState(path, data)
|
|
352
359
|
{
|
|
353
360
|
if (!this.raft)
|
|
354
361
|
{
|
|
355
362
|
return null;
|
|
356
363
|
}
|
|
357
|
-
if (leaderonly
|
|
364
|
+
if (data.leaderonly && this.raft.state != TinyRaft.LEADER)
|
|
358
365
|
{
|
|
359
366
|
throw new RequestError(503, 'Not leader');
|
|
360
367
|
}
|
|
361
|
-
if (
|
|
362
|
-
{
|
|
363
|
-
throw new RequestError(503, 'Quorum not available');
|
|
364
|
-
}
|
|
365
|
-
if (!this.synced)
|
|
368
|
+
if (!data.nowaitquorum)
|
|
366
369
|
{
|
|
367
|
-
|
|
368
|
-
await new Promise((ok, no) =>
|
|
370
|
+
if (this.raft.state == TinyRaft.CANDIDATE)
|
|
369
371
|
{
|
|
370
|
-
|
|
371
|
-
|
|
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
|
|
374
|
-
|
|
375
|
-
|
|
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
|
-
|
|
382
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|