Compare commits
No commits in common. "5140ba96da7af098868238649ee14375bcb43167" and "46276e3443d2fd7c70fff826e678b7ff37783706" have entirely different histories.
5140ba96da
...
46276e3443
498
ANTIETCD.md
498
ANTIETCD.md
|
@ -1,498 +0,0 @@
|
|||
# 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)
|
||||
- [CLI Client](#cli-client)
|
||||
- [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.
|
||||
|
||||
### CLI Client
|
||||
|
||||
```
|
||||
node_modules/.bin/anticli [OPTIONS] put <key> [<value>]
|
||||
node_modules/.bin/anticli [OPTIONS] get <key> [-p|--prefix] [-v|--print-value-only] [-k|--keys-only]
|
||||
node_modules/.bin/anticli [OPTIONS] del <key> [-p|--prefix]
|
||||
```
|
||||
|
||||
For `put`, if `<value>` is not specified, it will be read from STDIN.
|
||||
|
||||
Options:
|
||||
|
||||
<dl>
|
||||
|
||||
<dt>--endpoints|-e http://node1:2379,http://node2:2379,http://node3:2379</dt>
|
||||
<dd>Specify HTTP endpoints to connect to</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>--timeout 1000</dt>
|
||||
<dd>Specify request timeout in milliseconds</dd>
|
||||
|
||||
</dl>
|
||||
|
||||
## 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 "--"
|
||||
// Except that persist_filter should be a callback (key, value) => newValue
|
||||
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
|
39
anticli.js
39
anticli.js
|
@ -4,22 +4,6 @@ const fsp = require('fs').promises;
|
|||
const http = require('http');
|
||||
const https = require('https');
|
||||
|
||||
const help_text = `CLI for AntiEtcd
|
||||
(c) Vitaliy Filippov, 2024
|
||||
License: Mozilla Public License 2.0 or Vitastor Network Public License 1.1
|
||||
|
||||
Usage:
|
||||
|
||||
anticli.js [OPTIONS] put <key> [<value>]
|
||||
anticli.js [OPTIONS] get <key> [-p|--prefix] [-v|--print-value-only] [-k|--keys-only]
|
||||
anticli.js [OPTIONS] del <key> [-p|--prefix]
|
||||
|
||||
Options:
|
||||
|
||||
[--endpoints|-e http://node1:2379,http://node2:2379,http://node3:2379]
|
||||
[--cert cert.pem] [--key key.pem] [--timeout 1000]
|
||||
`;
|
||||
|
||||
class AntiEtcdCli
|
||||
{
|
||||
static parse(args)
|
||||
|
@ -31,7 +15,15 @@ class AntiEtcdCli
|
|||
const arg = args[i].toLowerCase().replace(/^--(.+)$/, (m, m1) => '--'+m1.replace(/-/g, '_'));
|
||||
if (arg === '-h' || arg === '--help')
|
||||
{
|
||||
process.stderr.write(help_text);
|
||||
process.stderr.write(
|
||||
'USAGE:\n'+
|
||||
' anticli.js [OPTIONS] put <key> [<value>]\n'+
|
||||
' anticli.js [OPTIONS] get <key> [-p|--prefix] [-v|--print-value-only] [-k|--keys-only]\n'+
|
||||
' anticli.js [OPTIONS] del <key> [-p|--prefix]\n'+
|
||||
'OPTIONS:\n'+
|
||||
' [--endpoints|-e http://node1:2379,http://node2:2379,http://node3:2379]\n'+
|
||||
' [--timeout 1000]'
|
||||
);
|
||||
process.exit();
|
||||
}
|
||||
else if (arg == '-e' || arg == '--endpoints')
|
||||
|
@ -79,13 +71,6 @@ class AntiEtcdCli
|
|||
{
|
||||
this.options.endpoints = [ 'http://localhost:2379' ];
|
||||
}
|
||||
if (this.options.cert && this.options.key)
|
||||
{
|
||||
this.tls = {
|
||||
key: await fsp.readFile(this.options.key),
|
||||
cert: await fsp.readFile(this.options.cert),
|
||||
};
|
||||
}
|
||||
if (cmd[0] == 'get')
|
||||
{
|
||||
await this.get(cmd.slice(1));
|
||||
|
@ -166,7 +151,7 @@ class AntiEtcdCli
|
|||
for (const url of this.options.endpoints)
|
||||
{
|
||||
const cur_url = url.replace(/\/+$/, '')+path;
|
||||
const res = await POST(cur_url, this.tls||{}, body, this.options.timeout||1000);
|
||||
const res = await POST(cur_url, body, this.options.timeout||1000);
|
||||
if (res.json)
|
||||
{
|
||||
if (res.json.error)
|
||||
|
@ -195,7 +180,7 @@ class AntiEtcdCli
|
|||
}
|
||||
}
|
||||
|
||||
function POST(url, options, body, timeout)
|
||||
function POST(url, body, timeout)
|
||||
{
|
||||
return new Promise(ok =>
|
||||
{
|
||||
|
@ -210,7 +195,7 @@ function POST(url, options, body, timeout)
|
|||
let req = (url.substr(0, 6).toLowerCase() == 'https://' ? https : http).request(url, { method: 'POST', headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': body_text.length,
|
||||
}, timeout, ...options }, (res) =>
|
||||
} }, (res) =>
|
||||
{
|
||||
if (!req)
|
||||
{
|
||||
|
|
|
@ -8,10 +8,10 @@ License: Mozilla Public License 2.0 or Vitastor Network Public License 1.1
|
|||
|
||||
Usage:
|
||||
|
||||
${process.argv[0]} ${process.argv[1]} \
|
||||
[--cert ssl.crt] [--key ssl.key] [--port 12379] \
|
||||
[--data data.gz] [--persist-filter ./filter.js] [--persist_interval 500] \
|
||||
[--node_id node1 --cluster_key abcdef --cluster node1=http://localhost:12379,node2=http://localhost:12380,node3=http://localhost:12381] \
|
||||
${process.argv[0]} ${process.argv[1]}
|
||||
[--cert ssl.crt] [--key ssl.key] [--port 12379]
|
||||
[--data data.gz] [--persist-filter ./filter.js] [--persist_interval 500]
|
||||
[--node_id node1 --cluster_key abcdef --cluster node1=http://localhost:12379,node2=http://localhost:12380,node3=http://localhost:12381]
|
||||
[other options]
|
||||
|
||||
Supported etcd REST APIs:
|
||||
|
@ -48,8 +48,6 @@ Persistence:
|
|||
Use persistence filter from ./filter.js (or a module).
|
||||
Persistence filter is a function(cfg) returning function(key, value) ran
|
||||
for every change and returning a new value or undefined to skip persistence.
|
||||
--compact_revisions 1000
|
||||
Number of previous revisions to keep deletion information in memory
|
||||
|
||||
Clustering:
|
||||
|
||||
|
@ -79,6 +77,8 @@ Clustering:
|
|||
Timeout for forwarding requests from follower to leader in milliseconds
|
||||
--replication_timeout 1000
|
||||
Timeout for replicating requests from leader to follower in milliseconds
|
||||
--compact_revisions 1000
|
||||
Number of previous revisions to keep deletion information in memory
|
||||
--compact_timeout 1000
|
||||
Timeout for compaction requests from leader to follower in milliseconds
|
||||
`;
|
||||
|
@ -91,7 +91,7 @@ function parse()
|
|||
const arg = process.argv[i].toLowerCase().replace(/^--(.+)$/, (m, m1) => '--'+m1.replace(/-/g, '_'));
|
||||
if (arg === '-h' || arg === '--help')
|
||||
{
|
||||
process.stderr.write(help_text);
|
||||
console.error(help_text.trim());
|
||||
process.exit();
|
||||
}
|
||||
else if (arg.substr(0, 2) == '--')
|
||||
|
|
|
@ -1,155 +0,0 @@
|
|||
import type { EventEmitter } from 'events';
|
||||
|
||||
import type { TinyRaftEvents } from './tinyraft';
|
||||
|
||||
export type AntiEtcdEvents = {
|
||||
raftchange: TinyRaftEvents['change'],
|
||||
};
|
||||
|
||||
export class AntiEtcd extends EventEmitter<AntiEtcdEvents>
|
||||
{
|
||||
constructor(cfg: object);
|
||||
start(): Promise<void>;
|
||||
stop(): Promise<void>;
|
||||
api(path: 'kv_txn'|'kv_range'|'kv_put'|'kv_deleterange'|'lease_grant'|'lease_revoke'|'lease_keepalive', params: object): Promise<object>;
|
||||
txn(params: TxnRequest): Promise<TxnResponse>;
|
||||
range(params: RangeRequest): Promise<RangeResponse>;
|
||||
put(params: PutRequest): Promise<PutResponse>;
|
||||
deleterange(params: DeleteRangeRequest): Promise<DeleteRangeResponse>;
|
||||
lease_grant(params: LeaseGrantRequest): Promise<LeaseGrantResponse>;
|
||||
lease_revoke(params: LeaseRevokeRequest): Promise<LeaseRevokeResponse>;
|
||||
lease_keepalive(params: LeaseKeepaliveRequest): Promise<LeaseKeepaliveResponse>;
|
||||
create_watch(params: WatchCreateRequest, callback: (ServerMessage) => void): Promise<string|number>;
|
||||
cancel_watch(watch_id: string|number): Promise<void>;
|
||||
}
|
||||
|
||||
export 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,
|
||||
};
|
||||
|
||||
export type TxnResponse = {
|
||||
header: { revision: number },
|
||||
succeeded: boolean,
|
||||
responses: (
|
||||
{ response_put: PutResponse }
|
||||
| { response_range: RangeResponse }
|
||||
| { response_delete_range: DeleteRangeResponse }
|
||||
)[],
|
||||
};
|
||||
|
||||
export type PutRequest = {
|
||||
key: string,
|
||||
value: string,
|
||||
lease?: string,
|
||||
};
|
||||
|
||||
export type PutResponse = {
|
||||
header: { revision: number },
|
||||
};
|
||||
|
||||
export type RangeRequest = {
|
||||
key: string,
|
||||
range_end?: string,
|
||||
keys_only?: boolean,
|
||||
serializable?: boolean,
|
||||
};
|
||||
|
||||
export type RangeResponse = {
|
||||
header: { revision: number },
|
||||
kvs: { key: string }[] | {
|
||||
key: string,
|
||||
value: string,
|
||||
lease?: string,
|
||||
mod_revision: number,
|
||||
}[],
|
||||
};
|
||||
|
||||
export type DeleteRangeRequest = {
|
||||
key: string,
|
||||
range_end?: string,
|
||||
};
|
||||
|
||||
export type DeleteRangeResponse = {
|
||||
header: { revision: number },
|
||||
// number of deleted keys
|
||||
deleted: number,
|
||||
};
|
||||
|
||||
export type LeaseGrantRequest = {
|
||||
ID?: string,
|
||||
TTL: number,
|
||||
};
|
||||
|
||||
export type LeaseGrantResponse = {
|
||||
header: { revision: number },
|
||||
ID: string,
|
||||
TTL: number,
|
||||
};
|
||||
|
||||
export type LeaseKeepaliveRequest = {
|
||||
ID: string,
|
||||
};
|
||||
|
||||
export type LeaseKeepaliveResponse = {
|
||||
result: {
|
||||
header: { revision: number },
|
||||
ID: string,
|
||||
TTL: number,
|
||||
}
|
||||
};
|
||||
|
||||
export type LeaseRevokeRequest = {
|
||||
ID: string,
|
||||
};
|
||||
|
||||
export type LeaseRevokeResponse = {
|
||||
header: { revision: number },
|
||||
};
|
||||
|
||||
export type WatchCreateRequest = {
|
||||
key: string,
|
||||
range_end?: string,
|
||||
start_revision?: number,
|
||||
watch_id?: string|number,
|
||||
}
|
||||
|
||||
export type ClientMessage =
|
||||
{ create_request: WatchCreateRequest }
|
||||
| { cancel_request: { watch_id: string } }
|
||||
| { progress_request: {} };
|
||||
|
||||
export type ServerMessage = {
|
||||
result: {
|
||||
header: { revision: number },
|
||||
watch_id: string|number,
|
||||
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' };
|
35
antietcd.js
35
antietcd.js
|
@ -29,7 +29,6 @@ class AntiEtcd extends EventEmitter
|
|||
this.stopped = false;
|
||||
this.inflight = 0;
|
||||
this.wait_inflight = [];
|
||||
this.api_watches = {};
|
||||
}
|
||||
|
||||
async start()
|
||||
|
@ -125,16 +124,6 @@ class AntiEtcd extends EventEmitter
|
|||
}));
|
||||
});
|
||||
}
|
||||
if (!this.cluster)
|
||||
{
|
||||
// Run deletion compaction without followers
|
||||
const mod_revision = this.antietcd.etctree.mod_revision;
|
||||
if (mod_revision - this.antietcd.etctree.compact_revision > (this.cfg.compact_revisions||1000)*2)
|
||||
{
|
||||
const revision = mod_revision - (this.cfg.compact_revisions||1000);
|
||||
this.antietcd.etctree.compact(revision);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_handleRequest(req, res)
|
||||
|
@ -404,30 +393,6 @@ class AntiEtcd extends EventEmitter
|
|||
return await this.api('lease_keepalive', params);
|
||||
}
|
||||
|
||||
// public watch API
|
||||
async create_watch(params, callback)
|
||||
{
|
||||
const watch = this.etctree.api_create_watch({ ...params, watch_id: null }, callback);
|
||||
if (!watch.created)
|
||||
{
|
||||
throw new RequestError(400, 'Requested watch revision is compacted', { compact_revision: watch.compact_revision });
|
||||
}
|
||||
const watch_id = params.watch_id || watch.watch_id;
|
||||
this.api_watches[watch_id] = watch.watch_id;
|
||||
return watch_id;
|
||||
}
|
||||
|
||||
async cancel_watch(watch_id)
|
||||
{
|
||||
const mapped_id = this.api_watches[watch_id];
|
||||
if (!mapped_id)
|
||||
{
|
||||
throw new RequestError(400, 'Watch not found');
|
||||
}
|
||||
this.etctree.api_cancel_watch({ watch_id: mapped_id });
|
||||
delete this.api_watches[watch_id];
|
||||
}
|
||||
|
||||
// internal handlers
|
||||
async _handle_kv_txn(data)
|
||||
{
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
class RequestError
|
||||
{
|
||||
constructor(code, text, details)
|
||||
constructor(code, text)
|
||||
{
|
||||
this.code = code;
|
||||
this.message = text;
|
||||
this.details = details;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,36 +0,0 @@
|
|||
import type { EventEmitter } from 'events';
|
||||
|
||||
export type TinyRaftEvents = {
|
||||
change: {
|
||||
state: 'LEADER'|'FOLLOWER'|'CANDIDATE',
|
||||
term: number,
|
||||
leader: string|number,
|
||||
followers: (string|number)[],
|
||||
}[],
|
||||
};
|
||||
|
||||
export type RaftMessage = {
|
||||
type: 'VOTE_REQUEST'|'VOTE'|'PING'|'PONG',
|
||||
term: number,
|
||||
leader?: string|number,
|
||||
priority?: number,
|
||||
};
|
||||
|
||||
export class TinyRaft extends EventEmitter<TinyRaftEvents>
|
||||
{
|
||||
constructor(cfg: {
|
||||
nodes: (number|string)[],
|
||||
nodeId: number|string,
|
||||
electionTimeout?: number,
|
||||
heartbeatTimeout?: number,
|
||||
leadershipTimeout?: number,
|
||||
initialTerm?: number,
|
||||
leaderPriority?: number,
|
||||
send: (to: number|string, msg: RaftMessage) => void,
|
||||
});
|
||||
start();
|
||||
stop();
|
||||
onReceive(from: number|string, msg: RaftMessage);
|
||||
markAlive();
|
||||
setNodes(nodes: (number|string)[]);
|
||||
}
|
|
@ -177,7 +177,7 @@ class TinyRaft extends EventEmitter
|
|||
}
|
||||
}
|
||||
const prio = (this.leader == this.nodeId ? this.leaderPriority : undefined);
|
||||
this.send(from, { type: VOTE, term: this.term, leader: this.leader, priority: prio });
|
||||
this.send(from, { type: VOTE, term: this.term, leader: this.leader, leaderPriority: prio });
|
||||
}
|
||||
|
||||
_onReceiveVote(from, msg)
|
||||
|
|
Loading…
Reference in New Issue