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/README.md +53 -3
- package/anticli.js +100 -15
- 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 +3 -3
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] [--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
|
|
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
|
|
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
|
|
327
|
-
{
|
|
328
|
-
return Buffer.from(str).toString('base64');
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
function de64(str)
|
|
416
|
+
function is_true(s)
|
|
332
417
|
{
|
|
333
|
-
return
|
|
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.
|
|
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;
|
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
|
-
--
|
|
50
|
-
|
|
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()
|