|
|
|
@ -0,0 +1,468 @@
|
|
|
|
|
# AntiEtcd
|
|
|
|
|
|
|
|
|
|
Simplistic miniature etcd replacement based on [TinyRaft](https://git.yourcmc.ru/vitalif/tinyraft/)
|
|
|
|
|
|
|
|
|
|
- Embeddable
|
|
|
|
|
- REST API only, gRPC is shit and will never be supported
|
|
|
|
|
- [TinyRaft](https://git.yourcmc.ru/vitalif/tinyraft/)-based leader election
|
|
|
|
|
- Websocket-based cluster communication
|
|
|
|
|
- Supports a limited subset of etcd REST APIs
|
|
|
|
|
- With optional persistence
|
|
|
|
|
|
|
|
|
|
(c) Vitaliy Filippov, 2024
|
|
|
|
|
|
|
|
|
|
License: Mozilla Public License 2.0 or [VNPL-1.1](https://git.yourcmc.ru/vitalif/vitastor/src/branch/master/VNPL-1.1.txt)
|
|
|
|
|
|
|
|
|
|
# Contents
|
|
|
|
|
|
|
|
|
|
- [CLI Usage](#cli-usage)
|
|
|
|
|
- [Options](#options)
|
|
|
|
|
- [HTTP](#http)
|
|
|
|
|
- [Persistence](#persistence)
|
|
|
|
|
- [Clustering](#clustering)
|
|
|
|
|
- [Embedded Usage](#embedded-usage)
|
|
|
|
|
- [About Persistence](#about-persistence)
|
|
|
|
|
- [Supported etcd APIs](#supported-etcd-apis)
|
|
|
|
|
- [/v3/kv/txn](#v3-kv-txn)
|
|
|
|
|
- [/v3/kv/put](#v3-kv-put)
|
|
|
|
|
- [/v3/kv/range](#v3-kv-range)
|
|
|
|
|
- [/v3/kv/deleterange](#v3-kv-deleterange)
|
|
|
|
|
- [/v3/lease/grant](#v3-lease-grant)
|
|
|
|
|
- [/v3/lease/keepalive](#v3-lease-keepalive)
|
|
|
|
|
- [/v3/lease/revoke or /v3/kv/lease/revoke](#v3-lease-revoke-or-v3-kv-lease-revoke)
|
|
|
|
|
- [Websocket-based watch APIs](#websocket-based-watch-apis)
|
|
|
|
|
- [HTTP Error Codes](#http-error-codes)
|
|
|
|
|
|
|
|
|
|
## CLI Usage
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
npm install antietcd
|
|
|
|
|
|
|
|
|
|
node_modules/.bin/antietcd \
|
|
|
|
|
[--cert ssl.crt] [--key ssl.key] [--port 12379] \
|
|
|
|
|
[--data data.gz] [--persist_interval 500] \
|
|
|
|
|
[--node_id node1 --cluster_key abcdef --cluster node1=http://localhost:12379,node2=http://localhost:12380,node3=http://localhost:12381]
|
|
|
|
|
[other options]
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Antietcd doesn't background itself, so use systemd or start-stop-daemon to run it as a background service.
|
|
|
|
|
|
|
|
|
|
## Options
|
|
|
|
|
|
|
|
|
|
### HTTP
|
|
|
|
|
|
|
|
|
|
<dl>
|
|
|
|
|
|
|
|
|
|
<dt>--port 2379</dt>
|
|
|
|
|
<dd>Listen port</dd>
|
|
|
|
|
|
|
|
|
|
<dt>--cert <cert></dt>
|
|
|
|
|
<dd>Use TLS with this certificate file (PEM format)</dd>
|
|
|
|
|
|
|
|
|
|
<dt>--key <key></dt>
|
|
|
|
|
<dd>Use TLS with this key file (PEM format)</dd>
|
|
|
|
|
|
|
|
|
|
<dt>--ca <ca></dt>
|
|
|
|
|
<dd>Use trusted root certificates from this file.
|
|
|
|
|
Specify <ca> = <cert> if your certificate is self-signed.</dd>
|
|
|
|
|
|
|
|
|
|
<dt>--client_cert_auth 1</dt>
|
|
|
|
|
<dd>Require TLS client certificates signed by <ca> or by default CA to connect.</dd>
|
|
|
|
|
|
|
|
|
|
<dt>--ws_keepalive_interval 30000</dt>
|
|
|
|
|
<dd>Client websocket ping (keepalive) interval in milliseconds</dd>
|
|
|
|
|
|
|
|
|
|
</dl>
|
|
|
|
|
|
|
|
|
|
### Persistence
|
|
|
|
|
|
|
|
|
|
<dl>
|
|
|
|
|
|
|
|
|
|
<dt>--data <filename></dt>
|
|
|
|
|
<dd>Store persistent data in <filename></dd>
|
|
|
|
|
|
|
|
|
|
<dt>--persist_interval <milliseconds></dt>
|
|
|
|
|
<dd>Persist data on disk after this interval, not immediately after change</dd>
|
|
|
|
|
|
|
|
|
|
<dt>--persist_filter ./filter.js</dt>
|
|
|
|
|
<dd>Use persistence filter from ./filter.js (or a module). <br />
|
|
|
|
|
Persistence filter is a function(cfg) returning function(key, value) ran
|
|
|
|
|
for every change and returning a new value or undefined to skip persistence.</dd>
|
|
|
|
|
|
|
|
|
|
<dt>--compact_revisions 1000</dt>
|
|
|
|
|
<dd>Number of previous revisions to keep deletion information in memory</dd>
|
|
|
|
|
|
|
|
|
|
</dl>
|
|
|
|
|
|
|
|
|
|
### Clustering
|
|
|
|
|
|
|
|
|
|
<dl>
|
|
|
|
|
|
|
|
|
|
<dt>--node_id <id></dt>
|
|
|
|
|
<dd>ID of this cluster node</dd>
|
|
|
|
|
|
|
|
|
|
<dt>--cluster <id1>=<url1>,<id2>=<url2>,...</dt>
|
|
|
|
|
<dd>All other cluster nodes</dd>
|
|
|
|
|
|
|
|
|
|
<dt>--cluster_key <key></dt>
|
|
|
|
|
<dd>Shared cluster key for identification</dd>
|
|
|
|
|
|
|
|
|
|
<dt>--election_timeout 5000</dt>
|
|
|
|
|
<dd>Raft election timeout</dd>
|
|
|
|
|
|
|
|
|
|
<dt>--heartbeat_timeout 1000</dt>
|
|
|
|
|
<dd>Raft leader heartbeat timeout</dd>
|
|
|
|
|
|
|
|
|
|
<dt>--wait_quorum_timeout 30000</dt>
|
|
|
|
|
<dd>Timeout for requests to wait for quorum to come up</dd>
|
|
|
|
|
|
|
|
|
|
<dt>--leader_priority <number></dt>
|
|
|
|
|
<dd>Raft leader priority for this node (optional)</dd>
|
|
|
|
|
|
|
|
|
|
<dt>--stale_read 1</dt>
|
|
|
|
|
<dd>Allow to serve reads from followers. Specify 0 to disallow</dd>
|
|
|
|
|
|
|
|
|
|
<dt>--reconnect_interval 1000</dt>
|
|
|
|
|
<dd>Unavailable peer connection retry interval</dd>
|
|
|
|
|
|
|
|
|
|
<dt>--dump_timeout 5000</dt>
|
|
|
|
|
<dd>Timeout for dump command in milliseconds</dd>
|
|
|
|
|
|
|
|
|
|
<dt>--load_timeout 5000</dt>
|
|
|
|
|
<dd>Timeout for load command in milliseconds</dd>
|
|
|
|
|
|
|
|
|
|
<dt>--forward_timeout 1000</dt>
|
|
|
|
|
<dd>Timeout for forwarding requests from follower to leader in milliseconds</dd>
|
|
|
|
|
|
|
|
|
|
<dt>--replication_timeout 1000</dt>
|
|
|
|
|
<dd>Timeout for replicating requests from leader to follower in milliseconds</dd>
|
|
|
|
|
|
|
|
|
|
<dt>--compact_timeout 1000</dt>
|
|
|
|
|
<dd>Timeout for compaction requests from leader to follower in milliseconds</dd>
|
|
|
|
|
|
|
|
|
|
</dl>
|
|
|
|
|
|
|
|
|
|
## Embedded Usage
|
|
|
|
|
|
|
|
|
|
```js
|
|
|
|
|
const AntiEtcd = require('antietcd');
|
|
|
|
|
|
|
|
|
|
// Configuration may contain all the same options like in CLI, without "--"
|
|
|
|
|
const srv = new AntiEtcd({ ...configuration });
|
|
|
|
|
|
|
|
|
|
// Start server
|
|
|
|
|
srv.start();
|
|
|
|
|
|
|
|
|
|
// Make a local API call in generic style:
|
|
|
|
|
let res = await srv.api('kv_txn'|'kv_range'|'kv_put'|'kv_deleterange'|'lease_grant'|'lease_revoke'|'lease_keepalive', { ...params });
|
|
|
|
|
|
|
|
|
|
// Or function-style:
|
|
|
|
|
res = await srv.txn(params);
|
|
|
|
|
res = await srv.range(params);
|
|
|
|
|
res = await srv.put(params);
|
|
|
|
|
res = await srv.deleterange(params);
|
|
|
|
|
res = await srv.lease_grant(params);
|
|
|
|
|
res = await srv.lease_revoke(params);
|
|
|
|
|
res = await srv.lease_keepalive(params);
|
|
|
|
|
|
|
|
|
|
// Error handling:
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
res = await srv.txn(params);
|
|
|
|
|
}
|
|
|
|
|
catch (e)
|
|
|
|
|
{
|
|
|
|
|
if (e instanceof AntiEtcd.RequestError)
|
|
|
|
|
{
|
|
|
|
|
// e.code is HTTP code
|
|
|
|
|
// e.message is error message
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Watch API:
|
|
|
|
|
const watch_id = await srv.create_watch(params, (message) => console.log(message));
|
|
|
|
|
await srv.cancel_watch(watch_id);
|
|
|
|
|
|
|
|
|
|
// Stop server
|
|
|
|
|
srv.stop();
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## About Persistence
|
|
|
|
|
|
|
|
|
|
Persistence is very simple: full database is dumped into JSON, gzipped and saved as file.
|
|
|
|
|
|
|
|
|
|
By default, it is written and fsynced on disk on every change, but it can be configured
|
|
|
|
|
to dump DB on disk at fixed intervals, for example, at most every 500 ms - of course,
|
|
|
|
|
at expense of slightly reduced crash resiliency (example: `--persist_interval 500`).
|
|
|
|
|
|
|
|
|
|
You can also specify a filter to exclude some data from persistence by using the option
|
|
|
|
|
`--persist_filter ./filter.js`. Persistence filter code example:
|
|
|
|
|
|
|
|
|
|
```js
|
|
|
|
|
function example_filter(cfg)
|
|
|
|
|
{
|
|
|
|
|
// <cfg> contains all command-line options
|
|
|
|
|
const prefix = cfg.exclude_keys;
|
|
|
|
|
if (!prefix)
|
|
|
|
|
{
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
return (key, value) =>
|
|
|
|
|
{
|
|
|
|
|
if (key.substr(0, prefix.length) == prefix)
|
|
|
|
|
{
|
|
|
|
|
// Skip all keys with prefix from persistence
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
if (key === '/statistics')
|
|
|
|
|
{
|
|
|
|
|
// Return <unneeded_key> from inside value
|
|
|
|
|
const decoded = JSON.parse(value);
|
|
|
|
|
return JSON.stringify({ ...decoded, unneeded_key: undefined });
|
|
|
|
|
}
|
|
|
|
|
return value;
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
module.exports = example_filter;
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Supported etcd APIs
|
|
|
|
|
|
|
|
|
|
NOTE: `key`, `value` and `range_end` are always encoded in base64, like in original etcd.
|
|
|
|
|
|
|
|
|
|
Range requests are only supported across "directories" separated by `/`.
|
|
|
|
|
|
|
|
|
|
It means that in range requests `key` must always end with `/` and `range_end` must always
|
|
|
|
|
end with `0`, and that such request will return a whole subtree of keys.
|
|
|
|
|
|
|
|
|
|
### /v3/kv/txn
|
|
|
|
|
|
|
|
|
|
Request:
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
type TxnRequest = {
|
|
|
|
|
compare?: (
|
|
|
|
|
{ key: string, target: "MOD", mod_revision: number, result?: "LESS" }
|
|
|
|
|
| { key: string, target: "CREATE", create_revision: number, result?: "LESS" }
|
|
|
|
|
| { key: string, target: "VERSION", version: number, result?: "LESS" }
|
|
|
|
|
| { key: string, target: "LEASE", lease: string, result?: "LESS" }
|
|
|
|
|
| { key: string, target: "VALUE", value: string }
|
|
|
|
|
)[],
|
|
|
|
|
success?: (
|
|
|
|
|
{ request_put: PutRequest }
|
|
|
|
|
| { request_range: RangeRequest }
|
|
|
|
|
| { request_delete_range: DeleteRangeRequest }
|
|
|
|
|
)[],
|
|
|
|
|
failure?: (
|
|
|
|
|
{ request_put: PutRequest }
|
|
|
|
|
| { request_range: RangeRequest }
|
|
|
|
|
| { request_delete_range: DeleteRangeRequest }
|
|
|
|
|
)[],
|
|
|
|
|
serializable?: boolean,
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
`serializable` allows to serve read-only requests from follower even if `stale_read` is not enabled.
|
|
|
|
|
|
|
|
|
|
Response:
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
type TxnResponse = {
|
|
|
|
|
header: { revision: number },
|
|
|
|
|
succeeded: boolean,
|
|
|
|
|
responses: (
|
|
|
|
|
{ response_put: PutResponse }
|
|
|
|
|
| { response_range: RangeResponse }
|
|
|
|
|
| { response_delete_range: DeleteRangeResponse }
|
|
|
|
|
)[],
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### /v3/kv/put
|
|
|
|
|
|
|
|
|
|
Request:
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
type PutRequest = {
|
|
|
|
|
key: string,
|
|
|
|
|
value: string,
|
|
|
|
|
lease?: string,
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Other parameters are not supported: prev_kv, ignore_value, ignore_lease.
|
|
|
|
|
|
|
|
|
|
Response:
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
type PutResponse = {
|
|
|
|
|
header: { revision: number },
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### /v3/kv/range
|
|
|
|
|
|
|
|
|
|
Request:
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
type RangeRequest = {
|
|
|
|
|
key: string,
|
|
|
|
|
range_end?: string,
|
|
|
|
|
keys_only?: boolean,
|
|
|
|
|
serializable?: boolean,
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
`serializable` allows to serve read-only requests from follower even if `stale_read` is not enabled.
|
|
|
|
|
|
|
|
|
|
Other parameters are not supported: revision, limit, sort_order, sort_target,
|
|
|
|
|
count_only, min_mod_revision, max_mod_revision, min_create_revision, max_create_revision.
|
|
|
|
|
|
|
|
|
|
Response:
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
type RangeResponse = {
|
|
|
|
|
header: { revision: number },
|
|
|
|
|
kvs: { key: string }[] | {
|
|
|
|
|
key: string,
|
|
|
|
|
value: string,
|
|
|
|
|
lease?: string,
|
|
|
|
|
mod_revision: number,
|
|
|
|
|
}[],
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### /v3/kv/deleterange
|
|
|
|
|
|
|
|
|
|
Request:
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
type DeleteRangeRequest = {
|
|
|
|
|
key: string,
|
|
|
|
|
range_end?: string,
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Other parameters are not supported: prev_kv.
|
|
|
|
|
|
|
|
|
|
Response:
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
type DeleteRangeResponse = {
|
|
|
|
|
header: { revision: number },
|
|
|
|
|
// number of deleted keys
|
|
|
|
|
deleted: number,
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### /v3/lease/grant
|
|
|
|
|
|
|
|
|
|
Request:
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
type LeaseGrantRequest = {
|
|
|
|
|
ID?: string,
|
|
|
|
|
TTL: number,
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Response:
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
type LeaseGrantResponse = {
|
|
|
|
|
header: { revision: number },
|
|
|
|
|
ID: string,
|
|
|
|
|
TTL: number,
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### /v3/lease/keepalive
|
|
|
|
|
|
|
|
|
|
Request:
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
type LeaseKeepaliveRequest = {
|
|
|
|
|
ID: string,
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Response:
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
type LeaseKeepaliveResponse = {
|
|
|
|
|
result: {
|
|
|
|
|
header: { revision: number },
|
|
|
|
|
ID: string,
|
|
|
|
|
TTL: number,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### /v3/lease/revoke or /v3/kv/lease/revoke
|
|
|
|
|
|
|
|
|
|
Request:
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
type LeaseRevokeRequest = {
|
|
|
|
|
ID: string,
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Response:
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
type LeaseRevokeResponse = {
|
|
|
|
|
header: { revision: number },
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### Websocket-based watch APIs
|
|
|
|
|
|
|
|
|
|
Client-to-server message format:
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
type ClientMessage =
|
|
|
|
|
{ create_request: {
|
|
|
|
|
key: string,
|
|
|
|
|
range_end?: string,
|
|
|
|
|
start_revision?: number,
|
|
|
|
|
watch_id?: string,
|
|
|
|
|
} }
|
|
|
|
|
| { cancel_request: {
|
|
|
|
|
watch_id: string,
|
|
|
|
|
} }
|
|
|
|
|
| { progress_request: {} }
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Server-to-client message format:
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
type ServerMessage = {
|
|
|
|
|
result: {
|
|
|
|
|
header: { revision: number },
|
|
|
|
|
watch_id: string,
|
|
|
|
|
created?: boolean,
|
|
|
|
|
canceled?: boolean,
|
|
|
|
|
compact_revision?: number,
|
|
|
|
|
events?: {
|
|
|
|
|
type: 'PUT'|'DELETE',
|
|
|
|
|
kv: {
|
|
|
|
|
key: string,
|
|
|
|
|
value: string,
|
|
|
|
|
lease?: string,
|
|
|
|
|
mod_revision: number,
|
|
|
|
|
},
|
|
|
|
|
}[],
|
|
|
|
|
}
|
|
|
|
|
} | { error: 'bad-json' } | { error: 'empty-message' }
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### HTTP Error Codes
|
|
|
|
|
|
|
|
|
|
- 400 for invalid requests
|
|
|
|
|
- 404 for unsupported API / URL not found
|
|
|
|
|
- 405 for non-POST request method
|
|
|
|
|
- 501 for unsupported API feature - non-directory range queries and so on
|
|
|
|
|
- 502 for server is stopping
|
|
|
|
|
- 503 for quorum-related errors - quorum not available and so on
|