diskeyval 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/.github/workflows/test.yml +31 -0
- package/LICENSE +7 -0
- package/PLAN.md +169 -0
- package/README.md +119 -0
- package/TESTING.md +94 -0
- package/example.ts +672 -0
- package/lib/index.ts +448 -0
- package/lib/raft/in-memory-network.ts +96 -0
- package/lib/raft/index.ts +24 -0
- package/lib/raft/node.ts +883 -0
- package/lib/raft/tls-transport.ts +451 -0
- package/lib/raft/types.ts +150 -0
- package/lib/storage/file-raft-storage.ts +84 -0
- package/package.json +23 -0
- package/test/e2e/cluster.e2e.test.ts +280 -0
- package/test/sim/cluster.sim.test.ts +105 -0
- package/test/soak/cluster.soak.test.ts +97 -0
- package/test/support/certs.ts +126 -0
- package/test/support/helpers.ts +51 -0
- package/test/support/ports.ts +33 -0
- package/test/support/tls-cluster.ts +84 -0
- package/test/unit/diskeyval.coverage.test.ts +193 -0
- package/test/unit/in-memory-network.coverage.test.ts +111 -0
- package/test/unit/raft.coverage.test.ts +534 -0
- package/test/unit/raft.election.test.ts +101 -0
- package/test/unit/raft.phase5.test.ts +90 -0
- package/test/unit/raft.reads.test.ts +96 -0
- package/test/unit/raft.replication.test.ts +106 -0
- package/test/unit/tls-transport.coverage.test.ts +332 -0
- package/test/unit/tls-transport.internals.coverage.test.ts +209 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { isNotLeaderError, isQuorumUnavailableError } from '../../lib/index.ts';
|
|
4
|
+
import { createTestCluster, forceLeader } from '../support/helpers.ts';
|
|
5
|
+
|
|
6
|
+
test('[unit/reads] leader read barrier succeeds with quorum heartbeat', async () => {
|
|
7
|
+
const cluster = createTestCluster();
|
|
8
|
+
await cluster.start();
|
|
9
|
+
try {
|
|
10
|
+
await forceLeader(cluster.nodes['node-1']);
|
|
11
|
+
await cluster.nodes['node-1'].set('item', 'value');
|
|
12
|
+
|
|
13
|
+
const value = await cluster.nodes['node-1'].get('item');
|
|
14
|
+
assert.equal(value, 'value');
|
|
15
|
+
} finally {
|
|
16
|
+
await cluster.stop();
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('[unit/reads] leader read barrier rejects when quorum unavailable', async () => {
|
|
21
|
+
const cluster = createTestCluster();
|
|
22
|
+
await cluster.start();
|
|
23
|
+
try {
|
|
24
|
+
await forceLeader(cluster.nodes['node-1']);
|
|
25
|
+
cluster.network.partition('node-1', 'node-2');
|
|
26
|
+
cluster.network.partition('node-1', 'node-3');
|
|
27
|
+
|
|
28
|
+
await assert.rejects(
|
|
29
|
+
() => cluster.nodes['node-1'].get('item'),
|
|
30
|
+
(error: unknown) => isQuorumUnavailableError(error)
|
|
31
|
+
);
|
|
32
|
+
} finally {
|
|
33
|
+
await cluster.stop();
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('[unit/reads] follower read redirects to known leader', async () => {
|
|
38
|
+
const cluster = createTestCluster();
|
|
39
|
+
await cluster.start();
|
|
40
|
+
try {
|
|
41
|
+
await forceLeader(cluster.nodes['node-1']);
|
|
42
|
+
|
|
43
|
+
await assert.rejects(
|
|
44
|
+
() => cluster.nodes['node-2'].get('item'),
|
|
45
|
+
(error: unknown) => {
|
|
46
|
+
assert.ok(isNotLeaderError(error));
|
|
47
|
+
assert.equal(error.leaderId, 'node-1');
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
);
|
|
51
|
+
} finally {
|
|
52
|
+
await cluster.stop();
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('[unit/reads] read after committed write sees new value', async () => {
|
|
57
|
+
const cluster = createTestCluster();
|
|
58
|
+
await cluster.start();
|
|
59
|
+
try {
|
|
60
|
+
await forceLeader(cluster.nodes['node-1']);
|
|
61
|
+
await cluster.nodes['node-1'].set('after-write', 42);
|
|
62
|
+
|
|
63
|
+
const value = await cluster.nodes['node-1'].get<number>('after-write');
|
|
64
|
+
assert.equal(value, 42);
|
|
65
|
+
} finally {
|
|
66
|
+
await cluster.stop();
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('[unit/reads] read does not observe uncommitted value', async () => {
|
|
71
|
+
const cluster = createTestCluster();
|
|
72
|
+
await cluster.start();
|
|
73
|
+
try {
|
|
74
|
+
await forceLeader(cluster.nodes['node-1']);
|
|
75
|
+
cluster.network.partition('node-1', 'node-2');
|
|
76
|
+
cluster.network.partition('node-1', 'node-3');
|
|
77
|
+
|
|
78
|
+
await assert.rejects(() => cluster.nodes['node-1'].set('unsafe', 'value'));
|
|
79
|
+
await assert.rejects(() => cluster.nodes['node-1'].get('unsafe'));
|
|
80
|
+
} finally {
|
|
81
|
+
await cluster.stop();
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('[unit/reads] read returns undefined for missing key', async () => {
|
|
86
|
+
const cluster = createTestCluster();
|
|
87
|
+
await cluster.start();
|
|
88
|
+
try {
|
|
89
|
+
await forceLeader(cluster.nodes['node-1']);
|
|
90
|
+
|
|
91
|
+
const value = await cluster.nodes['node-1'].get('missing');
|
|
92
|
+
assert.equal(value, undefined);
|
|
93
|
+
} finally {
|
|
94
|
+
await cluster.stop();
|
|
95
|
+
}
|
|
96
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { isQuorumUnavailableError } from '../../lib/index.ts';
|
|
4
|
+
import { createTestCluster, forceLeader, waitForCondition } from '../support/helpers.ts';
|
|
5
|
+
|
|
6
|
+
test('[unit/replication] leader appends new entry to local log', async () => {
|
|
7
|
+
const cluster = createTestCluster();
|
|
8
|
+
await cluster.start();
|
|
9
|
+
try {
|
|
10
|
+
await forceLeader(cluster.nodes['node-1']);
|
|
11
|
+
const raft = cluster.nodes['node-1'].getRaftNode();
|
|
12
|
+
const before = raft.getState().lastLogIndex;
|
|
13
|
+
|
|
14
|
+
await cluster.nodes['node-1'].set('alpha', 1);
|
|
15
|
+
|
|
16
|
+
const after = raft.getState().lastLogIndex;
|
|
17
|
+
assert.equal(after, before + 1);
|
|
18
|
+
} finally {
|
|
19
|
+
await cluster.stop();
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('[unit/replication] leader replicates entry to followers', async () => {
|
|
24
|
+
const cluster = createTestCluster();
|
|
25
|
+
await cluster.start();
|
|
26
|
+
try {
|
|
27
|
+
await forceLeader(cluster.nodes['node-1']);
|
|
28
|
+
await cluster.nodes['node-1'].set('shared', 'value');
|
|
29
|
+
|
|
30
|
+
await waitForCondition(
|
|
31
|
+
() => cluster.nodes['node-2'].state.shared === 'value' && cluster.nodes['node-3'].state.shared === 'value',
|
|
32
|
+
{ timeoutMs: 700, message: 'Followers did not apply replicated value' }
|
|
33
|
+
);
|
|
34
|
+
} finally {
|
|
35
|
+
await cluster.stop();
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('[unit/replication] commit index advances after majority acks', async () => {
|
|
40
|
+
const cluster = createTestCluster();
|
|
41
|
+
await cluster.start();
|
|
42
|
+
try {
|
|
43
|
+
await forceLeader(cluster.nodes['node-1']);
|
|
44
|
+
const raft = cluster.nodes['node-1'].getRaftNode();
|
|
45
|
+
const before = raft.getState().commitIndex;
|
|
46
|
+
|
|
47
|
+
await cluster.nodes['node-1'].set('k', 'v');
|
|
48
|
+
|
|
49
|
+
const after = raft.getState().commitIndex;
|
|
50
|
+
assert.ok(after > before);
|
|
51
|
+
} finally {
|
|
52
|
+
await cluster.stop();
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('[unit/replication] cluster does not commit with minority replication', async () => {
|
|
57
|
+
const cluster = createTestCluster();
|
|
58
|
+
await cluster.start();
|
|
59
|
+
try {
|
|
60
|
+
await forceLeader(cluster.nodes['node-1']);
|
|
61
|
+
cluster.network.partition('node-1', 'node-2');
|
|
62
|
+
cluster.network.partition('node-1', 'node-3');
|
|
63
|
+
|
|
64
|
+
await assert.rejects(
|
|
65
|
+
() => cluster.nodes['node-1'].set('uncommitted', true),
|
|
66
|
+
(error: unknown) => isQuorumUnavailableError(error)
|
|
67
|
+
);
|
|
68
|
+
assert.equal(cluster.nodes['node-1'].state.uncommitted, undefined);
|
|
69
|
+
assert.equal(cluster.nodes['node-2'].state.uncommitted, undefined);
|
|
70
|
+
assert.equal(cluster.nodes['node-3'].state.uncommitted, undefined);
|
|
71
|
+
} finally {
|
|
72
|
+
await cluster.stop();
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('[unit/replication] write proposal promise resolves only on commit', async () => {
|
|
77
|
+
const cluster = createTestCluster();
|
|
78
|
+
await cluster.start();
|
|
79
|
+
try {
|
|
80
|
+
await forceLeader(cluster.nodes['node-1']);
|
|
81
|
+
const pendingSet = cluster.nodes['node-1'].set('ready', 'now');
|
|
82
|
+
await pendingSet;
|
|
83
|
+
|
|
84
|
+
assert.equal(cluster.nodes['node-1'].state.ready, 'now');
|
|
85
|
+
} finally {
|
|
86
|
+
await cluster.stop();
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('[unit/replication] write proposal promise rejects on leadership loss', async () => {
|
|
91
|
+
const cluster = createTestCluster();
|
|
92
|
+
await cluster.start();
|
|
93
|
+
try {
|
|
94
|
+
await forceLeader(cluster.nodes['node-1']);
|
|
95
|
+
|
|
96
|
+
cluster.network.partition('node-1', 'node-2');
|
|
97
|
+
cluster.network.partition('node-1', 'node-3');
|
|
98
|
+
|
|
99
|
+
await assert.rejects(
|
|
100
|
+
() => cluster.nodes['node-1'].set('x', 'y'),
|
|
101
|
+
/quorum|majority|Timed out/i
|
|
102
|
+
);
|
|
103
|
+
} finally {
|
|
104
|
+
await cluster.stop();
|
|
105
|
+
}
|
|
106
|
+
});
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { connect, createServer } from 'node:tls';
|
|
4
|
+
import { createTlsRaftTransport } from '../../lib/raft/tls-transport.ts';
|
|
5
|
+
import type { RaftRpcEndpoint } from '../../lib/raft/types.ts';
|
|
6
|
+
import { createTestCertificates, hasOpenSsl } from '../support/certs.ts';
|
|
7
|
+
import { getFreePorts } from '../support/ports.ts';
|
|
8
|
+
|
|
9
|
+
type Command = { type: 'set'; key: string; value: unknown };
|
|
10
|
+
|
|
11
|
+
function createEndpoint(options?: {
|
|
12
|
+
onRequestVote?: RaftRpcEndpoint<Command>['onRequestVote'];
|
|
13
|
+
onAppendEntries?: RaftRpcEndpoint<Command>['onAppendEntries'];
|
|
14
|
+
}): RaftRpcEndpoint<Command> {
|
|
15
|
+
return {
|
|
16
|
+
onRequestVote:
|
|
17
|
+
options?.onRequestVote ??
|
|
18
|
+
(async (_fromNodeId, request) => ({
|
|
19
|
+
term: request.term,
|
|
20
|
+
voteGranted: true
|
|
21
|
+
})),
|
|
22
|
+
onAppendEntries:
|
|
23
|
+
options?.onAppendEntries ??
|
|
24
|
+
(async (_fromNodeId, request) => ({
|
|
25
|
+
term: request.term,
|
|
26
|
+
success: true,
|
|
27
|
+
matchIndex: request.prevLogIndex + request.entries.length
|
|
28
|
+
}))
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
test('[unit/tls-transport] unknown peers and start/stop idempotency', { skip: !hasOpenSsl() }, async () => {
|
|
33
|
+
const certs = createTestCertificates(['node-a']);
|
|
34
|
+
const [port] = await getFreePorts(1);
|
|
35
|
+
const transport = createTlsRaftTransport<Command>({
|
|
36
|
+
nodeId: 'node-a',
|
|
37
|
+
host: '127.0.0.1',
|
|
38
|
+
port,
|
|
39
|
+
peers: [],
|
|
40
|
+
tls: {
|
|
41
|
+
cert: certs.nodes['node-a'].certPem,
|
|
42
|
+
key: certs.nodes['node-a'].keyPem,
|
|
43
|
+
ca: certs.caPem
|
|
44
|
+
},
|
|
45
|
+
rpcTimeoutMs: 500
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
await assert.rejects(
|
|
50
|
+
() =>
|
|
51
|
+
transport.requestVote('node-a', 'node-missing', {
|
|
52
|
+
term: 1,
|
|
53
|
+
candidateId: 'node-a',
|
|
54
|
+
lastLogIndex: 0,
|
|
55
|
+
lastLogTerm: 0
|
|
56
|
+
}),
|
|
57
|
+
/Unknown peer/i
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
await transport.stop();
|
|
61
|
+
await transport.start(createEndpoint());
|
|
62
|
+
await transport.start(createEndpoint());
|
|
63
|
+
await transport.stop();
|
|
64
|
+
await transport.stop();
|
|
65
|
+
} finally {
|
|
66
|
+
certs.cleanup();
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('[unit/tls-transport] rpc propagates remote endpoint errors', { skip: !hasOpenSsl() }, async () => {
|
|
71
|
+
const certs = createTestCertificates(['node-a', 'node-b']);
|
|
72
|
+
const [portA, portB] = await getFreePorts(2);
|
|
73
|
+
|
|
74
|
+
const transportA = createTlsRaftTransport<Command>({
|
|
75
|
+
nodeId: 'node-a',
|
|
76
|
+
host: '127.0.0.1',
|
|
77
|
+
port: portA,
|
|
78
|
+
peers: [{ nodeId: 'node-b', host: '127.0.0.1', port: portB }],
|
|
79
|
+
tls: {
|
|
80
|
+
cert: certs.nodes['node-a'].certPem,
|
|
81
|
+
key: certs.nodes['node-a'].keyPem,
|
|
82
|
+
ca: certs.caPem
|
|
83
|
+
},
|
|
84
|
+
rpcTimeoutMs: 1_000
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const transportB = createTlsRaftTransport<Command>({
|
|
88
|
+
nodeId: 'node-b',
|
|
89
|
+
host: '127.0.0.1',
|
|
90
|
+
port: portB,
|
|
91
|
+
peers: [{ nodeId: 'node-a', host: '127.0.0.1', port: portA }],
|
|
92
|
+
tls: {
|
|
93
|
+
cert: certs.nodes['node-b'].certPem,
|
|
94
|
+
key: certs.nodes['node-b'].keyPem,
|
|
95
|
+
ca: certs.caPem
|
|
96
|
+
},
|
|
97
|
+
rpcTimeoutMs: 1_000
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
await transportA.start(createEndpoint());
|
|
102
|
+
await transportB.start(
|
|
103
|
+
createEndpoint({
|
|
104
|
+
onRequestVote: async () => {
|
|
105
|
+
throw new Error('boom-from-endpoint');
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
await assert.rejects(
|
|
111
|
+
() =>
|
|
112
|
+
transportA.requestVote('node-a', 'node-b', {
|
|
113
|
+
term: 1,
|
|
114
|
+
candidateId: 'node-a',
|
|
115
|
+
lastLogIndex: 0,
|
|
116
|
+
lastLogTerm: 0
|
|
117
|
+
}),
|
|
118
|
+
/boom-from-endpoint/i
|
|
119
|
+
);
|
|
120
|
+
} finally {
|
|
121
|
+
await transportA.stop();
|
|
122
|
+
await transportB.stop();
|
|
123
|
+
certs.cleanup();
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('[unit/tls-transport] rejects forged fromNodeId in payload', { skip: !hasOpenSsl() }, async () => {
|
|
128
|
+
const certs = createTestCertificates(['node-a', 'node-b']);
|
|
129
|
+
const [portA] = await getFreePorts(1);
|
|
130
|
+
|
|
131
|
+
const transportA = createTlsRaftTransport<Command>({
|
|
132
|
+
nodeId: 'node-a',
|
|
133
|
+
host: '127.0.0.1',
|
|
134
|
+
port: portA,
|
|
135
|
+
peers: [{ nodeId: 'node-b', host: '127.0.0.1', port: 9_999 }],
|
|
136
|
+
tls: {
|
|
137
|
+
cert: certs.nodes['node-a'].certPem,
|
|
138
|
+
key: certs.nodes['node-a'].keyPem,
|
|
139
|
+
ca: certs.caPem
|
|
140
|
+
},
|
|
141
|
+
rpcTimeoutMs: 1_000
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
await transportA.start(createEndpoint());
|
|
146
|
+
|
|
147
|
+
const socket = connect({
|
|
148
|
+
host: '127.0.0.1',
|
|
149
|
+
port: portA,
|
|
150
|
+
cert: certs.nodes['node-b'].certPem,
|
|
151
|
+
key: certs.nodes['node-b'].keyPem,
|
|
152
|
+
ca: certs.caPem,
|
|
153
|
+
rejectUnauthorized: true,
|
|
154
|
+
servername: 'node-a'
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const response = await new Promise<string>((resolve, reject) => {
|
|
158
|
+
let buffer = '';
|
|
159
|
+
|
|
160
|
+
socket.once('secureConnect', () => {
|
|
161
|
+
socket.write(
|
|
162
|
+
`${JSON.stringify({
|
|
163
|
+
id: 'forged-id',
|
|
164
|
+
method: 'requestVote',
|
|
165
|
+
fromNodeId: 'node-c',
|
|
166
|
+
payload: {
|
|
167
|
+
term: 1,
|
|
168
|
+
candidateId: 'node-c',
|
|
169
|
+
lastLogIndex: 0,
|
|
170
|
+
lastLogTerm: 0
|
|
171
|
+
}
|
|
172
|
+
})}\n`
|
|
173
|
+
);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
socket.on('data', (chunk) => {
|
|
177
|
+
buffer += chunk.toString('utf8');
|
|
178
|
+
const newline = buffer.indexOf('\n');
|
|
179
|
+
if (newline < 0) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
resolve(buffer.slice(0, newline));
|
|
183
|
+
socket.end();
|
|
184
|
+
});
|
|
185
|
+
socket.once('error', reject);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const parsed = JSON.parse(response) as {
|
|
189
|
+
ok: boolean;
|
|
190
|
+
error?: { message: string };
|
|
191
|
+
};
|
|
192
|
+
assert.equal(parsed.ok, false);
|
|
193
|
+
assert.match(parsed.error?.message ?? '', /Peer identity mismatch/i);
|
|
194
|
+
} finally {
|
|
195
|
+
await transportA.stop();
|
|
196
|
+
certs.cleanup();
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test('[unit/tls-transport] upsertPeer and removePeer alter routability', { skip: !hasOpenSsl() }, async () => {
|
|
201
|
+
const certs = createTestCertificates(['node-a', 'node-b']);
|
|
202
|
+
const [portA, portB] = await getFreePorts(2);
|
|
203
|
+
|
|
204
|
+
const transportA = createTlsRaftTransport<Command>({
|
|
205
|
+
nodeId: 'node-a',
|
|
206
|
+
host: '127.0.0.1',
|
|
207
|
+
port: portA,
|
|
208
|
+
peers: [],
|
|
209
|
+
tls: {
|
|
210
|
+
cert: certs.nodes['node-a'].certPem,
|
|
211
|
+
key: certs.nodes['node-a'].keyPem,
|
|
212
|
+
ca: certs.caPem
|
|
213
|
+
},
|
|
214
|
+
rpcTimeoutMs: 1_000
|
|
215
|
+
});
|
|
216
|
+
const transportB = createTlsRaftTransport<Command>({
|
|
217
|
+
nodeId: 'node-b',
|
|
218
|
+
host: '127.0.0.1',
|
|
219
|
+
port: portB,
|
|
220
|
+
peers: [{ nodeId: 'node-a', host: '127.0.0.1', port: portA }],
|
|
221
|
+
tls: {
|
|
222
|
+
cert: certs.nodes['node-b'].certPem,
|
|
223
|
+
key: certs.nodes['node-b'].keyPem,
|
|
224
|
+
ca: certs.caPem
|
|
225
|
+
},
|
|
226
|
+
rpcTimeoutMs: 1_000
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
await transportA.start(createEndpoint());
|
|
231
|
+
await transportB.start(createEndpoint());
|
|
232
|
+
|
|
233
|
+
await assert.rejects(
|
|
234
|
+
() =>
|
|
235
|
+
transportA.requestVote('node-a', 'node-b', {
|
|
236
|
+
term: 1,
|
|
237
|
+
candidateId: 'node-a',
|
|
238
|
+
lastLogIndex: 0,
|
|
239
|
+
lastLogTerm: 0
|
|
240
|
+
}),
|
|
241
|
+
/Unknown peer/i
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
transportA.upsertPeer?.({ nodeId: 'node-b', host: '127.0.0.1', port: portB });
|
|
245
|
+
const vote = await transportA.requestVote('node-a', 'node-b', {
|
|
246
|
+
term: 1,
|
|
247
|
+
candidateId: 'node-a',
|
|
248
|
+
lastLogIndex: 0,
|
|
249
|
+
lastLogTerm: 0
|
|
250
|
+
});
|
|
251
|
+
assert.equal(vote.voteGranted, true);
|
|
252
|
+
|
|
253
|
+
transportA.removePeer?.('node-b');
|
|
254
|
+
await assert.rejects(
|
|
255
|
+
() =>
|
|
256
|
+
transportA.requestVote('node-a', 'node-b', {
|
|
257
|
+
term: 2,
|
|
258
|
+
candidateId: 'node-a',
|
|
259
|
+
lastLogIndex: 0,
|
|
260
|
+
lastLogTerm: 0
|
|
261
|
+
}),
|
|
262
|
+
/Unknown peer/i
|
|
263
|
+
);
|
|
264
|
+
} finally {
|
|
265
|
+
await transportA.stop();
|
|
266
|
+
await transportB.stop();
|
|
267
|
+
certs.cleanup();
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test('[unit/tls-transport] request times out when peer accepts but never replies', { skip: !hasOpenSsl() }, async () => {
|
|
272
|
+
const certs = createTestCertificates(['node-a', 'node-b']);
|
|
273
|
+
const [portA, silentPort] = await getFreePorts(2);
|
|
274
|
+
const sockets = new Set<import('node:tls').TLSSocket>();
|
|
275
|
+
|
|
276
|
+
const silentServer = createServer(
|
|
277
|
+
{
|
|
278
|
+
cert: certs.nodes['node-b'].certPem,
|
|
279
|
+
key: certs.nodes['node-b'].keyPem,
|
|
280
|
+
ca: certs.caPem,
|
|
281
|
+
requestCert: true,
|
|
282
|
+
rejectUnauthorized: true,
|
|
283
|
+
minVersion: 'TLSv1.3'
|
|
284
|
+
},
|
|
285
|
+
(socket) => {
|
|
286
|
+
sockets.add(socket);
|
|
287
|
+
socket.on('close', () => {
|
|
288
|
+
sockets.delete(socket);
|
|
289
|
+
});
|
|
290
|
+
// Intentionally do nothing so the client read path times out.
|
|
291
|
+
}
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
await new Promise<void>((resolve, reject) => {
|
|
295
|
+
silentServer.once('error', reject);
|
|
296
|
+
silentServer.listen(silentPort, '127.0.0.1', () => resolve());
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
const transportA = createTlsRaftTransport<Command>({
|
|
300
|
+
nodeId: 'node-a',
|
|
301
|
+
host: '127.0.0.1',
|
|
302
|
+
port: portA,
|
|
303
|
+
peers: [{ nodeId: 'node-b', host: '127.0.0.1', port: silentPort }],
|
|
304
|
+
tls: {
|
|
305
|
+
cert: certs.nodes['node-a'].certPem,
|
|
306
|
+
key: certs.nodes['node-a'].keyPem,
|
|
307
|
+
ca: certs.caPem
|
|
308
|
+
},
|
|
309
|
+
rpcTimeoutMs: 50
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
try {
|
|
313
|
+
await transportA.start(createEndpoint());
|
|
314
|
+
await assert.rejects(
|
|
315
|
+
() =>
|
|
316
|
+
transportA.requestVote('node-a', 'node-b', {
|
|
317
|
+
term: 1,
|
|
318
|
+
candidateId: 'node-a',
|
|
319
|
+
lastLogIndex: 0,
|
|
320
|
+
lastLogTerm: 0
|
|
321
|
+
}),
|
|
322
|
+
/Timed out waiting for RPC response/i
|
|
323
|
+
);
|
|
324
|
+
} finally {
|
|
325
|
+
await transportA.stop();
|
|
326
|
+
for (const socket of sockets) {
|
|
327
|
+
socket.destroy();
|
|
328
|
+
}
|
|
329
|
+
await new Promise<void>((resolve) => silentServer.close(() => resolve()));
|
|
330
|
+
certs.cleanup();
|
|
331
|
+
}
|
|
332
|
+
});
|