apache-iggy 0.10.0 → 0.10.1-edge.2
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 +248 -28
- package/dist/client/client.config.d.ts +2 -0
- package/dist/client/client.config.js +13 -2
- package/dist/client/client.config.test.js +15 -2
- package/dist/client/client.connection-string.js +13 -1
- package/dist/client/client.connection-string.test.js +43 -1
- package/dist/client/client.connection.d.ts +33 -135
- package/dist/client/client.connection.js +190 -453
- package/dist/client/client.connection.test.js +244 -669
- package/dist/client/client.d.ts +4 -35
- package/dist/client/client.js +8 -101
- package/dist/client/client.operation.d.ts +120 -0
- package/dist/client/client.operation.js +428 -0
- package/dist/client/client.operation.test.d.ts +2 -0
- package/dist/client/client.operation.test.js +285 -0
- package/dist/client/client.socket.d.ts +92 -209
- package/dist/client/client.socket.js +1205 -981
- package/dist/client/client.socket.test.js +4865 -679
- package/dist/client/client.type.d.ts +65 -22
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.js +1 -0
- package/dist/client.e2e.js +3 -5
- package/dist/e2e/resource-options.e2e.d.ts +2 -0
- package/dist/{wire/stream/purge-stream.command.js → e2e/resource-options.e2e.js} +12 -20
- package/dist/e2e/resource-options.suite.d.ts +15 -0
- package/dist/e2e/resource-options.suite.js +272 -0
- package/dist/e2e/tcp.cluster.e2e.js +1 -0
- package/dist/e2e/tcp.consumer-stream.e2e.js +4 -0
- package/dist/e2e/tcp.parallel.e2e.js +5 -6
- package/dist/e2e/tcp.stream.e2e.js +0 -3
- package/dist/e2e/tcp.system.e2e.js +10 -1
- package/dist/e2e/tcp.token.e2e.js +8 -2
- package/dist/e2e/tcp.topic.e2e.js +0 -3
- package/dist/e2e/tcp.user.e2e.js +46 -3
- package/dist/e2e/tls.system.e2e.js +21 -12
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -1
- package/dist/stream/consumer-stream.js +97 -12
- package/dist/wire/cluster/cluster.type.d.ts +2 -2
- package/dist/wire/cluster/cluster.utils.d.ts +1 -0
- package/dist/wire/cluster/cluster.utils.js +4 -1
- package/dist/wire/command-set.d.ts +8 -8
- package/dist/wire/command-set.js +18 -9
- package/dist/wire/command.code.d.ts +3 -10
- package/dist/wire/command.code.js +9 -11
- package/dist/wire/command.utils.d.ts +3 -2
- package/dist/wire/command.utils.js +3 -2
- package/dist/wire/error.code.js +17 -1
- package/dist/wire/error.code.test.js +28 -1
- package/dist/wire/error.utils.d.ts +4 -2
- package/dist/wire/error.utils.js +6 -3
- package/dist/wire/identifier.utils.d.ts +2 -0
- package/dist/wire/identifier.utils.js +2 -0
- package/dist/wire/index.d.ts +1 -0
- package/dist/wire/index.js +1 -0
- package/dist/wire/message/header.type.d.ts +1 -1
- package/dist/wire/message/message-batch.test.js +11 -5
- package/dist/wire/message/message.utils.d.ts +0 -10
- package/dist/wire/message/message.utils.js +1 -22
- package/dist/wire/message/poll-messages.command.d.ts +12 -1
- package/dist/wire/message/poll-messages.command.js +64 -16
- package/dist/wire/message/poll-messages.command.test.js +143 -5
- package/dist/wire/message/poll.utils.d.ts +4 -0
- package/dist/wire/message/poll.utils.js +6 -1
- package/dist/wire/message/send-messages.command.d.ts +3 -3
- package/dist/wire/message/send-messages.command.js +103 -3
- package/dist/wire/message/send-messages.command.test.js +113 -2
- package/dist/wire/offset/delete-offset.command.d.ts +11 -4
- package/dist/wire/offset/delete-offset.command.js +1 -1
- package/dist/wire/offset/get-offset.command.d.ts +1 -1
- package/dist/wire/offset/offset-write.command.test.d.ts +2 -0
- package/dist/wire/offset/offset-write.command.test.js +43 -0
- package/dist/wire/offset/offset.utils.js +10 -6
- package/dist/wire/offset/store-offset.command.d.ts +10 -0
- package/dist/wire/offset/store-offset.command.js +1 -1
- package/dist/wire/options.utils.d.ts +46 -0
- package/dist/wire/options.utils.js +57 -0
- package/dist/wire/partition/partition-route.d.ts +12 -0
- package/dist/wire/partition/partition-route.js +40 -0
- package/dist/wire/partition/partition-route.test.d.ts +2 -0
- package/dist/wire/partition/partition-route.test.js +42 -0
- package/dist/wire/session/login-with-token.command.d.ts +2 -2
- package/dist/wire/session/login-with-token.command.js +7 -2
- package/dist/wire/session/login.command.d.ts +2 -2
- package/dist/wire/session/login.command.js +7 -2
- package/dist/wire/stream/create-stream.command.d.ts +5 -2
- package/dist/wire/stream/create-stream.command.js +20 -2
- package/dist/wire/stream/index.d.ts +1 -1
- package/dist/wire/stream/index.js +1 -1
- package/dist/wire/stream/stream-options.d.ts +77 -0
- package/dist/wire/stream/stream-options.js +101 -0
- package/dist/wire/stream/stream-options.test.d.ts +2 -0
- package/dist/wire/stream/stream-options.test.js +103 -0
- package/dist/wire/stream/stream.utils.d.ts +9 -0
- package/dist/wire/stream/stream.utils.js +17 -3
- package/dist/wire/stream/update-stream.command.d.ts +23 -5
- package/dist/wire/stream/update-stream.command.js +10 -17
- package/dist/wire/system/describe-options.command.d.ts +23 -6
- package/dist/wire/system/describe-options.command.js +21 -2
- package/dist/wire/system/get-stats.command.d.ts +17 -0
- package/dist/wire/system/get-stats.command.js +69 -16
- package/dist/wire/system/get-stats.command.test.d.ts +2 -0
- package/dist/wire/system/get-stats.command.test.js +97 -0
- package/dist/wire/system/index.d.ts +2 -0
- package/dist/wire/system/index.js +2 -0
- package/dist/wire/system/snapshot.command.d.ts +64 -0
- package/dist/wire/system/snapshot.command.js +76 -0
- package/dist/wire/system/snapshot.command.test.d.ts +2 -0
- package/dist/wire/system/snapshot.command.test.js +86 -0
- package/dist/wire/system/update-resource.command.d.ts +53 -0
- package/dist/wire/system/update-resource.command.js +130 -0
- package/dist/wire/system/update-resource.command.test.d.ts +2 -0
- package/dist/wire/system/update-resource.command.test.js +497 -0
- package/dist/wire/token/create-token.command.d.ts +1 -1
- package/dist/wire/token/create-token.command.js +1 -1
- package/dist/wire/token/create-token.command.test.js +6 -0
- package/dist/wire/topic/create-topic.command.d.ts +20 -12
- package/dist/wire/topic/create-topic.command.js +11 -4
- package/dist/wire/topic/index.d.ts +1 -2
- package/dist/wire/topic/index.js +1 -2
- package/dist/wire/topic/topic.utils.d.ts +14 -5
- package/dist/wire/topic/topic.utils.js +26 -11
- package/dist/wire/topic/topic.utils.test.js +13 -13
- package/dist/wire/topic/update-topic.command.d.ts +22 -15
- package/dist/wire/topic/update-topic.command.js +12 -34
- package/dist/wire/user/create-user.command.d.ts +12 -2
- package/dist/wire/user/create-user.command.js +7 -3
- package/dist/wire/user/index.d.ts +1 -0
- package/dist/wire/user/index.js +1 -0
- package/dist/wire/user/update-user.command.d.ts +25 -7
- package/dist/wire/user/update-user.command.js +18 -31
- package/dist/wire/user/user.utils.d.ts +2 -0
- package/dist/wire/user/user.utils.js +5 -3
- package/dist/wire/vsr/header.d.ts +23 -8
- package/dist/wire/vsr/header.js +32 -10
- package/dist/wire/vsr/header.test.js +5 -1
- package/dist/wire/vsr/index.d.ts +8 -2
- package/dist/wire/vsr/index.js +44 -11
- package/dist/wire/vsr/operation.d.ts +18 -6
- package/dist/wire/vsr/operation.js +46 -16
- package/dist/wire/vsr/operation.test.js +32 -8
- package/dist/wire/vsr/register.d.ts +10 -6
- package/dist/wire/vsr/register.js +24 -7
- package/dist/wire/vsr/register.test.js +28 -8
- package/dist/wire/vsr/reply.d.ts +4 -1
- package/dist/wire/vsr/reply.js +42 -7
- package/dist/wire/vsr/reply.test.js +11 -1
- package/dist/wire/vsr/session.d.ts +9 -10
- package/dist/wire/vsr/session.js +32 -23
- package/dist/wire/vsr/session.test.js +33 -7
- package/dist/wire/vsr/vsr.test.js +57 -6
- package/package.json +6 -7
- package/dist/wire/message/flush-unsaved-buffers.command.d.ts +0 -26
- package/dist/wire/message/flush-unsaved-buffers.command.js +0 -36
- package/dist/wire/stream/purge-stream.command.d.ts +0 -22
- package/dist/wire/topic/purge-topic.command.d.ts +0 -24
- package/dist/wire/topic/purge-topic.command.js +0 -39
|
@@ -17,780 +17,355 @@
|
|
|
17
17
|
import assert from 'node:assert/strict';
|
|
18
18
|
import { once } from 'node:events';
|
|
19
19
|
import { readFileSync } from 'node:fs';
|
|
20
|
-
import { createServer } from 'node:net';
|
|
20
|
+
import { createServer, Socket } from 'node:net';
|
|
21
21
|
import { createServer as createTlsServer } from 'node:tls';
|
|
22
|
-
import { describe, it
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import { Command, HEADER_SIZE, REPLY_OFFSET } from '../wire/vsr/header.js';
|
|
26
|
-
|
|
22
|
+
import { describe, it } from 'node:test';
|
|
23
|
+
import { IggyConnection, LINK_WRITE_QUEUE, MAX_PENDING_PER_LINK } from './client.connection.js';
|
|
24
|
+
import { ResponseFrameDecoder, ProtocolFrameError } from './client.frame.js';
|
|
25
|
+
import { Command, HEADER_SIZE, REPLY_OFFSET, REQUEST_OFFSET, encodeRequestHeader } from '../wire/vsr/header.js';
|
|
26
|
+
import { Operation } from '../wire/vsr/operation.js';
|
|
27
|
+
import { FrozenFrame, OPERATION_TIMEOUT_MS, VsrResponseTimeoutError } from './client.operation.js';
|
|
28
|
+
import { ConnectionClosedError } from '../index.js';
|
|
27
29
|
const TLS_CERTIFICATE = readFileSync(new URL('../../../../core/certs/iggy_cert.pem', import.meta.url));
|
|
28
30
|
const TLS_KEY = readFileSync(new URL('../../../../core/certs/iggy_key.pem', import.meta.url));
|
|
29
31
|
const TLS_CA_CERTIFICATE = readFileSync(new URL('../../../../core/certs/iggy_ca_cert.pem', import.meta.url));
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
32
|
+
const FRAME_LIMIT = 2 * HEADER_SIZE;
|
|
33
|
+
const connectionConfig = (server) => ({
|
|
34
|
+
transport: 'TCP', options: { host: '127.0.0.1', port: server.address().port },
|
|
35
|
+
credentials: { username: 'iggy', password: 'iggy' }, heartbeatInterval: 0,
|
|
36
|
+
reconnect: { enabled: false, interval: 0, maxRetries: 0 }, maxResponseFrameSize: FRAME_LIMIT
|
|
37
|
+
});
|
|
38
|
+
const requestFrame = (id) => encodeRequestHeader({
|
|
39
|
+
size: HEADER_SIZE, client: 1n, request: id, operation: Operation.NonReplicated, session: 1n, nonReplicatedCode: 1
|
|
40
|
+
});
|
|
41
|
+
const replyFrame = (request, body = Buffer.alloc(0)) => {
|
|
42
|
+
const response = Buffer.alloc(HEADER_SIZE + body.length);
|
|
43
|
+
response.writeUInt32LE(response.length, REPLY_OFFSET.size);
|
|
44
|
+
response[REPLY_OFFSET.command] = Command.Reply;
|
|
45
|
+
response[REPLY_OFFSET.operation] = request[REQUEST_OFFSET.operation];
|
|
46
|
+
response.writeBigUInt64LE(request.readBigUInt64LE(REQUEST_OFFSET.request), REPLY_OFFSET.request);
|
|
47
|
+
body.copy(response, HEADER_SIZE);
|
|
48
|
+
return response;
|
|
38
49
|
};
|
|
39
|
-
const startServer = async () => {
|
|
40
|
-
const
|
|
50
|
+
const startServer = async (handler, tls = false) => {
|
|
51
|
+
const accept = (socket) => {
|
|
52
|
+
const decoder = new ResponseFrameDecoder(FRAME_LIMIT);
|
|
53
|
+
socket.on('error', () => undefined);
|
|
54
|
+
socket.on('data', (bytes) => { if (!handler)
|
|
55
|
+
return; for (const frame of decoder.push(Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes)))
|
|
56
|
+
handler?.(frame, socket); });
|
|
57
|
+
};
|
|
58
|
+
const server = tls ? createTlsServer({ cert: TLS_CERTIFICATE, key: TLS_KEY }, accept) : createServer(accept);
|
|
41
59
|
server.listen(0, '127.0.0.1');
|
|
42
60
|
await once(server, 'listening');
|
|
43
61
|
return server;
|
|
44
62
|
};
|
|
45
|
-
const connectionConfig = (server) => ({
|
|
46
|
-
transport: 'TCP',
|
|
47
|
-
options: {
|
|
48
|
-
host: '127.0.0.1',
|
|
49
|
-
port: server.address().port
|
|
50
|
-
},
|
|
51
|
-
credentials: { username: 'iggy', password: 'iggy' },
|
|
52
|
-
reconnect: { enabled: false, interval: 0, maxRetries: 0 },
|
|
53
|
-
maxResponseFrameSize: FRAME_LIMIT
|
|
54
|
-
});
|
|
55
|
-
const replyFrame = (body) => {
|
|
56
|
-
const frame = Buffer.alloc(HEADER_SIZE + body.length);
|
|
57
|
-
frame.writeUInt32LE(frame.length, REPLY_OFFSET.size);
|
|
58
|
-
frame.writeUInt8(Command.Reply, REPLY_OFFSET.command);
|
|
59
|
-
body.copy(frame, HEADER_SIZE);
|
|
60
|
-
return frame;
|
|
61
|
-
};
|
|
62
63
|
const closeConnection = async (connection, server) => {
|
|
63
64
|
connection._destroy();
|
|
64
|
-
if (!connection.socket.destroyed)
|
|
65
|
-
await once(connection.socket, 'close');
|
|
66
65
|
await new Promise((resolve) => server.close(() => resolve()));
|
|
67
66
|
};
|
|
68
|
-
|
|
69
|
-
describe('IggyConnection', () => {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
67
|
+
const exchange = (connection, id, signal = new AbortController().signal) => connection.exchange(() => requestFrame(id), Date.now() + 1_000, signal);
|
|
68
|
+
describe('IggyConnection multiplexing', () => {
|
|
69
|
+
it('matches normalized peer addresses and ports with a configured-host fallback', () => {
|
|
70
|
+
const host = 'bootstrap.example.test';
|
|
71
|
+
const configuredPort = 8090;
|
|
72
|
+
const peerPort = configuredPort + 1;
|
|
73
|
+
const connection = new IggyConnection({ transport: 'TCP', options: { host, port: configuredPort },
|
|
74
|
+
credentials: { username: 'iggy', password: 'iggy' } });
|
|
75
|
+
assert.equal(connection.isConnectedTo(host.toUpperCase(), configuredPort), true);
|
|
76
|
+
assert.equal(connection.isConnectedTo('192.0.2.10', peerPort), false);
|
|
77
|
+
connection.socket = new Socket();
|
|
78
|
+
Object.defineProperties(connection.socket, {
|
|
79
|
+
remoteAddress: { value: '::ffff:192.0.2.10' },
|
|
80
|
+
remotePort: { value: peerPort }
|
|
81
|
+
});
|
|
79
82
|
try {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
assert.equal(
|
|
83
|
-
assert.equal(connection.
|
|
83
|
+
assert.equal(connection.isConnectedTo('192.0.2.10', peerPort), true);
|
|
84
|
+
assert.equal(connection.isConnectedTo('::FFFF:192.0.2.10', peerPort), true);
|
|
85
|
+
assert.equal(connection.isConnectedTo('192.0.2.10', configuredPort), false);
|
|
86
|
+
assert.equal(connection.isConnectedTo('192.0.2.11', peerPort), false);
|
|
87
|
+
assert.equal(connection.isConnectedTo(host.toUpperCase(), configuredPort), true);
|
|
88
|
+
assert.equal(connection.isConnectedTo(host, peerPort), false);
|
|
84
89
|
}
|
|
85
90
|
finally {
|
|
86
|
-
|
|
91
|
+
connection._destroy();
|
|
87
92
|
}
|
|
88
93
|
});
|
|
89
|
-
it('
|
|
90
|
-
const server = await startServer();
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
});
|
|
96
|
-
const connection = new IggyConnection(connectionConfig(server)
|
|
94
|
+
it('recognizes an IP roster endpoint for a DNS TLS connection', async () => {
|
|
95
|
+
const server = await startServer((frame, socket) => socket.write(replyFrame(frame)), true);
|
|
96
|
+
const secured = once(server, 'secureConnection');
|
|
97
|
+
const host = 'bootstrap.example.test';
|
|
98
|
+
const port = server.address().port;
|
|
99
|
+
let accepted = 0;
|
|
100
|
+
server.on('secureConnection', () => { accepted += 1; });
|
|
101
|
+
const connection = new IggyConnection({ ...connectionConfig(server), transport: 'TLS',
|
|
102
|
+
options: { host, port, family: 4, rejectUnauthorized: false,
|
|
103
|
+
lookup: (_hostname, _options, callback) => callback(null, '127.0.0.1', 4) } });
|
|
97
104
|
try {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
assert.equal(
|
|
102
|
-
assert.equal(
|
|
103
|
-
connection.
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
assert.deepEqual(await received, frame);
|
|
105
|
+
await connection.connect();
|
|
106
|
+
const [peer] = await secured;
|
|
107
|
+
const socket = connection.socket;
|
|
108
|
+
assert.equal(peer.servername, host);
|
|
109
|
+
assert.equal(socket.remoteAddress, '127.0.0.1');
|
|
110
|
+
assert.equal(connection.isConnectedTo('127.0.0.1', port), true);
|
|
111
|
+
assert.equal(connection.isConnectedTo('::ffff:127.0.0.1', port), true);
|
|
112
|
+
assert.equal(connection.isConnectedTo(host.toUpperCase(), port), true);
|
|
113
|
+
assert.equal((await exchange(connection, 1n)).error, undefined);
|
|
114
|
+
await connection.connect();
|
|
115
|
+
assert.equal(connection.socket, socket);
|
|
116
|
+
assert.equal(accepted, 1);
|
|
111
117
|
}
|
|
112
118
|
finally {
|
|
113
119
|
await closeConnection(connection, server);
|
|
114
120
|
}
|
|
115
121
|
});
|
|
116
|
-
it('
|
|
117
|
-
const server = await startServer();
|
|
122
|
+
it('dials lazily and shares an in-flight connection attempt', async () => {
|
|
123
|
+
const server = await startServer((frame, socket) => socket.write(replyFrame(frame)));
|
|
124
|
+
let accepted = 0;
|
|
125
|
+
server.on('connection', () => { accepted += 1; });
|
|
118
126
|
const connection = new IggyConnection(connectionConfig(server));
|
|
119
127
|
try {
|
|
120
|
-
await
|
|
121
|
-
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
assert.
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const error = once(connection, 'error');
|
|
129
|
-
connection._onData(malformed);
|
|
130
|
-
assert.ok((await error)[0] instanceof ProtocolFrameError);
|
|
128
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
129
|
+
assert.equal(accepted, 0);
|
|
130
|
+
const pending = connection.connect();
|
|
131
|
+
assert.equal(connection.connect(), pending);
|
|
132
|
+
await pending;
|
|
133
|
+
assert.equal(accepted, 1);
|
|
134
|
+
assert.equal(connection.isConnectedTo('localhost', server.address().port), true);
|
|
135
|
+
assert.equal((await exchange(connection, 1n)).error, undefined);
|
|
131
136
|
}
|
|
132
137
|
finally {
|
|
133
138
|
await closeConnection(connection, server);
|
|
134
139
|
}
|
|
135
140
|
});
|
|
136
|
-
it('
|
|
137
|
-
const
|
|
141
|
+
it('correlates out-of-order replies on one socket', async () => {
|
|
142
|
+
const requests = [];
|
|
143
|
+
const server = await startServer((frame, socket) => {
|
|
144
|
+
requests.push(frame);
|
|
145
|
+
if (requests.length === 2)
|
|
146
|
+
socket.write(Buffer.concat([replyFrame(requests[1], Buffer.from('two')), replyFrame(requests[0], Buffer.from('one'))]));
|
|
147
|
+
});
|
|
138
148
|
const connection = new IggyConnection(connectionConfig(server));
|
|
139
149
|
try {
|
|
140
150
|
await connection.connect();
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
connection.socket.emit('error', Object.assign(new Error('reset'), { code: 'ECONNRESET' }));
|
|
145
|
-
assert.equal(emitted, false);
|
|
151
|
+
const [first, second] = await Promise.all([exchange(connection, 1n), exchange(connection, 2n)]);
|
|
152
|
+
assert.equal(first.response?.subarray(HEADER_SIZE).toString(), 'one');
|
|
153
|
+
assert.equal(second.response?.subarray(HEADER_SIZE).toString(), 'two');
|
|
146
154
|
}
|
|
147
155
|
finally {
|
|
148
156
|
await closeConnection(connection, server);
|
|
149
157
|
}
|
|
150
158
|
});
|
|
151
|
-
it('
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
serverSocket = socket;
|
|
159
|
+
it('isolates cancellation and discards a late reply without closing the socket', async () => {
|
|
160
|
+
const canceled = new AbortController();
|
|
161
|
+
let first;
|
|
162
|
+
const server = await startServer((frame, socket) => {
|
|
163
|
+
if (frame.readBigUInt64LE(REQUEST_OFFSET.request) === 1n) {
|
|
164
|
+
first = frame;
|
|
165
|
+
canceled.abort();
|
|
166
|
+
}
|
|
167
|
+
else
|
|
168
|
+
socket.write(Buffer.concat([replyFrame(first), replyFrame(frame, Buffer.from('alive'))]));
|
|
162
169
|
});
|
|
163
|
-
const connection = new IggyConnection(
|
|
170
|
+
const connection = new IggyConnection(connectionConfig(server));
|
|
164
171
|
try {
|
|
165
172
|
await connection.connect();
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
connection._destroy();
|
|
170
|
-
await assert.rejects(() => connection.connect(), /connection is closed/);
|
|
171
|
-
await new Promise((resolve) => setTimeout(resolve, 40));
|
|
172
|
-
assert.equal(connections, 1);
|
|
173
|
+
assert.ok((await exchange(connection, 1n, canceled.signal)).error);
|
|
174
|
+
assert.equal((await exchange(connection, 2n)).response?.subarray(HEADER_SIZE).toString(), 'alive');
|
|
175
|
+
assert.equal(connection.connected, true);
|
|
173
176
|
}
|
|
174
177
|
finally {
|
|
175
178
|
await closeConnection(connection, server);
|
|
176
179
|
}
|
|
177
180
|
});
|
|
178
|
-
it('
|
|
181
|
+
it('times out an expired deadline before building a frame', async () => {
|
|
179
182
|
const server = await startServer();
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
reconnect: { enabled: true, interval: 10, maxRetries: 2 }
|
|
183
|
-
};
|
|
184
|
-
let serverSocket;
|
|
185
|
-
let connections = 0;
|
|
186
|
-
server.on('connection', (socket) => {
|
|
187
|
-
connections += 1;
|
|
188
|
-
serverSocket = socket;
|
|
189
|
-
});
|
|
190
|
-
const connection = new IggyConnection(config);
|
|
191
|
-
connection.on('error', () => undefined);
|
|
183
|
+
const connection = new IggyConnection(connectionConfig(server));
|
|
184
|
+
let built = 0;
|
|
192
185
|
try {
|
|
193
186
|
await connection.connect();
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
const first = connection.connect();
|
|
200
|
-
assert.equal(connection.connect(), first);
|
|
201
|
-
assert.equal(await first, connection);
|
|
202
|
-
assert.equal(connection.connected, true);
|
|
203
|
-
assert.equal(connections, 2);
|
|
204
|
-
const frame = replyFrame(Buffer.from('fresh'));
|
|
205
|
-
const response = once(connection, 'response');
|
|
206
|
-
oldSocket.emit('data', Buffer.alloc(8));
|
|
207
|
-
connection._onData(frame);
|
|
208
|
-
assert.deepEqual((await response)[0], frame);
|
|
187
|
+
const result = await connection.exchange(() => { built += 1; return requestFrame(1n); }, Date.now() - 1, new AbortController().signal);
|
|
188
|
+
assert.ok(result.error instanceof VsrResponseTimeoutError);
|
|
189
|
+
assert.match(String(result.error), /after 0 ms/);
|
|
190
|
+
assert.equal(result.started, false);
|
|
191
|
+
assert.equal(built, 0);
|
|
209
192
|
}
|
|
210
193
|
finally {
|
|
211
194
|
await closeConnection(connection, server);
|
|
212
195
|
}
|
|
213
196
|
});
|
|
214
|
-
it('
|
|
197
|
+
it('reports a caller abort ahead of an expired deadline', async () => {
|
|
215
198
|
const server = await startServer();
|
|
216
|
-
const
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
...connectionConfig(server),
|
|
221
|
-
reconnect: { enabled: true, interval: 10, maxRetries: 1 }
|
|
222
|
-
});
|
|
223
|
-
connection.on('error', () => undefined);
|
|
199
|
+
const connection = new IggyConnection(connectionConfig(server));
|
|
200
|
+
const cancel = new AbortController();
|
|
201
|
+
const reason = new Error('caller canceled');
|
|
202
|
+
let built = 0;
|
|
224
203
|
try {
|
|
225
204
|
await connection.connect();
|
|
226
|
-
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
await reconnected;
|
|
232
|
-
assert.equal(connection.config.options.host, '127.0.0.1');
|
|
233
|
-
assert.equal(connection.config.options.port, seedPort);
|
|
234
|
-
assert.equal(connection.connected, true);
|
|
235
|
-
assert.equal(connection.isConnectedTo('127.0.0.1', unavailablePort), false);
|
|
205
|
+
cancel.abort(reason);
|
|
206
|
+
const result = await connection.exchange(() => { built += 1; return requestFrame(1n); }, Date.now() - 1, cancel.signal);
|
|
207
|
+
assert.equal(result.error, reason);
|
|
208
|
+
assert.equal(result.started, false);
|
|
209
|
+
assert.equal(built, 0);
|
|
236
210
|
}
|
|
237
211
|
finally {
|
|
238
212
|
await closeConnection(connection, server);
|
|
239
213
|
}
|
|
240
214
|
});
|
|
241
|
-
it('
|
|
242
|
-
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
215
|
+
it('assembles partial frames and rejects an oversized declared frame', async () => {
|
|
216
|
+
let count = 0;
|
|
217
|
+
const server = await startServer((frame, socket) => {
|
|
218
|
+
const response = replyFrame(frame, Buffer.from('complete'));
|
|
219
|
+
if (++count === 1) {
|
|
220
|
+
socket.write(response.subarray(0, 6));
|
|
221
|
+
setImmediate(() => socket.write(response.subarray(6)));
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
response.writeUInt32LE(FRAME_LIMIT + 1, REPLY_OFFSET.size);
|
|
225
|
+
socket.write(response);
|
|
226
|
+
}
|
|
250
227
|
});
|
|
251
|
-
|
|
252
|
-
target.on('connection', () => {
|
|
253
|
-
targetConnections += 1;
|
|
254
|
-
});
|
|
255
|
-
const connection = new IggyConnection({
|
|
256
|
-
...connectionConfig(seed),
|
|
257
|
-
reconnect: { enabled: true, interval: 50, maxRetries: 3 }
|
|
258
|
-
});
|
|
259
|
-
connection.on('error', () => undefined);
|
|
228
|
+
const connection = new IggyConnection(connectionConfig(server));
|
|
260
229
|
try {
|
|
261
230
|
await connection.connect();
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
await connection.redirect('127.0.0.1', targetPort);
|
|
266
|
-
assert.equal(connection.connected, true);
|
|
267
|
-
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
268
|
-
assert.equal(connection.connected, true);
|
|
269
|
-
assert.equal(connection.isConnectedTo('127.0.0.1', targetPort), true);
|
|
270
|
-
assert.equal(seedConnections, 1);
|
|
271
|
-
assert.equal(targetConnections, 1);
|
|
231
|
+
assert.equal((await exchange(connection, 1n)).response?.subarray(HEADER_SIZE).toString(), 'complete');
|
|
232
|
+
assert.ok((await exchange(connection, 2n)).error instanceof ProtocolFrameError);
|
|
233
|
+
assert.equal(connection.connected, false);
|
|
272
234
|
}
|
|
273
235
|
finally {
|
|
274
|
-
connection
|
|
275
|
-
await new Promise((resolve) => seed.close(() => resolve()));
|
|
276
|
-
await new Promise((resolve) => target.close(() => resolve()));
|
|
236
|
+
await closeConnection(connection, server);
|
|
277
237
|
}
|
|
278
238
|
});
|
|
279
|
-
it('
|
|
239
|
+
it('bounds pending calls before frame allocation and releases capacity after cancellation', async () => {
|
|
280
240
|
const server = await startServer();
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
});
|
|
285
|
-
const rejections = [];
|
|
286
|
-
const onUnhandled = (reason) => rejections.push(reason);
|
|
287
|
-
process.on('unhandledRejection', onUnhandled);
|
|
288
|
-
const connection = new IggyConnection({
|
|
289
|
-
...connectionConfig(server),
|
|
290
|
-
reconnect: { enabled: true, interval: 10, maxRetries: 3 }
|
|
291
|
-
});
|
|
241
|
+
const connection = new IggyConnection(connectionConfig(server));
|
|
242
|
+
const cancel = new AbortController();
|
|
243
|
+
let allocated = 0;
|
|
292
244
|
try {
|
|
293
245
|
await connection.connect();
|
|
294
|
-
const
|
|
295
|
-
const onError = (error) => {
|
|
296
|
-
if (!error.message.includes('reconnect maxRetries exceeded'))
|
|
297
|
-
return;
|
|
298
|
-
connection.removeListener('error', onError);
|
|
299
|
-
resolve(error);
|
|
300
|
-
};
|
|
301
|
-
connection.on('error', onError);
|
|
302
|
-
});
|
|
303
|
-
const closed = new Promise((resolve) => server.close(() => resolve()));
|
|
304
|
-
serverSocket?.destroy();
|
|
305
|
-
const retryStartedAt = Date.now();
|
|
306
|
-
await closed;
|
|
307
|
-
const error = await exhausted;
|
|
308
|
-
// Three retries at a 10 ms interval must spend at least 30 ms in
|
|
309
|
-
// backoff; a broken wait would redial back to back.
|
|
310
|
-
assert.ok(Date.now() - retryStartedAt >= 30, 'reconnect backoff did not elapse between retries');
|
|
311
|
-
assert.match(error.message, /reconnect maxRetries exceeded/);
|
|
246
|
+
const calls = Array.from({ length: MAX_PENDING_PER_LINK + 1 }, (_, index) => connection.exchange(() => { allocated += 1; return requestFrame(BigInt(index + 1)); }, Date.now() + 2_000, cancel.signal));
|
|
312
247
|
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
313
|
-
assert.
|
|
248
|
+
assert.equal(allocated, MAX_PENDING_PER_LINK);
|
|
249
|
+
cancel.abort();
|
|
250
|
+
await Promise.all(calls);
|
|
251
|
+
assert.equal(allocated, MAX_PENDING_PER_LINK);
|
|
252
|
+
assert.equal(connection.connected, true);
|
|
314
253
|
}
|
|
315
254
|
finally {
|
|
316
|
-
|
|
317
|
-
connection
|
|
255
|
+
cancel.abort();
|
|
256
|
+
await closeConnection(connection, server);
|
|
318
257
|
}
|
|
319
258
|
});
|
|
320
|
-
it('
|
|
321
|
-
const
|
|
322
|
-
const
|
|
323
|
-
const
|
|
324
|
-
const
|
|
325
|
-
let
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
const
|
|
330
|
-
...connectionConfig(seed),
|
|
331
|
-
reconnect: { enabled: true, interval: 10, maxRetries: 4 }
|
|
332
|
-
});
|
|
333
|
-
connection.on('error', () => undefined);
|
|
259
|
+
it('bounds queued writers and pins canceled frame bytes until the write callback completes', async () => {
|
|
260
|
+
const received = [];
|
|
261
|
+
const server = await startServer((frame) => { received.push(frame); });
|
|
262
|
+
const connection = new IggyConnection(connectionConfig(server));
|
|
263
|
+
const cancel = new AbortController();
|
|
264
|
+
let released = false;
|
|
265
|
+
const owner = new FrozenFrame(requestFrame(1n), () => { released = true; });
|
|
266
|
+
let completeWrite;
|
|
267
|
+
let writing;
|
|
268
|
+
const started = new Promise((resolve) => { writing = resolve; });
|
|
334
269
|
try {
|
|
335
270
|
await connection.connect();
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
const
|
|
342
|
-
targetSocket?.destroy();
|
|
343
|
-
await targetClosed;
|
|
344
|
-
await reconnected;
|
|
271
|
+
const write = connection.socket.write.bind(connection.socket);
|
|
272
|
+
connection.socket.write = ((chunk, callback) => write(chunk, (error) => { completeWrite = () => callback(error); writing(); }));
|
|
273
|
+
const first = connection.exchange(() => owner.buffer, Date.now() + 2_000, cancel.signal, owner);
|
|
274
|
+
await started;
|
|
275
|
+
owner.release();
|
|
276
|
+
const waiting = Array.from({ length: LINK_WRITE_QUEUE + 8 }, (_, index) => exchange(connection, BigInt(index + 2), cancel.signal));
|
|
345
277
|
await new Promise((resolve) => setImmediate(resolve));
|
|
346
|
-
assert.equal(connection.
|
|
347
|
-
|
|
348
|
-
assert.equal(
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
try {
|
|
360
|
-
connection.rememberRoster([
|
|
361
|
-
{ host: '127.0.0.1', port: seedPort },
|
|
362
|
-
{ host: '127.0.0.1', port: seedPort + 1 },
|
|
363
|
-
{ host: '127.0.0.1', port: seedPort + 2 }
|
|
364
|
-
]);
|
|
365
|
-
// The endpoint the client is on leads, the roster follows, and the
|
|
366
|
-
// roster's copy of that endpoint does not earn a second attempt.
|
|
367
|
-
assert.deepEqual(connection._redialCandidates().map((options) => options.port), [seedPort, seedPort + 1, seedPort + 2]);
|
|
278
|
+
assert.equal(connection.writes.length, LINK_WRITE_QUEUE);
|
|
279
|
+
cancel.abort();
|
|
280
|
+
assert.equal((await first).started, true);
|
|
281
|
+
assert.ok((await Promise.all(waiting)).every((result) => !result.started));
|
|
282
|
+
assert.equal(released, false);
|
|
283
|
+
assert.equal(owner.buffer.readBigUInt64LE(REQUEST_OFFSET.request), 1n);
|
|
284
|
+
connection.socket.write = write;
|
|
285
|
+
assert.ok(completeWrite);
|
|
286
|
+
completeWrite();
|
|
287
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
288
|
+
assert.equal(released, true);
|
|
289
|
+
assert.throws(() => owner.buffer, /released/);
|
|
290
|
+
assert.equal(received.length, 1);
|
|
368
291
|
}
|
|
369
292
|
finally {
|
|
370
|
-
|
|
371
|
-
|
|
293
|
+
cancel.abort();
|
|
294
|
+
completeWrite?.();
|
|
295
|
+
await closeConnection(connection, server);
|
|
372
296
|
}
|
|
373
297
|
});
|
|
374
|
-
it('
|
|
375
|
-
const
|
|
376
|
-
const
|
|
377
|
-
const connection = new IggyConnection(connectionConfig(seed));
|
|
378
|
-
connection.on('error', () => undefined);
|
|
298
|
+
it('expires an incomplete frame independently of the caller deadline', async (context) => {
|
|
299
|
+
const server = await startServer((frame, socket) => socket.write(replyFrame(frame, Buffer.from('body')).subarray(0, HEADER_SIZE + 1)));
|
|
300
|
+
const connection = new IggyConnection(connectionConfig(server));
|
|
379
301
|
try {
|
|
380
302
|
await connection.connect();
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
const
|
|
387
|
-
assert.
|
|
388
|
-
|
|
389
|
-
});
|
|
390
|
-
assert.deepEqual(connection.nextRosterEndpoint(visited), {
|
|
391
|
-
host: '127.0.0.1', port: seedPort + 2
|
|
392
|
-
});
|
|
393
|
-
assert.equal(connection.nextRosterEndpoint(visited), undefined);
|
|
394
|
-
assert.equal(visited.size, 3);
|
|
395
|
-
}
|
|
396
|
-
finally {
|
|
397
|
-
await closeConnection(connection, seed);
|
|
398
|
-
}
|
|
399
|
-
});
|
|
400
|
-
it('dials the endpoint it is on, then the seed, then the roster', async () => {
|
|
401
|
-
const seed = await startServer();
|
|
402
|
-
const seedPort = seed.address().port;
|
|
403
|
-
const connection = new IggyConnection(connectionConfig(seed));
|
|
404
|
-
connection.on('error', () => undefined);
|
|
405
|
-
try {
|
|
406
|
-
// A redirect moves the client off its seed; the seed is still the one
|
|
407
|
-
// endpoint the caller vouched for, so it comes before a roster the
|
|
408
|
-
// cluster may have reshaped since.
|
|
409
|
-
connection.config.options = {
|
|
410
|
-
...connection.config.options,
|
|
411
|
-
port: seedPort + 9
|
|
412
|
-
};
|
|
413
|
-
connection.rememberRoster([{ host: '127.0.0.1', port: seedPort + 5 }]);
|
|
414
|
-
assert.deepEqual(connection._redialCandidates().map((options) => options.port), [seedPort + 9, seedPort, seedPort + 5]);
|
|
415
|
-
}
|
|
416
|
-
finally {
|
|
417
|
-
connection._destroy();
|
|
418
|
-
await new Promise((resolve) => seed.close(() => resolve()));
|
|
419
|
-
}
|
|
420
|
-
});
|
|
421
|
-
it('counts endpoints that only differ in spelling once', async () => {
|
|
422
|
-
const seed = await startServer();
|
|
423
|
-
const seedPort = seed.address().port;
|
|
424
|
-
const connection = new IggyConnection(connectionConfig(seed));
|
|
425
|
-
connection.on('error', () => undefined);
|
|
426
|
-
try {
|
|
427
|
-
// The loopback aliases and an IPv4-mapped address all name the endpoint
|
|
428
|
-
// the client is already on, so none of them earns a dial of its own.
|
|
429
|
-
connection.rememberRoster([
|
|
430
|
-
{ host: 'localhost', port: seedPort },
|
|
431
|
-
{ host: '::1', port: seedPort },
|
|
432
|
-
{ host: '::ffff:127.0.0.1', port: seedPort },
|
|
433
|
-
{ host: '127.0.0.1', port: seedPort + 1 }
|
|
434
|
-
]);
|
|
435
|
-
assert.deepEqual(connection._redialCandidates().map((options) => options.port), [seedPort, seedPort + 1]);
|
|
436
|
-
}
|
|
437
|
-
finally {
|
|
438
|
-
connection._destroy();
|
|
439
|
-
await new Promise((resolve) => seed.close(() => resolve()));
|
|
440
|
-
}
|
|
441
|
-
});
|
|
442
|
-
it('does not redial at all when reconnection is disabled', async () => {
|
|
443
|
-
// `enabled: false` is what a caller says to opt out. The retry budget is
|
|
444
|
-
// whatever the defaults hold, so a loop that reads it without checking
|
|
445
|
-
// this flag would run every one of those passes - and with the backoff
|
|
446
|
-
// gated on the same flag, back to back.
|
|
447
|
-
//
|
|
448
|
-
// The endpoint accepts and hangs up, so the drop that would start a
|
|
449
|
-
// redial happens and every dial of it is counted.
|
|
450
|
-
const hangup = await startServer();
|
|
451
|
-
const hangupPort = hangup.address().port;
|
|
452
|
-
let accepted = 0;
|
|
453
|
-
hangup.on('connection', (socket) => {
|
|
454
|
-
accepted += 1;
|
|
455
|
-
socket.destroy();
|
|
456
|
-
});
|
|
457
|
-
const connection = new IggyConnection({
|
|
458
|
-
transport: 'TCP',
|
|
459
|
-
options: { host: '127.0.0.1', port: hangupPort },
|
|
460
|
-
credentials: { username: 'iggy', password: 'iggy' },
|
|
461
|
-
reconnect: { enabled: false, interval: 10, maxRetries: 12 },
|
|
462
|
-
maxResponseFrameSize: FRAME_LIMIT
|
|
463
|
-
});
|
|
464
|
-
connection.on('error', () => undefined);
|
|
465
|
-
try {
|
|
466
|
-
await connection.connect().catch(() => undefined);
|
|
467
|
-
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
468
|
-
assert.equal(accepted, 1, 'a client that turned reconnection off redialed anyway');
|
|
469
|
-
assert.equal(connection.connected, false);
|
|
470
|
-
}
|
|
471
|
-
finally {
|
|
472
|
-
connection._destroy();
|
|
473
|
-
await new Promise((resolve) => hangup.close(() => resolve()));
|
|
474
|
-
}
|
|
475
|
-
});
|
|
476
|
-
it('sweeps the endpoints it knows once when reconnection is disabled', async () => {
|
|
477
|
-
// Opting out of retries is not opting out of the endpoints: with more
|
|
478
|
-
// than one known, they get exactly one pass and no backoff, as in the
|
|
479
|
-
// other SDKs.
|
|
480
|
-
const dead = await startServer();
|
|
481
|
-
const deadPort = dead.address().port;
|
|
482
|
-
await new Promise((resolve) => dead.close(() => resolve()));
|
|
483
|
-
const live = await startServer();
|
|
484
|
-
const livePort = live.address().port;
|
|
485
|
-
let accepted = 0;
|
|
486
|
-
live.on('connection', () => { accepted += 1; });
|
|
487
|
-
const connection = new IggyConnection({
|
|
488
|
-
transport: 'TCP',
|
|
489
|
-
options: { host: '127.0.0.1', port: deadPort },
|
|
490
|
-
credentials: { username: 'iggy', password: 'iggy' },
|
|
491
|
-
reconnect: { enabled: false, interval: 10, maxRetries: 12 },
|
|
492
|
-
maxResponseFrameSize: FRAME_LIMIT
|
|
493
|
-
});
|
|
494
|
-
connection.on('error', () => undefined);
|
|
495
|
-
try {
|
|
496
|
-
connection.rememberRoster([{ host: '127.0.0.1', port: livePort }]);
|
|
497
|
-
await connection.connect().catch(() => undefined);
|
|
498
|
-
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
499
|
-
assert.equal(accepted, 1, 'the known endpoints got either no pass or more than one');
|
|
500
|
-
}
|
|
501
|
-
finally {
|
|
502
|
-
connection._destroy();
|
|
503
|
-
await new Promise((resolve) => live.close(() => resolve()));
|
|
504
|
-
}
|
|
505
|
-
});
|
|
506
|
-
it('makes one pass when every endpoint is down and reconnection is disabled', async () => {
|
|
507
|
-
// One pass, not the whole retry budget: with the budget read but the
|
|
508
|
-
// flag ignored, a client that opted out of retries dials every endpoint
|
|
509
|
-
// once per pass for all of them -- and with the backoff gated on the same
|
|
510
|
-
// flag, back to back.
|
|
511
|
-
//
|
|
512
|
-
// Plain TCP behind a TLS client, closed at once: the dial fails, so the
|
|
513
|
-
// pass moves on, and every dial is counted where it lands.
|
|
514
|
-
const first = await startServer();
|
|
515
|
-
const firstPort = first.address().port;
|
|
516
|
-
let firstDials = 0;
|
|
517
|
-
first.on('connection', (socket) => {
|
|
518
|
-
firstDials += 1;
|
|
519
|
-
socket.destroy();
|
|
520
|
-
});
|
|
521
|
-
const second = await startServer();
|
|
522
|
-
const secondPort = second.address().port;
|
|
523
|
-
let secondDials = 0;
|
|
524
|
-
second.on('connection', (socket) => {
|
|
525
|
-
secondDials += 1;
|
|
526
|
-
socket.destroy();
|
|
527
|
-
});
|
|
528
|
-
const connection = new IggyConnection({
|
|
529
|
-
transport: 'TLS',
|
|
530
|
-
options: {
|
|
531
|
-
host: '127.0.0.1',
|
|
532
|
-
port: firstPort,
|
|
533
|
-
rejectUnauthorized: false
|
|
534
|
-
},
|
|
535
|
-
credentials: { username: 'iggy', password: 'iggy' },
|
|
536
|
-
reconnect: { enabled: false, interval: 10, maxRetries: 12 },
|
|
537
|
-
maxResponseFrameSize: FRAME_LIMIT
|
|
538
|
-
});
|
|
539
|
-
connection.on('error', () => undefined);
|
|
540
|
-
try {
|
|
541
|
-
connection.rememberRoster([{ host: '127.0.0.1', port: secondPort }]);
|
|
542
|
-
await connection.connect().catch(() => undefined);
|
|
543
|
-
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
544
|
-
assert.equal(connection.connected, false);
|
|
545
|
-
// The connect's own dial of the configured endpoint, then one pass over
|
|
546
|
-
// both: the endpoint the client starts on is dialed twice, the one
|
|
547
|
-
// behind it once.
|
|
548
|
-
assert.deepEqual([firstDials, secondDials], [2, 1], 'a client that opted out of retries swept more than once');
|
|
549
|
-
}
|
|
550
|
-
finally {
|
|
551
|
-
connection._destroy();
|
|
552
|
-
await new Promise((resolve) => first.close(() => resolve()));
|
|
553
|
-
await new Promise((resolve) => second.close(() => resolve()));
|
|
554
|
-
}
|
|
555
|
-
});
|
|
556
|
-
it('skips the first backoff when another endpoint is known', async () => {
|
|
557
|
-
// The endpoint the client is on is dead and a live one sits behind it in
|
|
558
|
-
// the roster: waiting out the interval before the first pass would push
|
|
559
|
-
// the failover past what the caller waits for, and the node just lost may
|
|
560
|
-
// be gone for good.
|
|
561
|
-
const dead = await startServer();
|
|
562
|
-
const deadPort = dead.address().port;
|
|
563
|
-
await new Promise((resolve) => dead.close(() => resolve()));
|
|
564
|
-
const live = await startServer();
|
|
565
|
-
const livePort = live.address().port;
|
|
566
|
-
const interval = 3000;
|
|
567
|
-
const connection = new IggyConnection({
|
|
568
|
-
transport: 'TCP',
|
|
569
|
-
options: { host: '127.0.0.1', port: deadPort },
|
|
570
|
-
credentials: { username: 'iggy', password: 'iggy' },
|
|
571
|
-
reconnect: { enabled: true, interval, maxRetries: 3 },
|
|
572
|
-
maxResponseFrameSize: FRAME_LIMIT
|
|
573
|
-
});
|
|
574
|
-
connection.on('error', () => undefined);
|
|
575
|
-
try {
|
|
576
|
-
connection.rememberRoster([{ host: '127.0.0.1', port: livePort }]);
|
|
577
|
-
const dialed = once(live, 'connection');
|
|
578
|
-
const started = Date.now();
|
|
579
|
-
void connection.connect().catch(() => undefined);
|
|
580
|
-
await dialed;
|
|
581
|
-
assert.ok(Date.now() - started < interval, 'the failover waited out the backoff before its first pass');
|
|
582
|
-
}
|
|
583
|
-
finally {
|
|
584
|
-
connection._destroy();
|
|
585
|
-
await new Promise((resolve) => live.close(() => resolve()));
|
|
586
|
-
}
|
|
587
|
-
});
|
|
588
|
-
it('bounds a dial that never becomes usable when others are queued behind it', async () => {
|
|
589
|
-
// Plain TCP behind a TLS client: the socket connects, so only a bound on
|
|
590
|
-
// the handshake ends the attempt. The endpoint behind it is dead, so the
|
|
591
|
-
// pass has to end on its own rather than hang on the first one.
|
|
592
|
-
const silent = await startServer();
|
|
593
|
-
const silentPort = silent.address().port;
|
|
594
|
-
const held = [];
|
|
595
|
-
silent.on('connection', (socket) => { held.push(socket); });
|
|
596
|
-
const dead = await startServer();
|
|
597
|
-
const deadPort = dead.address().port;
|
|
598
|
-
await new Promise((resolve) => dead.close(() => resolve()));
|
|
599
|
-
const connection = new IggyConnection({
|
|
600
|
-
transport: 'TLS',
|
|
601
|
-
options: {
|
|
602
|
-
host: '127.0.0.1',
|
|
603
|
-
port: silentPort,
|
|
604
|
-
rejectUnauthorized: false
|
|
605
|
-
},
|
|
606
|
-
credentials: { username: 'iggy', password: 'iggy' },
|
|
607
|
-
reconnect: { enabled: true, interval: 10, maxRetries: 3 },
|
|
608
|
-
maxResponseFrameSize: FRAME_LIMIT
|
|
609
|
-
});
|
|
610
|
-
connection.on('error', () => undefined);
|
|
611
|
-
try {
|
|
612
|
-
connection.rememberRoster([{ host: '127.0.0.1', port: deadPort }]);
|
|
613
|
-
void connection.connect().catch(() => undefined);
|
|
614
|
-
// Unbounded, the first dial never ends and this endpoint is dialed
|
|
615
|
-
// exactly once, forever.
|
|
616
|
-
const deadline = Date.now() + 8_000;
|
|
617
|
-
while (held.length < 2 && Date.now() < deadline)
|
|
618
|
-
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
619
|
-
assert.ok(held.length >= 2, 'a dial that never became usable held the pass');
|
|
303
|
+
context.mock.timers.enable({ apis: ['setTimeout'] });
|
|
304
|
+
const received = once(connection.socket, 'data');
|
|
305
|
+
const pending = connection.exchange(() => requestFrame(1n), Date.now() + OPERATION_TIMEOUT_MS * 2, new AbortController().signal);
|
|
306
|
+
await received;
|
|
307
|
+
context.mock.timers.tick(OPERATION_TIMEOUT_MS);
|
|
308
|
+
const result = await pending;
|
|
309
|
+
assert.ok(result.error instanceof ProtocolFrameError);
|
|
310
|
+
assert.match(String(result.error), /incomplete consensus frame timed out/);
|
|
620
311
|
assert.equal(connection.connected, false);
|
|
621
312
|
}
|
|
622
313
|
finally {
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
await new Promise((resolve) => silent.close(() => resolve()));
|
|
314
|
+
context.mock.timers.reset();
|
|
315
|
+
await closeConnection(connection, server);
|
|
626
316
|
}
|
|
627
317
|
});
|
|
628
|
-
it('
|
|
629
|
-
// The endpoint the client is on is dead, so every dial to it is refused
|
|
630
|
-
// - and the live roster endpoint behind it is what the pass would reach
|
|
631
|
-
// next, unless the destroy in between stops the pass.
|
|
632
|
-
const dead = await startServer();
|
|
633
|
-
const deadPort = dead.address().port;
|
|
634
|
-
await new Promise((resolve) => dead.close(() => resolve()));
|
|
635
|
-
const live = await startServer();
|
|
636
|
-
const livePort = live.address().port;
|
|
318
|
+
it('never reconnects a failed physical link', async () => {
|
|
637
319
|
let accepted = 0;
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
options: { host: '127.0.0.1', port: deadPort },
|
|
642
|
-
credentials: { username: 'iggy', password: 'iggy' },
|
|
643
|
-
reconnect: { enabled: true, interval: 10, maxRetries: 3 },
|
|
644
|
-
maxResponseFrameSize: FRAME_LIMIT
|
|
645
|
-
});
|
|
646
|
-
let destroyed = false;
|
|
647
|
-
let connectsAfterDestroy = 0;
|
|
648
|
-
connection.on('connect', () => {
|
|
649
|
-
if (destroyed)
|
|
650
|
-
connectsAfterDestroy += 1;
|
|
651
|
-
});
|
|
652
|
-
try {
|
|
653
|
-
connection.rememberRoster([{ host: '127.0.0.1', port: livePort }]);
|
|
654
|
-
// The first failure is the initial connect, which is what starts the
|
|
655
|
-
// redial pass; the next one is that pass's first candidate, so
|
|
656
|
-
// destroying there lands between two candidates rather than before the
|
|
657
|
-
// pass.
|
|
658
|
-
const destroyedMidPass = new Promise((resolve) => {
|
|
659
|
-
let failures = 0;
|
|
660
|
-
connection.on('error', () => {
|
|
661
|
-
failures += 1;
|
|
662
|
-
if (failures < 2 || destroyed)
|
|
663
|
-
return;
|
|
664
|
-
connection._destroy();
|
|
665
|
-
destroyed = true;
|
|
666
|
-
resolve();
|
|
667
|
-
});
|
|
668
|
-
});
|
|
669
|
-
await connection.connect().catch(() => undefined);
|
|
670
|
-
await destroyedMidPass;
|
|
671
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
672
|
-
assert.equal(accepted, 0, 'a destroyed connection must not keep dialing the rest of the pass');
|
|
673
|
-
assert.equal(connectsAfterDestroy, 0, 'a destroyed connection must not announce a connection');
|
|
674
|
-
assert.equal(connection.connected, false);
|
|
675
|
-
}
|
|
676
|
-
finally {
|
|
677
|
-
connection._destroy();
|
|
678
|
-
await new Promise((resolve) => live.close(() => resolve()));
|
|
679
|
-
}
|
|
680
|
-
});
|
|
681
|
-
it('settles a dial in flight when a redirect replaces the socket', async () => {
|
|
682
|
-
const seed = await startServer();
|
|
683
|
-
const target = await startServer();
|
|
684
|
-
const targetPort = target.address().port;
|
|
685
|
-
const connection = new IggyConnection(connectionConfig(seed));
|
|
686
|
-
connection.on('error', () => undefined);
|
|
687
|
-
try {
|
|
688
|
-
const pending = connection.connect();
|
|
689
|
-
const redirected = connection.redirect('127.0.0.1', targetPort);
|
|
690
|
-
await assert.rejects(() => pending, /connection closed before it was established/);
|
|
691
|
-
await redirected;
|
|
692
|
-
assert.equal(connection.connected, true);
|
|
693
|
-
assert.equal(connection.isConnectedTo('127.0.0.1', targetPort), true);
|
|
694
|
-
}
|
|
695
|
-
finally {
|
|
696
|
-
connection._destroy();
|
|
697
|
-
await new Promise((resolve) => seed.close(() => resolve()));
|
|
698
|
-
await new Promise((resolve) => target.close(() => resolve()));
|
|
699
|
-
}
|
|
700
|
-
});
|
|
701
|
-
it('shares the redirect dial with a disconnected listener', async () => {
|
|
702
|
-
const seed = await startServer();
|
|
703
|
-
const target = await startServer();
|
|
704
|
-
const targetPort = target.address().port;
|
|
705
|
-
let seedConnections = 0;
|
|
706
|
-
let targetConnections = 0;
|
|
707
|
-
seed.on('connection', () => {
|
|
708
|
-
seedConnections += 1;
|
|
709
|
-
});
|
|
710
|
-
target.on('connection', () => {
|
|
711
|
-
targetConnections += 1;
|
|
712
|
-
});
|
|
713
|
-
const connection = new IggyConnection(connectionConfig(seed));
|
|
714
|
-
connection.on('error', () => undefined);
|
|
715
|
-
let listenerConnection;
|
|
716
|
-
try {
|
|
717
|
-
await connection.connect();
|
|
718
|
-
connection.once('disconnected', () => {
|
|
719
|
-
listenerConnection = connection.connect();
|
|
720
|
-
});
|
|
721
|
-
await connection.redirect('127.0.0.1', targetPort);
|
|
722
|
-
assert.ok(listenerConnection);
|
|
723
|
-
assert.equal(await listenerConnection, connection);
|
|
724
|
-
assert.equal(connection.connected, true);
|
|
725
|
-
assert.equal(connection.isConnectedTo('127.0.0.1', targetPort), true);
|
|
726
|
-
assert.equal(seedConnections, 1);
|
|
727
|
-
assert.equal(targetConnections, 1);
|
|
728
|
-
}
|
|
729
|
-
finally {
|
|
730
|
-
connection._destroy();
|
|
731
|
-
await new Promise((resolve) => seed.close(() => resolve()));
|
|
732
|
-
await new Promise((resolve) => target.close(() => resolve()));
|
|
733
|
-
}
|
|
734
|
-
});
|
|
735
|
-
it('rejects an unreadable tls_ca_file with a TypeError at socket creation', () => {
|
|
736
|
-
assert.throws(() => new IggyConnection({
|
|
737
|
-
transport: 'TLS',
|
|
738
|
-
options: {
|
|
739
|
-
host: '127.0.0.1',
|
|
740
|
-
port: 8090,
|
|
741
|
-
caFile: '/does/not/exist.pem'
|
|
742
|
-
},
|
|
743
|
-
credentials: { username: 'iggy', password: 'iggy' },
|
|
744
|
-
reconnect: { enabled: false, interval: 0, maxRetries: 0 }
|
|
745
|
-
}), /cannot read tls_ca_file/);
|
|
746
|
-
});
|
|
747
|
-
it('sends a DNS host as the SNI server name when none is set', async () => {
|
|
748
|
-
const server = await startTlsServer();
|
|
749
|
-
const secureConnection = once(server, 'secureConnection');
|
|
750
|
-
const connection = new IggyConnection({
|
|
751
|
-
transport: 'TLS',
|
|
752
|
-
options: {
|
|
753
|
-
host: 'localhost',
|
|
754
|
-
port: server.address().port,
|
|
755
|
-
ca: TLS_CA_CERTIFICATE
|
|
756
|
-
},
|
|
757
|
-
credentials: { username: 'iggy', password: 'iggy' },
|
|
758
|
-
reconnect: { enabled: false, interval: 0, maxRetries: 0 }
|
|
759
|
-
});
|
|
320
|
+
const server = await startServer((_frame, socket) => socket.destroy());
|
|
321
|
+
server.on('connection', () => { accepted += 1; });
|
|
322
|
+
const connection = new IggyConnection(connectionConfig(server));
|
|
760
323
|
try {
|
|
761
324
|
await connection.connect();
|
|
762
|
-
assert.
|
|
763
|
-
|
|
764
|
-
assert.equal(
|
|
325
|
+
assert.ok((await exchange(connection, 1n)).error instanceof ConnectionClosedError);
|
|
326
|
+
await assert.rejects(connection.connect());
|
|
327
|
+
assert.equal(accepted, 1);
|
|
765
328
|
}
|
|
766
329
|
finally {
|
|
767
330
|
await closeConnection(connection, server);
|
|
768
331
|
}
|
|
769
332
|
});
|
|
770
|
-
it('
|
|
771
|
-
const server = await
|
|
772
|
-
const
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
options: {
|
|
776
|
-
host: '127.0.0.1',
|
|
777
|
-
port: server.address().port,
|
|
778
|
-
rejectUnauthorized: false
|
|
779
|
-
},
|
|
780
|
-
credentials: { username: 'iggy', password: 'iggy' },
|
|
781
|
-
reconnect: { enabled: false, interval: 0, maxRetries: 0 }
|
|
782
|
-
});
|
|
333
|
+
it('bounds TCP plus TLS establishment when the peer never completes TLS', async () => {
|
|
334
|
+
const server = await startServer();
|
|
335
|
+
const connection = new IggyConnection({ ...connectionConfig(server), transport: 'TLS',
|
|
336
|
+
options: { ...connectionConfig(server).options, rejectUnauthorized: false } });
|
|
337
|
+
const started = Date.now();
|
|
783
338
|
try {
|
|
784
|
-
await connection.connect();
|
|
785
|
-
assert.
|
|
786
|
-
|
|
787
|
-
// Node reports a missing SNI name as false on the server side.
|
|
788
|
-
assert.ok(!tlsSocket.servername);
|
|
339
|
+
await assert.rejects(connection.connect(), /timed out/);
|
|
340
|
+
assert.ok(Date.now() - started < 3_000);
|
|
341
|
+
assert.equal(connection.connected, false);
|
|
789
342
|
}
|
|
790
343
|
finally {
|
|
791
344
|
await closeConnection(connection, server);
|
|
792
345
|
}
|
|
793
346
|
});
|
|
794
|
-
|
|
347
|
+
it('rejects an unreadable CA file when connecting without leaking a socket', async () => {
|
|
348
|
+
const connection = new IggyConnection({ transport: 'TLS', options: { host: '127.0.0.1', port: 8090,
|
|
349
|
+
caFile: '/does/not/exist.pem' }, credentials: { username: 'iggy', password: 'iggy' } });
|
|
350
|
+
await assert.rejects(connection.connect(), /cannot read tls_ca_file/);
|
|
351
|
+
connection._destroy();
|
|
352
|
+
});
|
|
353
|
+
for (const host of ['localhost', '127.0.0.1']) {
|
|
354
|
+
it(`uses the correct TLS server name for ${host}`, async () => {
|
|
355
|
+
const server = await startServer(undefined, true);
|
|
356
|
+
const secured = once(server, 'secureConnection');
|
|
357
|
+
const connection = new IggyConnection({ ...connectionConfig(server), transport: 'TLS',
|
|
358
|
+
options: { host, port: server.address().port,
|
|
359
|
+
...(host === 'localhost' ? { ca: TLS_CA_CERTIFICATE } : { rejectUnauthorized: false }) } });
|
|
360
|
+
try {
|
|
361
|
+
await connection.connect();
|
|
362
|
+
const [socket] = await secured;
|
|
363
|
+
assert.equal(socket.servername || undefined, host === 'localhost' ? host : undefined);
|
|
364
|
+
}
|
|
365
|
+
finally {
|
|
366
|
+
await closeConnection(connection, server);
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
}
|
|
795
370
|
});
|
|
796
371
|
//# sourceMappingURL=client.connection.test.js.map
|