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/antietcd.d.ts
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import type { EventEmitter } from 'events';
|
|
2
|
+
|
|
3
|
+
import type { TinyRaftEvents } from './tinyraft';
|
|
4
|
+
|
|
5
|
+
export type AntiEtcdEvents = {
|
|
6
|
+
raftchange: TinyRaftEvents['change'],
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export class AntiEtcd extends EventEmitter<AntiEtcdEvents>
|
|
10
|
+
{
|
|
11
|
+
constructor(cfg: object);
|
|
12
|
+
start(): Promise<void>;
|
|
13
|
+
stop(): Promise<void>;
|
|
14
|
+
api(path: 'kv_txn'|'kv_range'|'kv_put'|'kv_deleterange'|'lease_grant'|'lease_revoke'|'lease_keepalive', params: object): Promise<object>;
|
|
15
|
+
txn(params: TxnRequest): Promise<TxnResponse>;
|
|
16
|
+
range(params: RangeRequest): Promise<RangeResponse>;
|
|
17
|
+
put(params: PutRequest): Promise<PutResponse>;
|
|
18
|
+
deleterange(params: DeleteRangeRequest): Promise<DeleteRangeResponse>;
|
|
19
|
+
lease_grant(params: LeaseGrantRequest): Promise<LeaseGrantResponse>;
|
|
20
|
+
lease_revoke(params: LeaseRevokeRequest): Promise<LeaseRevokeResponse>;
|
|
21
|
+
lease_keepalive(params: LeaseKeepaliveRequest): Promise<LeaseKeepaliveResponse>;
|
|
22
|
+
create_watch(params: WatchCreateRequest, callback: (ServerMessage) => void): Promise<string|number>;
|
|
23
|
+
cancel_watch(watch_id: string|number): Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type TxnRequest = {
|
|
27
|
+
compare?: (
|
|
28
|
+
{ key: string, target: "MOD", mod_revision: number, result?: "LESS" }
|
|
29
|
+
| { key: string, target: "CREATE", create_revision: number, result?: "LESS" }
|
|
30
|
+
| { key: string, target: "VERSION", version: number, result?: "LESS" }
|
|
31
|
+
| { key: string, target: "LEASE", lease: string, result?: "LESS" }
|
|
32
|
+
| { key: string, target: "VALUE", value: string }
|
|
33
|
+
)[],
|
|
34
|
+
success?: (
|
|
35
|
+
{ request_put: PutRequest }
|
|
36
|
+
| { request_range: RangeRequest }
|
|
37
|
+
| { request_delete_range: DeleteRangeRequest }
|
|
38
|
+
)[],
|
|
39
|
+
failure?: (
|
|
40
|
+
{ request_put: PutRequest }
|
|
41
|
+
| { request_range: RangeRequest }
|
|
42
|
+
| { request_delete_range: DeleteRangeRequest }
|
|
43
|
+
)[],
|
|
44
|
+
serializable?: boolean,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export type TxnResponse = {
|
|
48
|
+
header: { revision: number },
|
|
49
|
+
succeeded: boolean,
|
|
50
|
+
responses: (
|
|
51
|
+
{ response_put: PutResponse }
|
|
52
|
+
| { response_range: RangeResponse }
|
|
53
|
+
| { response_delete_range: DeleteRangeResponse }
|
|
54
|
+
)[],
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export type PutRequest = {
|
|
58
|
+
key: string,
|
|
59
|
+
value: string,
|
|
60
|
+
lease?: string,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export type PutResponse = {
|
|
64
|
+
header: { revision: number },
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export type RangeRequest = {
|
|
68
|
+
key: string,
|
|
69
|
+
range_end?: string,
|
|
70
|
+
keys_only?: boolean,
|
|
71
|
+
serializable?: boolean,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export type RangeResponse = {
|
|
75
|
+
header: { revision: number },
|
|
76
|
+
kvs: { key: string }[] | {
|
|
77
|
+
key: string,
|
|
78
|
+
value: string,
|
|
79
|
+
lease?: string,
|
|
80
|
+
mod_revision: number,
|
|
81
|
+
}[],
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export type DeleteRangeRequest = {
|
|
85
|
+
key: string,
|
|
86
|
+
range_end?: string,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export type DeleteRangeResponse = {
|
|
90
|
+
header: { revision: number },
|
|
91
|
+
// number of deleted keys
|
|
92
|
+
deleted: number,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export type LeaseGrantRequest = {
|
|
96
|
+
ID?: string,
|
|
97
|
+
TTL: number,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export type LeaseGrantResponse = {
|
|
101
|
+
header: { revision: number },
|
|
102
|
+
ID: string,
|
|
103
|
+
TTL: number,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export type LeaseKeepaliveRequest = {
|
|
107
|
+
ID: string,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
export type LeaseKeepaliveResponse = {
|
|
111
|
+
result: {
|
|
112
|
+
header: { revision: number },
|
|
113
|
+
ID: string,
|
|
114
|
+
TTL: number,
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
export type LeaseRevokeRequest = {
|
|
119
|
+
ID: string,
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export type LeaseRevokeResponse = {
|
|
123
|
+
header: { revision: number },
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export type WatchCreateRequest = {
|
|
127
|
+
key: string,
|
|
128
|
+
range_end?: string,
|
|
129
|
+
start_revision?: number,
|
|
130
|
+
watch_id?: string|number,
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export type ClientMessage =
|
|
134
|
+
{ create_request: WatchCreateRequest }
|
|
135
|
+
| { cancel_request: { watch_id: string } }
|
|
136
|
+
| { progress_request: {} };
|
|
137
|
+
|
|
138
|
+
export type ServerMessage = {
|
|
139
|
+
result: {
|
|
140
|
+
header: { revision: number },
|
|
141
|
+
watch_id: string|number,
|
|
142
|
+
created?: boolean,
|
|
143
|
+
canceled?: boolean,
|
|
144
|
+
compact_revision?: number,
|
|
145
|
+
events?: {
|
|
146
|
+
type: 'PUT'|'DELETE',
|
|
147
|
+
kv: {
|
|
148
|
+
key: string,
|
|
149
|
+
value: string,
|
|
150
|
+
lease?: string,
|
|
151
|
+
mod_revision: number,
|
|
152
|
+
},
|
|
153
|
+
}[],
|
|
154
|
+
}
|
|
155
|
+
} | { error: 'bad-json' } | { error: 'empty-message' };
|
package/antietcd.js
ADDED
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
#!/usr/bin/node
|
|
2
|
+
|
|
3
|
+
// AntiEtcd embeddable class
|
|
4
|
+
// (c) Vitaliy Filippov, 2024
|
|
5
|
+
// License: Mozilla Public License 2.0 or Vitastor Network Public License 1.1
|
|
6
|
+
|
|
7
|
+
const fsp = require('fs').promises;
|
|
8
|
+
const { URL } = require('url');
|
|
9
|
+
const http = require('http');
|
|
10
|
+
const https = require('https');
|
|
11
|
+
const EventEmitter = require('events');
|
|
12
|
+
|
|
13
|
+
const ws = require('ws');
|
|
14
|
+
|
|
15
|
+
const EtcTree = require('./etctree.js');
|
|
16
|
+
const AntiPersistence = require('./antipersistence.js');
|
|
17
|
+
const AntiCluster = require('./anticluster.js');
|
|
18
|
+
const { runCallbacks, RequestError } = require('./common.js');
|
|
19
|
+
|
|
20
|
+
class AntiEtcd extends EventEmitter
|
|
21
|
+
{
|
|
22
|
+
constructor(cfg)
|
|
23
|
+
{
|
|
24
|
+
super();
|
|
25
|
+
this.clients = {};
|
|
26
|
+
this.client_id = 1;
|
|
27
|
+
this.etctree = new EtcTree(true);
|
|
28
|
+
this.persistence = null;
|
|
29
|
+
this.cluster = null;
|
|
30
|
+
this.stored_term = 0;
|
|
31
|
+
this.cfg = cfg;
|
|
32
|
+
this.loading = false;
|
|
33
|
+
this.stopped = false;
|
|
34
|
+
this.inflight = 0;
|
|
35
|
+
this.wait_inflight = [];
|
|
36
|
+
this.api_watches = {};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async start()
|
|
40
|
+
{
|
|
41
|
+
if (this.cfg.data || this.cfg.cluster)
|
|
42
|
+
{
|
|
43
|
+
this.etctree.set_replicate_watcher(msg => this._persistAndReplicate(msg));
|
|
44
|
+
}
|
|
45
|
+
if (this.cfg.data)
|
|
46
|
+
{
|
|
47
|
+
this.persistence = new AntiPersistence(this);
|
|
48
|
+
// Load data from disk
|
|
49
|
+
await this.persistence.load();
|
|
50
|
+
}
|
|
51
|
+
if (this.cfg.cluster)
|
|
52
|
+
{
|
|
53
|
+
this.cluster = new AntiCluster(this);
|
|
54
|
+
}
|
|
55
|
+
if (this.cfg.cert)
|
|
56
|
+
{
|
|
57
|
+
this.tls = {
|
|
58
|
+
key: await fsp.readFile(this.cfg.key),
|
|
59
|
+
cert: await fsp.readFile(this.cfg.cert),
|
|
60
|
+
};
|
|
61
|
+
if (this.cfg.ca)
|
|
62
|
+
{
|
|
63
|
+
this.tls.ca = await fsp.readFile(this.cfg.ca);
|
|
64
|
+
}
|
|
65
|
+
if (this.cfg.client_cert_auth)
|
|
66
|
+
{
|
|
67
|
+
this.tls.requestCert = true;
|
|
68
|
+
}
|
|
69
|
+
this.server = https.createServer(this.tls, (req, res) => this._handleRequest(req, res));
|
|
70
|
+
}
|
|
71
|
+
else
|
|
72
|
+
{
|
|
73
|
+
this.server = http.createServer((req, res) => this._handleRequest(req, res));
|
|
74
|
+
}
|
|
75
|
+
this.wss = new ws.WebSocketServer({ server: this.server });
|
|
76
|
+
// eslint-disable-next-line no-unused-vars
|
|
77
|
+
this.wss.on('connection', (conn, req) => this._startWebsocket(conn, null));
|
|
78
|
+
this.server.listen(this.cfg.port || 2379);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async stop()
|
|
82
|
+
{
|
|
83
|
+
if (this.stopped)
|
|
84
|
+
{
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
this.stopped = true;
|
|
88
|
+
// Wait until all requests complete
|
|
89
|
+
while (this.inflight > 0)
|
|
90
|
+
{
|
|
91
|
+
await new Promise(ok => this.wait_inflight.push(ok));
|
|
92
|
+
}
|
|
93
|
+
if (this.persistence)
|
|
94
|
+
{
|
|
95
|
+
await this.persistence.persist();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async _persistAndReplicate(msg)
|
|
100
|
+
{
|
|
101
|
+
let res = [];
|
|
102
|
+
if (this.cluster)
|
|
103
|
+
{
|
|
104
|
+
// We have to guarantee that replication is processed sequentially
|
|
105
|
+
// So we have to send messages without first awaiting for anything!
|
|
106
|
+
res.push(this.cluster.replicateChange(msg));
|
|
107
|
+
}
|
|
108
|
+
if (this.persistence)
|
|
109
|
+
{
|
|
110
|
+
res.push(this.persistence.persistChange(msg));
|
|
111
|
+
}
|
|
112
|
+
if (res.length == 1)
|
|
113
|
+
{
|
|
114
|
+
await res[0];
|
|
115
|
+
}
|
|
116
|
+
else if (res.length > 0)
|
|
117
|
+
{
|
|
118
|
+
let done = 0;
|
|
119
|
+
await new Promise((allOk, allNo) =>
|
|
120
|
+
{
|
|
121
|
+
res.map(promise => promise.then(res =>
|
|
122
|
+
{
|
|
123
|
+
if ((++done) == res.length)
|
|
124
|
+
allOk();
|
|
125
|
+
}).catch(e =>
|
|
126
|
+
{
|
|
127
|
+
console.error(e);
|
|
128
|
+
allNo(e);
|
|
129
|
+
}));
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
if (!this.cluster)
|
|
133
|
+
{
|
|
134
|
+
// Run deletion compaction without followers
|
|
135
|
+
const mod_revision = this.antietcd.etctree.mod_revision;
|
|
136
|
+
if (mod_revision - this.antietcd.etctree.compact_revision > (this.cfg.compact_revisions||1000)*2)
|
|
137
|
+
{
|
|
138
|
+
const revision = mod_revision - (this.cfg.compact_revisions||1000);
|
|
139
|
+
this.antietcd.etctree.compact(revision);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
_handleRequest(req, res)
|
|
145
|
+
{
|
|
146
|
+
let data = [];
|
|
147
|
+
req.on('data', (chunk) => data.push(chunk));
|
|
148
|
+
req.on('end', async () =>
|
|
149
|
+
{
|
|
150
|
+
this.inflight++;
|
|
151
|
+
data = Buffer.concat(data);
|
|
152
|
+
let body = '';
|
|
153
|
+
let code = 200;
|
|
154
|
+
let ctype = 'text/plain; charset=utf-8';
|
|
155
|
+
let reply;
|
|
156
|
+
try
|
|
157
|
+
{
|
|
158
|
+
if (req.headers['content-type'] != 'application/json')
|
|
159
|
+
{
|
|
160
|
+
throw new RequestError(400, 'content-type should be application/json');
|
|
161
|
+
}
|
|
162
|
+
body = data.toString();
|
|
163
|
+
try
|
|
164
|
+
{
|
|
165
|
+
data = data.length ? JSON.parse(data) : {};
|
|
166
|
+
}
|
|
167
|
+
catch (e)
|
|
168
|
+
{
|
|
169
|
+
throw new RequestError(400, 'body should be valid JSON');
|
|
170
|
+
}
|
|
171
|
+
if (!(data instanceof Object) || data instanceof Array)
|
|
172
|
+
{
|
|
173
|
+
throw new RequestError(400, 'body should be JSON object');
|
|
174
|
+
}
|
|
175
|
+
reply = await this._runHandler(req, data);
|
|
176
|
+
reply = JSON.stringify(reply);
|
|
177
|
+
ctype = 'application/json';
|
|
178
|
+
}
|
|
179
|
+
catch (e)
|
|
180
|
+
{
|
|
181
|
+
if (e instanceof RequestError)
|
|
182
|
+
{
|
|
183
|
+
code = e.code;
|
|
184
|
+
reply = e.message;
|
|
185
|
+
}
|
|
186
|
+
else
|
|
187
|
+
{
|
|
188
|
+
console.error(e);
|
|
189
|
+
code = 500;
|
|
190
|
+
reply = 'Internal error: '+e.message;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
try
|
|
194
|
+
{
|
|
195
|
+
// Access log
|
|
196
|
+
if (this.cfg.log_level > 1)
|
|
197
|
+
{
|
|
198
|
+
console.log(
|
|
199
|
+
new Date().toISOString()+
|
|
200
|
+
' '+(req.headers['x-forwarded-for'] || (req.socket.remoteAddress + ':' + req.socket.remotePort))+
|
|
201
|
+
' '+req.method+' '+req.url+' '+code+'\n '+body.replace(/\n/g, '\\n')+
|
|
202
|
+
'\n '+reply.replace(/\n/g, '\\n')
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
reply = Buffer.from(reply);
|
|
206
|
+
res.writeHead(code, {
|
|
207
|
+
'Content-Type': ctype,
|
|
208
|
+
'Content-Length': reply.length,
|
|
209
|
+
});
|
|
210
|
+
res.write(reply);
|
|
211
|
+
res.end();
|
|
212
|
+
}
|
|
213
|
+
catch (e)
|
|
214
|
+
{
|
|
215
|
+
console.error(e);
|
|
216
|
+
}
|
|
217
|
+
this.inflight--;
|
|
218
|
+
if (!this.inflight)
|
|
219
|
+
{
|
|
220
|
+
runCallbacks(this, 'wait_inflight', []);
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
_startWebsocket(socket, reconnect)
|
|
226
|
+
{
|
|
227
|
+
const client_id = this.client_id++;
|
|
228
|
+
this.clients[client_id] = {
|
|
229
|
+
id: client_id,
|
|
230
|
+
addr: socket._socket ? socket._socket.remoteAddress+':'+socket._socket.remotePort : '',
|
|
231
|
+
socket,
|
|
232
|
+
alive: true,
|
|
233
|
+
watches: {},
|
|
234
|
+
};
|
|
235
|
+
socket.on('pong', () => this.clients[client_id].alive = true);
|
|
236
|
+
socket.on('error', e => console.error(e.syscall === 'connect' ? e.message : e));
|
|
237
|
+
const pinger = setInterval(() =>
|
|
238
|
+
{
|
|
239
|
+
if (!this.clients[client_id])
|
|
240
|
+
{
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (!this.clients[client_id].alive)
|
|
244
|
+
{
|
|
245
|
+
return socket.terminate();
|
|
246
|
+
}
|
|
247
|
+
this.clients[client_id].alive = false;
|
|
248
|
+
socket.ping(() => {});
|
|
249
|
+
}, this.cfg.ws_keepalive_interval||30000);
|
|
250
|
+
socket.on('message', (msg) =>
|
|
251
|
+
{
|
|
252
|
+
try
|
|
253
|
+
{
|
|
254
|
+
msg = JSON.parse(msg);
|
|
255
|
+
}
|
|
256
|
+
catch (e)
|
|
257
|
+
{
|
|
258
|
+
socket.send(JSON.stringify({ error: 'bad-json' }));
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
if (!msg)
|
|
262
|
+
{
|
|
263
|
+
socket.send(JSON.stringify({ error: 'empty-message' }));
|
|
264
|
+
}
|
|
265
|
+
else
|
|
266
|
+
{
|
|
267
|
+
this._handleMessage(client_id, msg, socket);
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
socket.on('close', () =>
|
|
271
|
+
{
|
|
272
|
+
this._unsubscribeClient(client_id);
|
|
273
|
+
clearInterval(pinger);
|
|
274
|
+
delete this.clients[client_id];
|
|
275
|
+
socket.terminate();
|
|
276
|
+
if (reconnect)
|
|
277
|
+
{
|
|
278
|
+
reconnect();
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
return client_id;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async _runHandler(req, data)
|
|
285
|
+
{
|
|
286
|
+
// v3/kv/txn
|
|
287
|
+
// v3/kv/range
|
|
288
|
+
// v3/kv/put
|
|
289
|
+
// v3/kv/deleterange
|
|
290
|
+
// v3/lease/grant
|
|
291
|
+
// v3/lease/keepalive
|
|
292
|
+
// v3/lease/revoke O_o
|
|
293
|
+
// v3/kv/lease/revoke O_o
|
|
294
|
+
const requestUrl = new URL(req.url, 'http://'+(req.headers.host || 'localhost'));
|
|
295
|
+
if (requestUrl.searchParams.get('leaderonly'))
|
|
296
|
+
{
|
|
297
|
+
data.leaderonly = true;
|
|
298
|
+
}
|
|
299
|
+
if (requestUrl.searchParams.get('serializable'))
|
|
300
|
+
{
|
|
301
|
+
data.serializable = true;
|
|
302
|
+
}
|
|
303
|
+
if (requestUrl.searchParams.get('nowaitquorum'))
|
|
304
|
+
{
|
|
305
|
+
data.nowaitquorum = true;
|
|
306
|
+
}
|
|
307
|
+
try
|
|
308
|
+
{
|
|
309
|
+
if (requestUrl.pathname.substr(0, 4) == '/v3/')
|
|
310
|
+
{
|
|
311
|
+
const path = requestUrl.pathname.substr(4).replace(/\/+$/, '').replace(/\/+/g, '_');
|
|
312
|
+
if (req.method != 'POST')
|
|
313
|
+
{
|
|
314
|
+
throw new RequestError(405, 'Please use POST method');
|
|
315
|
+
}
|
|
316
|
+
return await this.api(path, data);
|
|
317
|
+
}
|
|
318
|
+
else if (requestUrl.pathname == '/dump')
|
|
319
|
+
{
|
|
320
|
+
return await this.api('dump', data);
|
|
321
|
+
}
|
|
322
|
+
else
|
|
323
|
+
{
|
|
324
|
+
throw new RequestError(404, '');
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
catch (e)
|
|
328
|
+
{
|
|
329
|
+
if ((e instanceof RequestError) && e.code == 404)
|
|
330
|
+
{
|
|
331
|
+
throw new RequestError(404, 'Supported APIs: /v3/kv/txn, /v3/kv/range, /v3/kv/put, /v3/kv/deleterange, '+
|
|
332
|
+
'/v3/lease/grant, /v3/lease/revoke, /v3/kv/lease/revoke, /v3/lease/keepalive');
|
|
333
|
+
}
|
|
334
|
+
else
|
|
335
|
+
{
|
|
336
|
+
throw e;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// public generic handler
|
|
342
|
+
async api(path, data)
|
|
343
|
+
{
|
|
344
|
+
if (this.stopped)
|
|
345
|
+
{
|
|
346
|
+
throw new RequestError(502, 'Server is stopping');
|
|
347
|
+
}
|
|
348
|
+
if (path !== 'dump' && this.cluster)
|
|
349
|
+
{
|
|
350
|
+
const res = await this.cluster.checkRaftState(
|
|
351
|
+
path,
|
|
352
|
+
(data.leaderonly ? AntiCluster.LEADER_ONLY : 0) |
|
|
353
|
+
(data.serializable ? AntiCluster.READ_FROM_FOLLOWER : 0) |
|
|
354
|
+
(data.nowaitquorum ? AntiCluster.NO_WAIT_QUORUM : 0),
|
|
355
|
+
data
|
|
356
|
+
);
|
|
357
|
+
if (res)
|
|
358
|
+
{
|
|
359
|
+
return res;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
const cb = this['_handle_'+path];
|
|
363
|
+
if (cb)
|
|
364
|
+
{
|
|
365
|
+
const res = cb.call(this, data);
|
|
366
|
+
if (res instanceof Promise)
|
|
367
|
+
{
|
|
368
|
+
return await res;
|
|
369
|
+
}
|
|
370
|
+
return res;
|
|
371
|
+
}
|
|
372
|
+
throw new RequestError(404, 'Unsupported API');
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// public wrappers
|
|
376
|
+
async txn(params)
|
|
377
|
+
{
|
|
378
|
+
return await this.api('kv_txn', params);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
async range(params)
|
|
382
|
+
{
|
|
383
|
+
return await this.api('kv_range', params);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
async put(params)
|
|
387
|
+
{
|
|
388
|
+
return await this.api('kv_put', params);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async deleterange(params)
|
|
392
|
+
{
|
|
393
|
+
return await this.api('kv_deleterange', params);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
async lease_grant(params)
|
|
397
|
+
{
|
|
398
|
+
return await this.api('lease_grant', params);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
async lease_revoke(params)
|
|
402
|
+
{
|
|
403
|
+
return await this.api('lease_revoke', params);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
async lease_keepalive(params)
|
|
407
|
+
{
|
|
408
|
+
return await this.api('lease_keepalive', params);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// public watch API
|
|
412
|
+
async create_watch(params, callback)
|
|
413
|
+
{
|
|
414
|
+
const watch = this.etctree.api_create_watch({ ...params, watch_id: null }, callback);
|
|
415
|
+
if (!watch.created)
|
|
416
|
+
{
|
|
417
|
+
throw new RequestError(400, 'Requested watch revision is compacted', { compact_revision: watch.compact_revision });
|
|
418
|
+
}
|
|
419
|
+
const watch_id = params.watch_id || watch.watch_id;
|
|
420
|
+
this.api_watches[watch_id] = watch.watch_id;
|
|
421
|
+
return watch_id;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
async cancel_watch(watch_id)
|
|
425
|
+
{
|
|
426
|
+
const mapped_id = this.api_watches[watch_id];
|
|
427
|
+
if (!mapped_id)
|
|
428
|
+
{
|
|
429
|
+
throw new RequestError(400, 'Watch not found');
|
|
430
|
+
}
|
|
431
|
+
this.etctree.api_cancel_watch({ watch_id: mapped_id });
|
|
432
|
+
delete this.api_watches[watch_id];
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// internal handlers
|
|
436
|
+
async _handle_kv_txn(data)
|
|
437
|
+
{
|
|
438
|
+
return await this.etctree.api_txn(data);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
async _handle_kv_range(data)
|
|
442
|
+
{
|
|
443
|
+
const r = await this.etctree.api_txn({ success: [ { request_range: data } ] });
|
|
444
|
+
return { header: r.header, ...r.responses[0].response_range };
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
async _handle_kv_put(data)
|
|
448
|
+
{
|
|
449
|
+
const r = await this.etctree.api_txn({ success: [ { request_put: data } ] });
|
|
450
|
+
return { header: r.header, ...r.responses[0].response_put };
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
async _handle_kv_deleterange(data)
|
|
454
|
+
{
|
|
455
|
+
const r = await this.etctree.api_txn({ success: [ { request_delete_range: data } ] });
|
|
456
|
+
return { header: r.header, ...r.responses[0].response_delete_range };
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
_handle_lease_grant(data)
|
|
460
|
+
{
|
|
461
|
+
return this.etctree.api_grant_lease(data);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
_handle_lease_revoke(data)
|
|
465
|
+
{
|
|
466
|
+
return this.etctree.api_revoke_lease(data);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
_handle_kv_lease_revoke(data)
|
|
470
|
+
{
|
|
471
|
+
return this.etctree.api_revoke_lease(data);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
_handle_lease_keepalive(data)
|
|
475
|
+
{
|
|
476
|
+
return this.etctree.api_keepalive_lease(data);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// eslint-disable-next-line no-unused-vars
|
|
480
|
+
_handle_dump(data)
|
|
481
|
+
{
|
|
482
|
+
return { ...this.etctree.dump(), term: this.stored_term };
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
_handleMessage(client_id, msg, socket)
|
|
486
|
+
{
|
|
487
|
+
const client = this.clients[client_id];
|
|
488
|
+
if (this.cfg.access_log)
|
|
489
|
+
{
|
|
490
|
+
console.log(new Date().toISOString()+' '+client.addr+' '+(client.raft_node_id || '-')+' -> '+JSON.stringify(msg));
|
|
491
|
+
}
|
|
492
|
+
if (msg.create_request)
|
|
493
|
+
{
|
|
494
|
+
const create_request = msg.create_request;
|
|
495
|
+
if (!create_request.watch_id || !client.watches[create_request.watch_id])
|
|
496
|
+
{
|
|
497
|
+
const watch = this.etctree.api_create_watch(
|
|
498
|
+
{ ...create_request, watch_id: null }, (msg) => socket.send(JSON.stringify(msg))
|
|
499
|
+
);
|
|
500
|
+
if (!watch.created)
|
|
501
|
+
{
|
|
502
|
+
socket.send(JSON.stringify({ result: { header: { revision: this.etctree.mod_revision }, watch_id: create_request.watch_id, ...watch } }));
|
|
503
|
+
}
|
|
504
|
+
else
|
|
505
|
+
{
|
|
506
|
+
create_request.watch_id = create_request.watch_id || watch.watch_id;
|
|
507
|
+
client.watches[create_request.watch_id] = watch.watch_id;
|
|
508
|
+
socket.send(JSON.stringify({ result: { header: { revision: this.etctree.mod_revision }, watch_id: create_request.watch_id, created: true } }));
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
else if (msg.cancel_request)
|
|
513
|
+
{
|
|
514
|
+
const mapped_id = client.watches[msg.cancel_request.watch_id];
|
|
515
|
+
if (mapped_id)
|
|
516
|
+
{
|
|
517
|
+
this.etctree.api_cancel_watch({ watch_id: mapped_id });
|
|
518
|
+
delete client.watches[msg.cancel_request.watch_id];
|
|
519
|
+
socket.send(JSON.stringify({ result: { header: { revision: this.etctree.mod_revision }, watch_id: msg.cancel_request.watch_id, canceled: true } }));
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
else if (msg.progress_request)
|
|
523
|
+
{
|
|
524
|
+
socket.send(JSON.stringify({ result: { header: { revision: this.etctree.mod_revision } } }));
|
|
525
|
+
}
|
|
526
|
+
else
|
|
527
|
+
{
|
|
528
|
+
if (!this.cluster)
|
|
529
|
+
{
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
this.cluster.handleWsMsg(client, msg);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
_unsubscribeClient(client_id)
|
|
537
|
+
{
|
|
538
|
+
if (!this.clients[client_id])
|
|
539
|
+
{
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
for (const watch_id in this.clients[client_id].watches)
|
|
543
|
+
{
|
|
544
|
+
const mapped_id = this.clients[client_id].watches[watch_id];
|
|
545
|
+
this.etctree.api_cancel_watch({ watch_id: mapped_id });
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
AntiEtcd.RequestError = RequestError;
|
|
551
|
+
|
|
552
|
+
module.exports = AntiEtcd;
|