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
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// Persistence for AntiEtcd
|
|
2
|
+
// (c) Vitaliy Filippov, 2024
|
|
3
|
+
// License: Mozilla Public License 2.0 or Vitastor Network Public License 1.1
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const fsp = require('fs').promises;
|
|
7
|
+
const zlib = require('zlib');
|
|
8
|
+
|
|
9
|
+
const stableStringify = require('./stable-stringify.js');
|
|
10
|
+
const EtcTree = require('./etctree.js');
|
|
11
|
+
const { de64, runCallbacks } = require('./common.js');
|
|
12
|
+
|
|
13
|
+
class AntiPersistence
|
|
14
|
+
{
|
|
15
|
+
constructor(antietcd)
|
|
16
|
+
{
|
|
17
|
+
this.cfg = antietcd.cfg;
|
|
18
|
+
this.antietcd = antietcd;
|
|
19
|
+
this.prev_value = {};
|
|
20
|
+
this.persist_timer = null;
|
|
21
|
+
this.wait_persist = null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async load()
|
|
25
|
+
{
|
|
26
|
+
// eslint-disable-next-line no-unused-vars
|
|
27
|
+
const [ err, stat ] = await new Promise(ok => fs.stat(this.cfg.data, (err, stat) => ok([ err, stat ])));
|
|
28
|
+
if (!err)
|
|
29
|
+
{
|
|
30
|
+
let data = await fsp.readFile(this.cfg.data);
|
|
31
|
+
data = await new Promise((ok, no) => zlib.gunzip(data, (err, res) => err ? no(err) : ok(res)));
|
|
32
|
+
data = JSON.parse(data);
|
|
33
|
+
this.loading = true;
|
|
34
|
+
this.antietcd.etctree.load(data);
|
|
35
|
+
this.loading = false;
|
|
36
|
+
this.antietcd.stored_term = data['term'] || 0;
|
|
37
|
+
}
|
|
38
|
+
else if (err.code != 'ENOENT')
|
|
39
|
+
{
|
|
40
|
+
throw err;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async persistChange(msg)
|
|
45
|
+
{
|
|
46
|
+
if (this.loading)
|
|
47
|
+
{
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (!msg.events || !msg.events.length)
|
|
51
|
+
{
|
|
52
|
+
// lease-only changes don't need to be persisted
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (this.cfg.persist_filter)
|
|
56
|
+
{
|
|
57
|
+
let changed = false;
|
|
58
|
+
for (const ev of msg.events)
|
|
59
|
+
{
|
|
60
|
+
if (ev.lease)
|
|
61
|
+
{
|
|
62
|
+
// Values with lease are never persisted
|
|
63
|
+
const key = de64(ev.key);
|
|
64
|
+
if (this.prev_value[key] !== undefined)
|
|
65
|
+
{
|
|
66
|
+
delete this.prev_value[key];
|
|
67
|
+
changed = true;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
else
|
|
71
|
+
{
|
|
72
|
+
const key = de64(ev.key);
|
|
73
|
+
const filtered = this.cfg.persist_filter(key, ev.value == null ? undefined : de64(ev.value));
|
|
74
|
+
if (!EtcTree.eq(filtered, this.prev_value[key]))
|
|
75
|
+
{
|
|
76
|
+
this.prev_value[key] = filtered;
|
|
77
|
+
changed = true;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (!changed)
|
|
82
|
+
{
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
await this.schedulePersist();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async schedulePersist()
|
|
90
|
+
{
|
|
91
|
+
if (!this.cfg.persist_interval)
|
|
92
|
+
{
|
|
93
|
+
await this.persist();
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (!this.persist_timer)
|
|
97
|
+
{
|
|
98
|
+
this.persist_timer = setTimeout(() =>
|
|
99
|
+
{
|
|
100
|
+
this.persist_timer = null;
|
|
101
|
+
this.persist().catch(console.error);
|
|
102
|
+
}, this.cfg.persist_interval);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async persist()
|
|
107
|
+
{
|
|
108
|
+
if (!this.cfg.data)
|
|
109
|
+
{
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
while (this.wait_persist)
|
|
113
|
+
{
|
|
114
|
+
await new Promise(ok => this.wait_persist.push(ok));
|
|
115
|
+
}
|
|
116
|
+
this.wait_persist = [];
|
|
117
|
+
try
|
|
118
|
+
{
|
|
119
|
+
let dump = this.antietcd.etctree.dump(true);
|
|
120
|
+
dump['term'] = this.antietcd.stored_term;
|
|
121
|
+
dump = stableStringify(dump);
|
|
122
|
+
dump = await new Promise((ok, no) => zlib.gzip(dump, (err, res) => err ? no(err) : ok(res)));
|
|
123
|
+
const fh = await fsp.open(this.cfg.data+'.tmp', 'w');
|
|
124
|
+
await fh.writeFile(dump);
|
|
125
|
+
await fh.sync();
|
|
126
|
+
await fh.close();
|
|
127
|
+
await fsp.rename(this.cfg.data+'.tmp', this.cfg.data);
|
|
128
|
+
}
|
|
129
|
+
catch (e)
|
|
130
|
+
{
|
|
131
|
+
console.error('Error persisting data to disk: '+e);
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
runCallbacks(this, 'wait_persist', null);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
module.exports = AntiPersistence;
|
package/common.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// (c) Vitaliy Filippov, 2024
|
|
2
|
+
// License: Mozilla Public License 2.0 or Vitastor Network Public License 1.1
|
|
3
|
+
|
|
4
|
+
class RequestError
|
|
5
|
+
{
|
|
6
|
+
constructor(code, text, details)
|
|
7
|
+
{
|
|
8
|
+
this.code = code;
|
|
9
|
+
this.message = text;
|
|
10
|
+
this.details = details;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function de64(k)
|
|
15
|
+
{
|
|
16
|
+
if (k == null) // null or undefined
|
|
17
|
+
return k;
|
|
18
|
+
return Buffer.from(k, 'base64').toString();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function runCallbacks(obj, key, new_value)
|
|
22
|
+
{
|
|
23
|
+
const cbs = obj[key];
|
|
24
|
+
obj[key] = new_value;
|
|
25
|
+
if (cbs)
|
|
26
|
+
{
|
|
27
|
+
for (const cb of cbs)
|
|
28
|
+
{
|
|
29
|
+
cb();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = {
|
|
35
|
+
RequestError,
|
|
36
|
+
de64,
|
|
37
|
+
runCallbacks,
|
|
38
|
+
};
|