antietcd 1.0.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 +501 -0
- package/anticli.js +263 -0
- package/anticluster.js +526 -0
- package/antietcd-app.js +122 -0
- package/antietcd.d.ts +155 -0
- package/antietcd.js +552 -0
- package/antipersistence.js +138 -0
- package/common.js +38 -0
- package/etctree.js +875 -0
- package/package.json +55 -0
- package/stable-stringify.js +78 -0
package/README.md
ADDED
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
# AntiEtcd
|
|
2
|
+
|
|
3
|
+
Simplistic miniature etcd replacement based on [TinyRaft](https://git.yourcmc.ru/vitalif/tinyraft/)
|
|
4
|
+
|
|
5
|
+
- Embeddable
|
|
6
|
+
- REST API only, gRPC is shit and will never be supported
|
|
7
|
+
- [TinyRaft](https://git.yourcmc.ru/vitalif/tinyraft/)-based leader election
|
|
8
|
+
- Websocket-based cluster communication
|
|
9
|
+
- Supports a limited subset of etcd REST APIs
|
|
10
|
+
- With optional persistence
|
|
11
|
+
|
|
12
|
+
# Contents
|
|
13
|
+
|
|
14
|
+
- [CLI Usage](#cli-usage)
|
|
15
|
+
- [CLI Client](#cli-client)
|
|
16
|
+
- [Options](#options)
|
|
17
|
+
- [HTTP](#http)
|
|
18
|
+
- [Persistence](#persistence)
|
|
19
|
+
- [Clustering](#clustering)
|
|
20
|
+
- [Embedded Usage](#embedded-usage)
|
|
21
|
+
- [About Persistence](#about-persistence)
|
|
22
|
+
- [Supported etcd APIs](#supported-etcd-apis)
|
|
23
|
+
- [/v3/kv/txn](#v3-kv-txn)
|
|
24
|
+
- [/v3/kv/put](#v3-kv-put)
|
|
25
|
+
- [/v3/kv/range](#v3-kv-range)
|
|
26
|
+
- [/v3/kv/deleterange](#v3-kv-deleterange)
|
|
27
|
+
- [/v3/lease/grant](#v3-lease-grant)
|
|
28
|
+
- [/v3/lease/keepalive](#v3-lease-keepalive)
|
|
29
|
+
- [/v3/lease/revoke or /v3/kv/lease/revoke](#v3-lease-revoke-or-v3-kv-lease-revoke)
|
|
30
|
+
- [Websocket-based watch APIs](#websocket-based-watch-apis)
|
|
31
|
+
- [HTTP Error Codes](#http-error-codes)
|
|
32
|
+
|
|
33
|
+
## CLI Usage
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
npm install antietcd
|
|
37
|
+
|
|
38
|
+
node_modules/.bin/antietcd \
|
|
39
|
+
[--cert ssl.crt] [--key ssl.key] [--port 12379] \
|
|
40
|
+
[--data data.gz] [--persist_interval 500] \
|
|
41
|
+
[--node_id node1 --cluster_key abcdef --cluster node1=http://localhost:12379,node2=http://localhost:12380,node3=http://localhost:12381]
|
|
42
|
+
[other options]
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Antietcd doesn't background itself, so use systemd or start-stop-daemon to run it as a background service.
|
|
46
|
+
|
|
47
|
+
### CLI Client
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
node_modules/.bin/anticli [OPTIONS] put <key> [<value>]
|
|
51
|
+
node_modules/.bin/anticli [OPTIONS] get <key> [-p|--prefix] [-v|--print-value-only] [-k|--keys-only]
|
|
52
|
+
node_modules/.bin/anticli [OPTIONS] del <key> [-p|--prefix]
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
For `put`, if `<value>` is not specified, it will be read from STDIN.
|
|
56
|
+
|
|
57
|
+
Options:
|
|
58
|
+
|
|
59
|
+
<dl>
|
|
60
|
+
|
|
61
|
+
<dt>--endpoints|-e http://node1:2379,http://node2:2379,http://node3:2379</dt>
|
|
62
|
+
<dd>Specify HTTP endpoints to connect to</dd>
|
|
63
|
+
|
|
64
|
+
<dt>--cert <cert></dt>
|
|
65
|
+
<dd>Use TLS with this certificate file (PEM format)</dd>
|
|
66
|
+
|
|
67
|
+
<dt>--key <key></dt>
|
|
68
|
+
<dd>Use TLS with this key file (PEM format)</dd>
|
|
69
|
+
|
|
70
|
+
<dt>--timeout 1000</dt>
|
|
71
|
+
<dd>Specify request timeout in milliseconds</dd>
|
|
72
|
+
|
|
73
|
+
</dl>
|
|
74
|
+
|
|
75
|
+
## Options
|
|
76
|
+
|
|
77
|
+
### HTTP
|
|
78
|
+
|
|
79
|
+
<dl>
|
|
80
|
+
|
|
81
|
+
<dt>--port 2379</dt>
|
|
82
|
+
<dd>Listen port</dd>
|
|
83
|
+
|
|
84
|
+
<dt>--cert <cert></dt>
|
|
85
|
+
<dd>Use TLS with this certificate file (PEM format)</dd>
|
|
86
|
+
|
|
87
|
+
<dt>--key <key></dt>
|
|
88
|
+
<dd>Use TLS with this key file (PEM format)</dd>
|
|
89
|
+
|
|
90
|
+
<dt>--ca <ca></dt>
|
|
91
|
+
<dd>Use trusted root certificates from this file.
|
|
92
|
+
Specify <ca> = <cert> if your certificate is self-signed.</dd>
|
|
93
|
+
|
|
94
|
+
<dt>--client_cert_auth 1</dt>
|
|
95
|
+
<dd>Require TLS client certificates signed by <ca> or by default CA to connect.</dd>
|
|
96
|
+
|
|
97
|
+
<dt>--ws_keepalive_interval 30000</dt>
|
|
98
|
+
<dd>Client websocket ping (keepalive) interval in milliseconds</dd>
|
|
99
|
+
|
|
100
|
+
</dl>
|
|
101
|
+
|
|
102
|
+
### Persistence
|
|
103
|
+
|
|
104
|
+
<dl>
|
|
105
|
+
|
|
106
|
+
<dt>--data <filename></dt>
|
|
107
|
+
<dd>Store persistent data in <filename></dd>
|
|
108
|
+
|
|
109
|
+
<dt>--persist_interval <milliseconds></dt>
|
|
110
|
+
<dd>Persist data on disk after this interval, not immediately after change</dd>
|
|
111
|
+
|
|
112
|
+
<dt>--persist_filter ./filter.js</dt>
|
|
113
|
+
<dd>Use persistence filter from ./filter.js (or a module). <br />
|
|
114
|
+
Persistence filter is a function(cfg) returning function(key, value) ran
|
|
115
|
+
for every change and returning a new value or undefined to skip persistence.</dd>
|
|
116
|
+
|
|
117
|
+
<dt>--compact_revisions 1000</dt>
|
|
118
|
+
<dd>Number of previous revisions to keep deletion information in memory</dd>
|
|
119
|
+
|
|
120
|
+
</dl>
|
|
121
|
+
|
|
122
|
+
### Clustering
|
|
123
|
+
|
|
124
|
+
<dl>
|
|
125
|
+
|
|
126
|
+
<dt>--node_id <id></dt>
|
|
127
|
+
<dd>ID of this cluster node</dd>
|
|
128
|
+
|
|
129
|
+
<dt>--cluster <id1>=<url1>,<id2>=<url2>,...</dt>
|
|
130
|
+
<dd>All other cluster nodes</dd>
|
|
131
|
+
|
|
132
|
+
<dt>--cluster_key <key></dt>
|
|
133
|
+
<dd>Shared cluster key for identification</dd>
|
|
134
|
+
|
|
135
|
+
<dt>--election_timeout 5000</dt>
|
|
136
|
+
<dd>Raft election timeout</dd>
|
|
137
|
+
|
|
138
|
+
<dt>--heartbeat_timeout 1000</dt>
|
|
139
|
+
<dd>Raft leader heartbeat timeout</dd>
|
|
140
|
+
|
|
141
|
+
<dt>--wait_quorum_timeout 30000</dt>
|
|
142
|
+
<dd>Timeout for requests to wait for quorum to come up</dd>
|
|
143
|
+
|
|
144
|
+
<dt>--leader_priority <number></dt>
|
|
145
|
+
<dd>Raft leader priority for this node (optional)</dd>
|
|
146
|
+
|
|
147
|
+
<dt>--stale_read 1</dt>
|
|
148
|
+
<dd>Allow to serve reads from followers. Specify 0 to disallow</dd>
|
|
149
|
+
|
|
150
|
+
<dt>--reconnect_interval 1000</dt>
|
|
151
|
+
<dd>Unavailable peer connection retry interval</dd>
|
|
152
|
+
|
|
153
|
+
<dt>--dump_timeout 5000</dt>
|
|
154
|
+
<dd>Timeout for dump command in milliseconds</dd>
|
|
155
|
+
|
|
156
|
+
<dt>--load_timeout 5000</dt>
|
|
157
|
+
<dd>Timeout for load command in milliseconds</dd>
|
|
158
|
+
|
|
159
|
+
<dt>--forward_timeout 1000</dt>
|
|
160
|
+
<dd>Timeout for forwarding requests from follower to leader in milliseconds</dd>
|
|
161
|
+
|
|
162
|
+
<dt>--replication_timeout 1000</dt>
|
|
163
|
+
<dd>Timeout for replicating requests from leader to follower in milliseconds</dd>
|
|
164
|
+
|
|
165
|
+
<dt>--compact_timeout 1000</dt>
|
|
166
|
+
<dd>Timeout for compaction requests from leader to follower in milliseconds</dd>
|
|
167
|
+
|
|
168
|
+
</dl>
|
|
169
|
+
|
|
170
|
+
## Embedded Usage
|
|
171
|
+
|
|
172
|
+
```js
|
|
173
|
+
const AntiEtcd = require('antietcd');
|
|
174
|
+
|
|
175
|
+
// Configuration may contain all the same options like in CLI, without "--"
|
|
176
|
+
// Except that persist_filter should be a callback (key, value) => newValue
|
|
177
|
+
const srv = new AntiEtcd({ ...configuration });
|
|
178
|
+
|
|
179
|
+
// Start server
|
|
180
|
+
srv.start();
|
|
181
|
+
|
|
182
|
+
// Make a local API call in generic style:
|
|
183
|
+
let res = await srv.api('kv_txn'|'kv_range'|'kv_put'|'kv_deleterange'|'lease_grant'|'lease_revoke'|'lease_keepalive', { ...params });
|
|
184
|
+
|
|
185
|
+
// Or function-style:
|
|
186
|
+
res = await srv.txn(params);
|
|
187
|
+
res = await srv.range(params);
|
|
188
|
+
res = await srv.put(params);
|
|
189
|
+
res = await srv.deleterange(params);
|
|
190
|
+
res = await srv.lease_grant(params);
|
|
191
|
+
res = await srv.lease_revoke(params);
|
|
192
|
+
res = await srv.lease_keepalive(params);
|
|
193
|
+
|
|
194
|
+
// Error handling:
|
|
195
|
+
try
|
|
196
|
+
{
|
|
197
|
+
res = await srv.txn(params);
|
|
198
|
+
}
|
|
199
|
+
catch (e)
|
|
200
|
+
{
|
|
201
|
+
if (e instanceof AntiEtcd.RequestError)
|
|
202
|
+
{
|
|
203
|
+
// e.code is HTTP code
|
|
204
|
+
// e.message is error message
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Watch API:
|
|
209
|
+
const watch_id = await srv.create_watch(params, (message) => console.log(message));
|
|
210
|
+
await srv.cancel_watch(watch_id);
|
|
211
|
+
|
|
212
|
+
// Stop server
|
|
213
|
+
srv.stop();
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## About Persistence
|
|
217
|
+
|
|
218
|
+
Persistence is very simple: full database is dumped into JSON, gzipped and saved as file.
|
|
219
|
+
|
|
220
|
+
By default, it is written and fsynced on disk on every change, but it can be configured
|
|
221
|
+
to dump DB on disk at fixed intervals, for example, at most every 500 ms - of course,
|
|
222
|
+
at expense of slightly reduced crash resiliency (example: `--persist_interval 500`).
|
|
223
|
+
|
|
224
|
+
You can also specify a filter to exclude some data from persistence by using the option
|
|
225
|
+
`--persist_filter ./filter.js`. Persistence filter code example:
|
|
226
|
+
|
|
227
|
+
```js
|
|
228
|
+
function example_filter(cfg)
|
|
229
|
+
{
|
|
230
|
+
// <cfg> contains all command-line options
|
|
231
|
+
const prefix = cfg.exclude_keys;
|
|
232
|
+
if (!prefix)
|
|
233
|
+
{
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
return (key, value) =>
|
|
237
|
+
{
|
|
238
|
+
if (key.substr(0, prefix.length) == prefix)
|
|
239
|
+
{
|
|
240
|
+
// Skip all keys with prefix from persistence
|
|
241
|
+
return undefined;
|
|
242
|
+
}
|
|
243
|
+
if (key === '/statistics')
|
|
244
|
+
{
|
|
245
|
+
// Return <unneeded_key> from inside value
|
|
246
|
+
const decoded = JSON.parse(value);
|
|
247
|
+
return JSON.stringify({ ...decoded, unneeded_key: undefined });
|
|
248
|
+
}
|
|
249
|
+
return value;
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
module.exports = example_filter;
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
## Supported etcd APIs
|
|
257
|
+
|
|
258
|
+
NOTE: `key`, `value` and `range_end` are always encoded in base64, like in original etcd.
|
|
259
|
+
|
|
260
|
+
Range requests are only supported across "directories" separated by `/`.
|
|
261
|
+
|
|
262
|
+
It means that in range requests `key` must always end with `/` and `range_end` must always
|
|
263
|
+
end with `0`, and that such request will return a whole subtree of keys.
|
|
264
|
+
|
|
265
|
+
### /v3/kv/txn
|
|
266
|
+
|
|
267
|
+
Request:
|
|
268
|
+
|
|
269
|
+
```ts
|
|
270
|
+
type TxnRequest = {
|
|
271
|
+
compare?: (
|
|
272
|
+
{ key: string, target: "MOD", mod_revision: number, result?: "LESS" }
|
|
273
|
+
| { key: string, target: "CREATE", create_revision: number, result?: "LESS" }
|
|
274
|
+
| { key: string, target: "VERSION", version: number, result?: "LESS" }
|
|
275
|
+
| { key: string, target: "LEASE", lease: string, result?: "LESS" }
|
|
276
|
+
| { key: string, target: "VALUE", value: string }
|
|
277
|
+
)[],
|
|
278
|
+
success?: (
|
|
279
|
+
{ request_put: PutRequest }
|
|
280
|
+
| { request_range: RangeRequest }
|
|
281
|
+
| { request_delete_range: DeleteRangeRequest }
|
|
282
|
+
)[],
|
|
283
|
+
failure?: (
|
|
284
|
+
{ request_put: PutRequest }
|
|
285
|
+
| { request_range: RangeRequest }
|
|
286
|
+
| { request_delete_range: DeleteRangeRequest }
|
|
287
|
+
)[],
|
|
288
|
+
serializable?: boolean,
|
|
289
|
+
}
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
`serializable` allows to serve read-only requests from follower even if `stale_read` is not enabled.
|
|
293
|
+
|
|
294
|
+
Response:
|
|
295
|
+
|
|
296
|
+
```ts
|
|
297
|
+
type TxnResponse = {
|
|
298
|
+
header: { revision: number },
|
|
299
|
+
succeeded: boolean,
|
|
300
|
+
responses: (
|
|
301
|
+
{ response_put: PutResponse }
|
|
302
|
+
| { response_range: RangeResponse }
|
|
303
|
+
| { response_delete_range: DeleteRangeResponse }
|
|
304
|
+
)[],
|
|
305
|
+
}
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
### /v3/kv/put
|
|
309
|
+
|
|
310
|
+
Request:
|
|
311
|
+
|
|
312
|
+
```ts
|
|
313
|
+
type PutRequest = {
|
|
314
|
+
key: string,
|
|
315
|
+
value: string,
|
|
316
|
+
lease?: string,
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
Other parameters are not supported: prev_kv, ignore_value, ignore_lease.
|
|
321
|
+
|
|
322
|
+
Response:
|
|
323
|
+
|
|
324
|
+
```ts
|
|
325
|
+
type PutResponse = {
|
|
326
|
+
header: { revision: number },
|
|
327
|
+
}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### /v3/kv/range
|
|
331
|
+
|
|
332
|
+
Request:
|
|
333
|
+
|
|
334
|
+
```ts
|
|
335
|
+
type RangeRequest = {
|
|
336
|
+
key: string,
|
|
337
|
+
range_end?: string,
|
|
338
|
+
keys_only?: boolean,
|
|
339
|
+
serializable?: boolean,
|
|
340
|
+
}
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
`serializable` allows to serve read-only requests from follower even if `stale_read` is not enabled.
|
|
344
|
+
|
|
345
|
+
Other parameters are not supported: revision, limit, sort_order, sort_target,
|
|
346
|
+
count_only, min_mod_revision, max_mod_revision, min_create_revision, max_create_revision.
|
|
347
|
+
|
|
348
|
+
Response:
|
|
349
|
+
|
|
350
|
+
```ts
|
|
351
|
+
type RangeResponse = {
|
|
352
|
+
header: { revision: number },
|
|
353
|
+
kvs: { key: string }[] | {
|
|
354
|
+
key: string,
|
|
355
|
+
value: string,
|
|
356
|
+
lease?: string,
|
|
357
|
+
mod_revision: number,
|
|
358
|
+
}[],
|
|
359
|
+
}
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
### /v3/kv/deleterange
|
|
363
|
+
|
|
364
|
+
Request:
|
|
365
|
+
|
|
366
|
+
```ts
|
|
367
|
+
type DeleteRangeRequest = {
|
|
368
|
+
key: string,
|
|
369
|
+
range_end?: string,
|
|
370
|
+
}
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
Other parameters are not supported: prev_kv.
|
|
374
|
+
|
|
375
|
+
Response:
|
|
376
|
+
|
|
377
|
+
```ts
|
|
378
|
+
type DeleteRangeResponse = {
|
|
379
|
+
header: { revision: number },
|
|
380
|
+
// number of deleted keys
|
|
381
|
+
deleted: number,
|
|
382
|
+
}
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
### /v3/lease/grant
|
|
386
|
+
|
|
387
|
+
Request:
|
|
388
|
+
|
|
389
|
+
```ts
|
|
390
|
+
type LeaseGrantRequest = {
|
|
391
|
+
ID?: string,
|
|
392
|
+
TTL: number,
|
|
393
|
+
}
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
Response:
|
|
397
|
+
|
|
398
|
+
```ts
|
|
399
|
+
type LeaseGrantResponse = {
|
|
400
|
+
header: { revision: number },
|
|
401
|
+
ID: string,
|
|
402
|
+
TTL: number,
|
|
403
|
+
}
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
### /v3/lease/keepalive
|
|
407
|
+
|
|
408
|
+
Request:
|
|
409
|
+
|
|
410
|
+
```ts
|
|
411
|
+
type LeaseKeepaliveRequest = {
|
|
412
|
+
ID: string,
|
|
413
|
+
}
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
Response:
|
|
417
|
+
|
|
418
|
+
```ts
|
|
419
|
+
type LeaseKeepaliveResponse = {
|
|
420
|
+
result: {
|
|
421
|
+
header: { revision: number },
|
|
422
|
+
ID: string,
|
|
423
|
+
TTL: number,
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
### /v3/lease/revoke or /v3/kv/lease/revoke
|
|
429
|
+
|
|
430
|
+
Request:
|
|
431
|
+
|
|
432
|
+
```ts
|
|
433
|
+
type LeaseRevokeRequest = {
|
|
434
|
+
ID: string,
|
|
435
|
+
}
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
Response:
|
|
439
|
+
|
|
440
|
+
```ts
|
|
441
|
+
type LeaseRevokeResponse = {
|
|
442
|
+
header: { revision: number },
|
|
443
|
+
}
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
### Websocket-based watch APIs
|
|
447
|
+
|
|
448
|
+
Client-to-server message format:
|
|
449
|
+
|
|
450
|
+
```ts
|
|
451
|
+
type ClientMessage =
|
|
452
|
+
{ create_request: {
|
|
453
|
+
key: string,
|
|
454
|
+
range_end?: string,
|
|
455
|
+
start_revision?: number,
|
|
456
|
+
watch_id?: string,
|
|
457
|
+
} }
|
|
458
|
+
| { cancel_request: {
|
|
459
|
+
watch_id: string,
|
|
460
|
+
} }
|
|
461
|
+
| { progress_request: {} }
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
Server-to-client message format:
|
|
465
|
+
|
|
466
|
+
```ts
|
|
467
|
+
type ServerMessage = {
|
|
468
|
+
result: {
|
|
469
|
+
header: { revision: number },
|
|
470
|
+
watch_id: string,
|
|
471
|
+
created?: boolean,
|
|
472
|
+
canceled?: boolean,
|
|
473
|
+
compact_revision?: number,
|
|
474
|
+
events?: {
|
|
475
|
+
type: 'PUT'|'DELETE',
|
|
476
|
+
kv: {
|
|
477
|
+
key: string,
|
|
478
|
+
value: string,
|
|
479
|
+
lease?: string,
|
|
480
|
+
mod_revision: number,
|
|
481
|
+
},
|
|
482
|
+
}[],
|
|
483
|
+
}
|
|
484
|
+
} | { error: 'bad-json' } | { error: 'empty-message' }
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
### HTTP Error Codes
|
|
488
|
+
|
|
489
|
+
- 400 for invalid requests
|
|
490
|
+
- 404 for unsupported API / URL not found
|
|
491
|
+
- 405 for non-POST request method
|
|
492
|
+
- 501 for unsupported API feature - non-directory range queries and so on
|
|
493
|
+
- 502 for server is stopping
|
|
494
|
+
- 503 for quorum-related errors - quorum not available and so on
|
|
495
|
+
|
|
496
|
+
# Author and License
|
|
497
|
+
|
|
498
|
+
Author: Vitaliy Filippov, 2024
|
|
499
|
+
|
|
500
|
+
License: [Mozilla Public License 2.0](https://www.mozilla.org/media/MPL/2.0/index.f75d2927d3c1.txt)
|
|
501
|
+
or [Vitastor Network Public License 1.1](https://git.yourcmc.ru/vitalif/vitastor/src/branch/master/VNPL-1.1.txt)
|