blob: 750635092b096491295dd9d7b840213e3750ba6e [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import assert from 'node:assert/strict';
import { once } from 'node:events';
import { readFileSync } from 'node:fs';
import type { AddressInfo, Socket } from 'node:net';
import { createServer, type Server } from 'node:net';
import { describe, it } from 'node:test';
import { createServer as createTlsServer } from 'node:tls';
import { COMMAND_CODE } from '../wire/command.code.js';
import { ResponseError } from '../wire/error.utils.js';
import {
Command,
EVICTION_OFFSET,
EvictionReason,
HEADER_SIZE,
REPLY_OFFSET,
REQUEST_OFFSET
} from '../wire/vsr/header.js';
import { Operation } from '../wire/vsr/operation.js';
import { VsrEvictionError } from '../wire/vsr/reply.js';
import { CommandResponseStream } from './client.socket.js';
import type { ClientConfig, CommandResponse } from './client.type.js';
const TEST_SESSION = 42n;
const TLS_CERTIFICATE = readFileSync(
new URL('../../../../core/certs/iggy_cert.pem', import.meta.url)
);
const TLS_KEY = readFileSync(
new URL('../../../../core/certs/iggy_key.pem', import.meta.url)
);
const TLS_CA_CERTIFICATE = readFileSync(
new URL('../../../../core/certs/iggy_ca_cert.pem', import.meta.url)
);
type FrameHandler = (frame: Buffer, socket: Socket) => void;
type VsrTestServer = {
port: number,
frames: Buffer[],
close: () => Promise<void>
};
/** Loopback server speaking just enough VSR framing for the client tests. */
const startVsrServer = async (
handler: FrameHandler,
transport: 'TCP' | 'TLS' = 'TCP'
): Promise<VsrTestServer> => {
const frames: Buffer[] = [];
const handleConnection = (socket: Socket) => {
let pending = Buffer.alloc(0);
socket.on('data', (data: Buffer) => {
pending = Buffer.concat([pending, data]);
while (pending.length >= HEADER_SIZE) {
const size = pending.readUInt32LE(REQUEST_OFFSET.size);
if (pending.length < size) break;
const frame = pending.subarray(0, size);
pending = pending.subarray(size);
frames.push(Buffer.from(frame));
handler(Buffer.from(frame), socket);
}
});
socket.on('error', () => {});
};
const server: Server = transport === 'TLS'
? createTlsServer(
{ cert: TLS_CERTIFICATE, key: TLS_KEY },
handleConnection
)
: createServer(handleConnection);
server.listen(0, '127.0.0.1');
await once(server, 'listening');
return {
port: (server.address() as AddressInfo).port,
frames,
close: () => new Promise((resolve) => server.close(() => resolve()))
};
};
const replyFrame = (
operation: number,
body: Buffer = Buffer.alloc(0),
status = 0
): Buffer => {
const frame = Buffer.alloc(HEADER_SIZE + body.length);
frame.writeUInt32LE(frame.length, REPLY_OFFSET.size);
frame.writeUInt8(Command.Reply, REPLY_OFFSET.command);
frame.writeUInt8(operation, REPLY_OFFSET.operation);
frame.writeUInt32LE(status, REPLY_OFFSET.status);
body.copy(frame, HEADER_SIZE);
return frame;
};
const evictionFrame = (reason: number): Buffer => {
const frame = Buffer.alloc(HEADER_SIZE);
frame.writeUInt32LE(HEADER_SIZE, REPLY_OFFSET.size);
frame.writeUInt8(Command.Eviction, REPLY_OFFSET.command);
frame.writeUInt8(reason, EVICTION_OFFSET.reason);
return frame;
};
const registerReplyBody = (): Buffer => {
const serverVersion = Buffer.from('0.0.0');
const body = Buffer.alloc(4 + 17 + serverVersion.length);
body.writeUInt32LE(0, 0);
body.writeUInt32LE(7, 4);
body.writeBigUInt64LE(TEST_SESSION, 8);
body.writeUInt32LE(0, 16);
body.writeUInt8(serverVersion.length, 20);
serverVersion.copy(body, 21);
return body;
};
const singleNodeMetadataBody = (port: number): Buffer => {
const name = Buffer.from('single-node');
const nodeName = Buffer.from('iggy-node');
const ip = Buffer.from('127.0.0.1');
const body = Buffer.alloc(
4 + name.length + 4 + 4 + nodeName.length + 4 + ip.length + 8 + 2
);
let offset = 0;
body.writeUInt32LE(name.length, offset); offset += 4;
name.copy(body, offset); offset += name.length;
body.writeUInt32LE(1, offset); offset += 4;
body.writeUInt32LE(nodeName.length, offset); offset += 4;
nodeName.copy(body, offset); offset += nodeName.length;
body.writeUInt32LE(ip.length, offset); offset += 4;
ip.copy(body, offset); offset += ip.length;
body.writeUInt16LE(port, offset); offset += 8;
body.writeUInt8(0, offset); offset += 1;
body.writeUInt8(0, offset);
return body;
};
const twoNodeMetadataBody = (
followerPort: number,
leaderPort: number,
leaderRole = 0,
leaderStatus = 0
): Buffer => {
const node = (
nodeName: Buffer,
port: number,
role: number,
status = 0
): Buffer => {
const ip = Buffer.from('127.0.0.1');
const encoded = Buffer.alloc(4 + nodeName.length + 4 + ip.length + 8 + 2);
let offset = 0;
encoded.writeUInt32LE(nodeName.length, offset); offset += 4;
nodeName.copy(encoded, offset); offset += nodeName.length;
encoded.writeUInt32LE(ip.length, offset); offset += 4;
ip.copy(encoded, offset); offset += ip.length;
encoded.writeUInt16LE(port, offset); offset += 8;
encoded.writeUInt8(role, offset); offset += 1;
encoded.writeUInt8(status, offset);
return encoded;
};
const name = Buffer.from('iggy-cluster');
const header = Buffer.alloc(4 + name.length + 4);
header.writeUInt32LE(name.length, 0);
name.copy(header, 4);
header.writeUInt32LE(2, 4 + name.length);
return Buffer.concat([
header,
node(Buffer.from('iggy-node-1'), leaderPort, leaderRole, leaderStatus),
node(Buffer.from('iggy-node-2'), followerPort, 1)
]);
};
const threeNodeMetadataBody = (
firstPort: number,
secondPort: number,
thirdPort: number
): Buffer => {
const node = (name: string, port: number, role: number): Buffer => {
const nodeName = Buffer.from(name);
const ip = Buffer.from('127.0.0.1');
const encoded = Buffer.alloc(4 + nodeName.length + 4 + ip.length + 8 + 2);
let offset = 0;
encoded.writeUInt32LE(nodeName.length, offset); offset += 4;
nodeName.copy(encoded, offset); offset += nodeName.length;
encoded.writeUInt32LE(ip.length, offset); offset += 4;
ip.copy(encoded, offset); offset += ip.length;
encoded.writeUInt16LE(port, offset); offset += 8;
encoded.writeUInt8(role, offset); offset += 1;
encoded.writeUInt8(0, offset);
return encoded;
};
const name = Buffer.from('iggy-cluster');
const header = Buffer.alloc(4 + name.length + 4);
header.writeUInt32LE(name.length, 0);
name.copy(header, 4);
header.writeUInt32LE(3, 4 + name.length);
return Buffer.concat([
header,
node('iggy-node-1', firstPort, 0),
node('iggy-node-2', secondPort, 1),
node('iggy-node-3', thirdPort, 1)
]);
};
/** Register, metadata, and echo behavior of a healthy single VSR node. */
const singleNodeHandler = (port: number): FrameHandler =>
(frame, socket) => {
const operation = frame.readUInt8(REQUEST_OFFSET.operation);
if (operation === Operation.Register) {
socket.write(replyFrame(Operation.Register, registerReplyBody()));
return;
}
const code = frame.readUInt32LE(REQUEST_OFFSET.reserved);
if (code === COMMAND_CODE.GetClusterMetadata) {
socket.write(
replyFrame(Operation.NonReplicated, singleNodeMetadataBody(port))
);
return;
}
socket.write(replyFrame(operation));
};
const vsrConfig = (port: number): ClientConfig => ({
transport: 'TCP',
options: { host: '127.0.0.1', port },
credentials: { username: 'iggy', password: 'iggy' },
reconnect: { enabled: false, interval: 100, maxRetries: 1 }
});
/** Shrinks the leaderless poll so a test observes it without waiting on it. */
/** The queue a command waits in, for parking one the way the client does. */
const execQueue = (client: CommandResponseStream): {
command: number,
payload: Buffer,
handleResponse: boolean,
deadline: number,
resolve: (v: CommandResponse | PromiseLike<CommandResponse>) => void,
reject: (e: unknown) => void
}[] => (client as unknown as { _execQueue: never[] })._execQueue;
/** The connection under a stream, for driving a redirect the way a move does. */
const connectionOf = (client: CommandResponseStream): {
redirect: (host: string, port: number) => Promise<void>
} => (client as unknown as {
connection: { redirect: (host: string, port: number) => Promise<void> }
}).connection;
const compressLeaderlessPoll = (
client: CommandResponseStream,
budget: number
): void => {
const settlement = client as unknown as {
leaderlessWaitBudget: number,
leaderlessPollInterval: number
};
settlement.leaderlessWaitBudget = budget;
settlement.leaderlessPollInterval = 1;
};
describe('VSR client socket', () => {
it('exchanges VSR frames over TLS', async () => {
const server = await startVsrServer(
(frame, socket) => singleNodeHandler(server.port)(frame, socket),
'TLS'
);
const client = new CommandResponseStream({
...vsrConfig(server.port),
transport: 'TLS',
options: {
host: '127.0.0.1',
port: server.port,
servername: 'localhost',
ca: TLS_CA_CERTIFICATE
}
});
try {
const response = await client.sendCommand(60_015, Buffer.from('tls'));
assert.equal(response.status, 0);
assert.equal(server.frames.length, 3);
} finally {
client.destroy();
await server.close();
}
});
it('registers one session before the first authenticated command', async () => {
const server = await startVsrServer(
(frame, socket) => singleNodeHandler(server.port)(frame, socket)
);
const client = new CommandResponseStream(vsrConfig(server.port));
try {
const response = await client.sendCommand(
60_001,
Buffer.from('opaque')
);
assert.equal(response.status, 0);
const operations = server.frames.map(
(frame) => frame.readUInt8(REQUEST_OFFSET.operation)
);
assert.deepEqual(operations, [
Operation.Register,
Operation.NonReplicated,
Operation.NonReplicated
]);
const register = server.frames[0];
assert.equal(register.readBigUInt64LE(REQUEST_OFFSET.request), 0n);
assert.equal(register.readBigUInt64LE(REQUEST_OFFSET.session), 0n);
const settlement = server.frames[1];
assert.equal(
settlement.readUInt32LE(REQUEST_OFFSET.reserved),
COMMAND_CODE.GetClusterMetadata
);
const request = server.frames[2];
assert.equal(
request.readBigUInt64LE(REQUEST_OFFSET.session),
TEST_SESSION
);
assert.equal(request.readUInt32LE(REQUEST_OFFSET.reserved), 60_001);
assert.equal(client.isAuthenticated, true);
assert.equal(client.userId, 7);
} finally {
client.destroy();
await server.close();
}
});
it('replays the identical frame on a transient response', async () => {
let attempts = 0;
const server = await startVsrServer((frame, socket) => {
const operation = frame.readUInt8(REQUEST_OFFSET.operation);
if (operation !== Operation.NonReplicated ||
frame.readUInt32LE(REQUEST_OFFSET.reserved) !== 60_002)
return singleNodeHandler(server.port)(frame, socket);
attempts += 1;
socket.write(
attempts === 1
? replyFrame(operation, Buffer.alloc(0), 57)
: replyFrame(operation, Buffer.from('done'))
);
});
const client = new CommandResponseStream(vsrConfig(server.port));
try {
const response = await client.sendCommand(
60_002,
Buffer.from('retry-me')
);
assert.equal(attempts, 2);
assert.deepEqual(response.data, Buffer.from('done'));
const sent = server.frames.filter(
(frame) => frame.readUInt32LE(REQUEST_OFFSET.reserved) === 60_002
);
assert.equal(sent.length, 2);
assert.deepEqual(sent[0], sent[1]);
} finally {
client.destroy();
await server.close();
}
});
it('routes eviction out of band without desynchronizing replies', async () => {
const server = await startVsrServer((frame, socket) => {
if (frame.readUInt32LE(REQUEST_OFFSET.reserved) === 60_003) {
socket.write(Buffer.concat([
evictionFrame(EvictionReason.NoSession),
replyFrame(Operation.NonReplicated, Buffer.from('stale'))
]));
return;
}
if (frame.readUInt32LE(REQUEST_OFFSET.reserved) === 60_014) {
socket.write(
replyFrame(Operation.NonReplicated, Buffer.from('fresh'))
);
return;
}
singleNodeHandler(server.port)(frame, socket);
});
const client = new CommandResponseStream(vsrConfig(server.port));
try {
let sessionResets = 0;
let evictions = 0;
client.on('sessionReset', () => { sessionResets += 1; });
client.on('eviction', () => { evictions += 1; });
await assert.rejects(
() => client.sendCommand(
60_003,
Buffer.alloc(0)
),
(error: unknown) =>
error instanceof VsrEvictionError && error.errorCode === 40
);
assert.ok(sessionResets >= 1);
assert.equal(evictions, 1);
assert.equal(client.isAuthenticated, false);
const response = await client.sendCommand(60_014, Buffer.alloc(0));
assert.deepEqual(response.data, Buffer.from('fresh'));
} finally {
client.destroy();
await server.close();
}
});
it('fails an in-flight request when the connection drops', async () => {
const server = await startVsrServer((frame, socket) => {
if (frame.readUInt32LE(REQUEST_OFFSET.reserved) === 60_004) {
socket.destroy();
return;
}
singleNodeHandler(server.port)(frame, socket);
});
const client = new CommandResponseStream(vsrConfig(server.port));
try {
await assert.rejects(
() => client.sendCommand(
60_004,
Buffer.alloc(0)
)
);
assert.equal(client.isAuthenticated, false);
} finally {
client.destroy();
await server.close();
}
});
it('redirects a login to the advertised leader and registers there',
async () => {
const leader = await startVsrServer(
(frame, socket) => singleNodeHandler(leader.port)(frame, socket)
);
const follower = await startVsrServer((frame, socket) => {
const operation = frame.readUInt8(REQUEST_OFFSET.operation);
if (operation === Operation.Register) {
socket.write(replyFrame(Operation.Register, registerReplyBody()));
return;
}
if (frame.readUInt32LE(REQUEST_OFFSET.reserved) ===
COMMAND_CODE.GetClusterMetadata) {
socket.write(replyFrame(
Operation.NonReplicated,
twoNodeMetadataBody(follower.port, leader.port)
));
return;
}
socket.write(replyFrame(operation, Buffer.alloc(0), 58));
});
const client = new CommandResponseStream(vsrConfig(follower.port));
try {
const response = await client.sendCommand(
COMMAND_CODE.LoginUser,
Buffer.concat([
Buffer.from([4]),
Buffer.from('iggy'),
Buffer.from([4]),
Buffer.from('iggy')
]));
assert.equal(response.status, 0);
assert.equal(client.isAuthenticated, true);
const followerOperations = follower.frames.map(
(frame) => frame.readUInt8(REQUEST_OFFSET.operation)
);
assert.deepEqual(followerOperations, [
Operation.Register,
Operation.NonReplicated
]);
await client.sendCommand(60_018, Buffer.alloc(0));
const leaderOperations = leader.frames.map(
(frame) => frame.readUInt8(REQUEST_OFFSET.operation)
);
assert.deepEqual(leaderOperations, [
Operation.Register,
Operation.NonReplicated,
Operation.NonReplicated
]);
assert.equal(
leader.frames[2].readUInt32LE(REQUEST_OFFSET.reserved),
60_018
);
assert.equal(follower.frames.length, 2);
} finally {
client.destroy();
await leader.close();
await follower.close();
}
});
it('rechecks leadership after a redirected login', async () => {
const leader = await startVsrServer(
(frame, socket) => singleNodeHandler(leader.port)(frame, socket)
);
const intermediate = await startVsrServer((frame, socket) => {
const operation = frame.readUInt8(REQUEST_OFFSET.operation);
if (operation === Operation.Register) {
socket.write(replyFrame(Operation.Register, registerReplyBody()));
return;
}
socket.write(replyFrame(
Operation.NonReplicated,
twoNodeMetadataBody(intermediate.port, leader.port)
));
});
const follower = await startVsrServer((frame, socket) => {
const operation = frame.readUInt8(REQUEST_OFFSET.operation);
if (operation === Operation.Register) {
socket.write(replyFrame(Operation.Register, registerReplyBody()));
return;
}
socket.write(replyFrame(
Operation.NonReplicated,
twoNodeMetadataBody(follower.port, intermediate.port)
));
});
const client = new CommandResponseStream(vsrConfig(follower.port));
try {
await client.authenticate(vsrConfig(follower.port).credentials);
await client.sendCommand(60_019, Buffer.alloc(0));
assert.deepEqual(
follower.frames.map((frame) => frame.readUInt8(REQUEST_OFFSET.operation)),
[Operation.Register, Operation.NonReplicated]
);
assert.deepEqual(
intermediate.frames.map(
(frame) => frame.readUInt8(REQUEST_OFFSET.operation)
),
[Operation.Register, Operation.NonReplicated]
);
assert.equal(
leader.frames[2].readUInt32LE(REQUEST_OFFSET.reserved),
60_019
);
} finally {
client.destroy();
await follower.close();
await intermediate.close();
await leader.close();
}
});
// The node a client authenticated on dies; its next command has to complete
// on a survivor the roster named, under a session established there.
// Mirrors `core/integration/tests/cluster/failover_client_continuity.rs`.
it('resumes on a survivor after the node it authenticated on dies',
async () => {
const primarySockets = new Set<Socket>();
let primaryDead = false;
const survivor = await startVsrServer((frame, socket) => {
const operation = frame.readUInt8(REQUEST_OFFSET.operation);
if (operation === Operation.Register) {
socket.write(replyFrame(Operation.Register, registerReplyBody()));
return;
}
const code = frame.readUInt32LE(REQUEST_OFFSET.reserved);
if (code === COMMAND_CODE.GetClusterMetadata) {
// The survivor leads once the primary is gone.
socket.write(replyFrame(
Operation.NonReplicated,
twoNodeMetadataBody(primary.port, survivor.port)
));
return;
}
socket.write(replyFrame(operation));
});
const primary = await startVsrServer((frame, socket) => {
primarySockets.add(socket);
if (primaryDead) {
socket.destroy();
return;
}
const operation = frame.readUInt8(REQUEST_OFFSET.operation);
if (operation === Operation.Register) {
socket.write(replyFrame(Operation.Register, registerReplyBody()));
return;
}
const code = frame.readUInt32LE(REQUEST_OFFSET.reserved);
if (code === COMMAND_CODE.GetClusterMetadata) {
// The primary leads, so the login settles here and the roster is
// only remembered, not acted on, until the node dies.
socket.write(replyFrame(
Operation.NonReplicated,
twoNodeMetadataBody(survivor.port, primary.port)
));
return;
}
socket.write(replyFrame(operation));
});
const config: ClientConfig = {
...vsrConfig(primary.port),
reconnect: { enabled: true, interval: 1, maxRetries: 3 }
};
const client = new CommandResponseStream(config);
try {
await client.authenticate(config.credentials);
await client.sendCommand(60_021, Buffer.alloc(0));
assert.ok(
primary.frames.some(
(frame) => frame.readUInt32LE(REQUEST_OFFSET.reserved) === 60_021
),
'the live primary answered the first command'
);
primaryDead = true;
for (const socket of primarySockets)
socket.destroy();
await primary.close();
// The attempt in flight when the socket died is allowed to fail; the
// one after it has to land on the survivor. Two attempts, not a
// polling loop: the comment above promises at most one failed
// submission, and a loop of twenty would pass with nineteen failures.
let resumed = false;
let lastError: unknown;
for (let attempt = 0; attempt < 2 && !resumed; attempt += 1) {
try {
await client.sendCommand(60_021, Buffer.alloc(0));
resumed = true;
} catch (error) {
lastError = error;
}
}
assert.ok(resumed, `the client never resumed: ${String(lastError)}`);
const operations = survivor.frames.map(
(frame) => frame.readUInt8(REQUEST_OFFSET.operation)
);
assert.ok(
operations.includes(Operation.Register),
'the client signed in again on the survivor'
);
assert.ok(
survivor.frames.some(
(frame) => frame.readUInt32LE(REQUEST_OFFSET.reserved) === 60_021
),
'the command landed on the survivor the roster named'
);
} finally {
client.destroy();
await survivor.close();
}
});
it('keeps a single-node login on its node', async () => {
const server = await startVsrServer(
(frame, socket) => singleNodeHandler(server.port)(frame, socket)
);
const client = new CommandResponseStream(vsrConfig(server.port));
try {
await client.authenticate(vsrConfig(server.port).credentials);
const operations = server.frames.map(
(frame) => frame.readUInt8(REQUEST_OFFSET.operation)
);
assert.deepEqual(operations, [
Operation.Register,
Operation.NonReplicated
]);
assert.equal(
server.frames[1].readUInt32LE(REQUEST_OFFSET.reserved),
COMMAND_CODE.GetClusterMetadata
);
assert.equal(client.isAuthenticated, true);
} finally {
client.destroy();
await server.close();
}
});
it('polls a leaderless roster before redirecting to the elected leader',
async () => {
const leader = await startVsrServer(
(frame, socket) => singleNodeHandler(leader.port)(frame, socket)
);
let rosterReads = 0;
const follower = await startVsrServer((frame, socket) => {
const operation = frame.readUInt8(REQUEST_OFFSET.operation);
if (operation === Operation.Register) {
socket.write(replyFrame(Operation.Register, registerReplyBody()));
return;
}
rosterReads += 1;
// The first answer is mid-election: neither node holds the leader role.
socket.write(replyFrame(
Operation.NonReplicated,
rosterReads === 1
? twoNodeMetadataBody(follower.port, leader.port, 1)
: twoNodeMetadataBody(follower.port, leader.port)
));
});
const client = new CommandResponseStream(vsrConfig(follower.port));
compressLeaderlessPoll(client, 1_000);
try {
await client.authenticate(vsrConfig(follower.port).credentials);
assert.equal(rosterReads, 2);
const leaderOperations = leader.frames.map(
(frame) => frame.readUInt8(REQUEST_OFFSET.operation)
);
assert.deepEqual(leaderOperations, [
Operation.Register,
Operation.NonReplicated
]);
assert.equal(client.isAuthenticated, true);
} finally {
client.destroy();
await leader.close();
await follower.close();
}
});
it('keeps a login alive when the session dies mid-poll', async () => {
let rosterReads = 0;
const server = await startVsrServer((frame, socket) => {
const operation = frame.readUInt8(REQUEST_OFFSET.operation);
if (operation === Operation.Register) {
socket.write(replyFrame(Operation.Register, registerReplyBody()));
return;
}
rosterReads += 1;
// The roster stays leaderless and the session dies in the same breath.
// Polling on without a session would re-enter authentication, which
// awaits the login being settled, and the login would never return.
socket.write(Buffer.concat([
replyFrame(
Operation.NonReplicated,
twoNodeMetadataBody(server.port, server.port, 1)
),
evictionFrame(EvictionReason.NoSession)
]));
});
const client = new CommandResponseStream(vsrConfig(server.port));
compressLeaderlessPoll(client, 1_000);
try {
const outcome = await Promise.race([
client.authenticate(vsrConfig(server.port).credentials)
.then(() => 'authenticated'),
// Unreferenced so a passing run is not held open by the stall timer.
new Promise((resolve) => {
setTimeout(() => resolve('stalled'), 500).unref();
})
]);
assert.equal(outcome, 'authenticated');
assert.equal(rosterReads, 1);
} finally {
client.destroy();
await server.close();
}
});
it('keeps a login on its node when no leader appears in the budget',
async () => {
const unavailable = await startVsrServer(() => {});
const unavailablePort = unavailable.port;
await unavailable.close();
// The roster marks its leader-role node unhealthy on a dead port, so a
// redirect would fail the dial instead of passing unnoticed.
const server = await startVsrServer((frame, socket) => {
const operation = frame.readUInt8(REQUEST_OFFSET.operation);
if (operation === Operation.Register) {
socket.write(replyFrame(Operation.Register, registerReplyBody()));
return;
}
socket.write(replyFrame(
Operation.NonReplicated,
twoNodeMetadataBody(server.port, unavailablePort, 0, 1)
));
});
const client = new CommandResponseStream(vsrConfig(server.port));
compressLeaderlessPoll(client, 0);
try {
await client.authenticate(vsrConfig(server.port).credentials);
const operations = server.frames.map(
(frame) => frame.readUInt8(REQUEST_OFFSET.operation)
);
assert.deepEqual(operations, [
Operation.Register,
Operation.NonReplicated
]);
assert.equal(client.isAuthenticated, true);
} finally {
client.destroy();
await server.close();
}
});
it('rejects instead of hanging while a connection attempt is unresolved', async () => {
const server = await startVsrServer(() => {});
const port = server.port;
await server.close();
const client = new CommandResponseStream({
...vsrConfig(port),
reconnect: { enabled: true, interval: 300, maxRetries: 1 }
});
try {
await assert.rejects(
() => client.sendCommand(
60_006,
Buffer.alloc(0)
)
);
await assert.rejects(
() => client.sendCommand(
60_006,
Buffer.alloc(0)
)
);
} finally {
client.destroy();
}
});
it('resets the session before an authenticated VSR login', async () => {
const server = await startVsrServer(
(frame, socket) => singleNodeHandler(server.port)(frame, socket)
);
const client = new CommandResponseStream(vsrConfig(server.port));
try {
await client.sendCommand(60_007, Buffer.alloc(0));
let sessionResets = 0;
client.on('sessionReset', () => { sessionResets += 1; });
await client.sendCommand(
COMMAND_CODE.LoginUser,
Buffer.concat([
Buffer.from([4]),
Buffer.from('iggy'),
Buffer.from([4]),
Buffer.from('iggy')
])
);
const operations = server.frames.map(
(frame) => frame.readUInt8(REQUEST_OFFSET.operation)
);
assert.equal(
operations.filter((operation) => operation === Operation.Register)
.length,
2
);
assert.ok(operations.includes(Operation.Logout));
assert.equal(sessionResets, 1);
assert.equal(client.isAuthenticated, true);
} finally {
client.destroy();
await server.close();
}
});
it('keeps logout and replacement login adjacent in the queue', async () => {
const server = await startVsrServer((frame, socket) => {
const code = frame.readUInt32LE(REQUEST_OFFSET.reserved);
if (code === 60_015) {
setTimeout(() => {
socket.write(
replyFrame(Operation.NonReplicated, Buffer.from('first'))
);
}, 10);
return;
}
singleNodeHandler(server.port)(frame, socket);
});
const client = new CommandResponseStream(vsrConfig(server.port));
try {
await client.authenticate(vsrConfig(server.port).credentials);
const first = client.sendCommand(60_015, Buffer.alloc(0));
const second = client.sendCommand(60_016, Buffer.alloc(0));
const login = client.sendCommand(
COMMAND_CODE.LoginUser,
Buffer.concat([
Buffer.from([4]),
Buffer.from('iggy'),
Buffer.from([4]),
Buffer.from('iggy')
])
);
await Promise.all([first, second, login]);
const operations = server.frames.map(
(frame) => frame.readUInt8(REQUEST_OFFSET.operation)
);
const logoutIndex = operations.lastIndexOf(Operation.Logout);
assert.equal(operations[logoutIndex + 1], Operation.Register);
} finally {
client.destroy();
await server.close();
}
});
it('supports raw VSR responses without decoding', async () => {
const server = await startVsrServer(
(frame, socket) => singleNodeHandler(server.port)(frame, socket)
);
const client = new CommandResponseStream(vsrConfig(server.port));
try {
await client.authenticate(vsrConfig(server.port).credentials);
const raw = await client.sendCommand(
60_008,
Buffer.alloc(0),
{ handleResponse: false }
);
assert.ok(Buffer.isBuffer(raw));
} finally {
client.destroy();
await server.close();
}
});
it('remaps a terminal VSR response to the original command', async () => {
const server = await startVsrServer((frame, socket) => {
if (frame.readUInt32LE(REQUEST_OFFSET.reserved) === 60_009) {
socket.write(replyFrame(Operation.NonReplicated, Buffer.alloc(0), 3));
return;
}
singleNodeHandler(server.port)(frame, socket);
});
const client = new CommandResponseStream(vsrConfig(server.port));
try {
await assert.rejects(
() => client.sendCommand(60_009, Buffer.alloc(0)),
(error: unknown) =>
error instanceof ResponseError &&
error.commandCode === 60_009 &&
error.errorCode === 3
);
} finally {
client.destroy();
await server.close();
}
});
it('rejects synchronous writes and timed-out exchanges', async () => {
const server = await startVsrServer(() => {});
const client = new CommandResponseStream(vsrConfig(server.port));
const exchange = (
client as unknown as {
_exchange: (write: () => void, timeout?: number) => Promise<Buffer>
}
)._exchange.bind(client);
try {
const writeError = new Error('write failed');
await assert.rejects(
() => exchange(() => { throw writeError; }),
(error: unknown) => error === writeError
);
const connection = (
client as unknown as {
connection: { emit: (event: string, error: Error) => boolean }
}
).connection;
const responseError = new Error('response failed');
const pending = exchange(() => {});
connection.emit('error', responseError);
await assert.rejects(
() => pending,
(error: unknown) => error === responseError
);
await assert.rejects(
() => exchange(() => {}, 1),
/timed out after 1 ms/
);
} finally {
client.destroy();
await server.close();
}
});
it('expires a VSR request before writing after its deadline', async () => {
const server = await startVsrServer(
(frame, socket) => singleNodeHandler(server.port)(frame, socket)
);
const client = new CommandResponseStream(vsrConfig(server.port));
const realNow = Date.now;
try {
await client.authenticate(vsrConfig(server.port).credentials);
let sessionResets = 0;
client.on('sessionReset', () => { sessionResets += 1; });
let now = 0;
Date.now = () => {
now += 30_001;
return now;
};
await assert.rejects(
() => client.sendCommand(60_013, Buffer.alloc(0)),
/timed out after 30000 ms/
);
assert.equal(
server.frames.some(
(frame) => frame.readUInt32LE(REQUEST_OFFSET.reserved) === 60_013
),
false
);
assert.equal(client.isAuthenticated, true);
assert.equal(sessionResets, 0);
} finally {
Date.now = realNow;
client.destroy();
await server.close();
}
});
it('re-issues every request refused by a demoted node, not just the first',
async () => {
// One demotion, several refused requests: each of them re-checking on
// its own would move the client once per request, and the first
// redirect's drop would fail the others' roster reads - reporting a
// refusal they never had to.
const leader = await startVsrServer((frame, socket) => {
singleNodeHandler(leader.port)(frame, socket);
});
// Leader at login, so the settlement leaves the client here, then
// demoted: the refusals below are what tells the client to look again.
let demotedYet = false;
const demoted = await startVsrServer((frame, socket) => {
const code = frame.readUInt32LE(REQUEST_OFFSET.reserved);
if (code === COMMAND_CODE.GetClusterMetadata) {
socket.write(replyFrame(
Operation.NonReplicated,
demotedYet
? twoNodeMetadataBody(demoted.port, leader.port)
: twoNodeMetadataBody(leader.port, demoted.port)
));
return;
}
if (code === 60_032) {
socket.write(replyFrame(Operation.NonReplicated, Buffer.alloc(0), 58));
return;
}
singleNodeHandler(demoted.port)(frame, socket);
});
const client = new CommandResponseStream(vsrConfig(demoted.port));
try {
await client.authenticate(vsrConfig(demoted.port).credentials);
demotedYet = true;
const rosterReadsBefore = demoted.frames.filter(
(frame) => frame.readUInt32LE(REQUEST_OFFSET.reserved) ===
COMMAND_CODE.GetClusterMetadata
).length;
// Two commands, both refused by the demoted node: one re-check between
// them, and both answered on the node it moved to.
const answers = await Promise.all([
client.sendCommand(60_032, Buffer.alloc(0)),
client.sendCommand(60_032, Buffer.alloc(0))
]);
assert.deepEqual(answers.map((answer) => answer.status), [0, 0]);
const rosterReads = demoted.frames.filter(
(frame) => frame.readUInt32LE(REQUEST_OFFSET.reserved) ===
COMMAND_CODE.GetClusterMetadata
).length - rosterReadsBefore;
assert.equal(rosterReads, 1,
'each refusal re-read the roster on its own'
);
const reissued = leader.frames.filter(
(frame) => frame.readUInt32LE(REQUEST_OFFSET.reserved) === 60_032
).length;
assert.equal(reissued, 2,
'a refused command was not re-issued on the node the move landed on'
);
const connection = (client as unknown as {
connection: { isConnectedTo: (host: string, port: number) => boolean }
}).connection;
assert.equal(connection.isConnectedTo('127.0.0.1', leader.port), true);
} finally {
client.destroy();
await leader.close();
await demoted.close();
}
}
);
it('walks past two refusing replicas to the partition primary',
async () => {
const third = await startVsrServer((frame, socket) => {
singleNodeHandler(third.port)(frame, socket);
});
const second = await startVsrServer((frame, socket) => {
const operation = frame.readUInt8(REQUEST_OFFSET.operation);
if (operation === Operation.Register) {
socket.write(replyFrame(Operation.Register, registerReplyBody()));
return;
}
if (frame.readUInt32LE(REQUEST_OFFSET.reserved) === 60_040) {
socket.write(replyFrame(Operation.NonReplicated, Buffer.alloc(0), 58));
return;
}
socket.write(replyFrame(operation));
});
const metadataLeader = await startVsrServer((frame, socket) => {
const operation = frame.readUInt8(REQUEST_OFFSET.operation);
const code = frame.readUInt32LE(REQUEST_OFFSET.reserved);
if (operation === Operation.Register) {
socket.write(replyFrame(Operation.Register, registerReplyBody()));
return;
}
if (code === COMMAND_CODE.GetClusterMetadata) {
socket.write(replyFrame(
Operation.NonReplicated,
threeNodeMetadataBody(
metadataLeader.port,
second.port,
third.port
)
));
return;
}
if (code === 60_040) {
socket.write(replyFrame(Operation.NonReplicated, Buffer.alloc(0), 58));
return;
}
socket.write(replyFrame(operation));
});
const client = new CommandResponseStream(vsrConfig(metadataLeader.port));
try {
await client.authenticate(vsrConfig(metadataLeader.port).credentials);
const response = await client.sendCommand(60_040, Buffer.alloc(0), {
deadline: Date.now() + 15_000
});
assert.equal(response.status, 0);
assert.ok(
second.frames.some((frame) =>
frame.readUInt32LE(REQUEST_OFFSET.reserved) === 60_040),
'the roster walk skipped the second replica'
);
assert.ok(
third.frames.some((frame) =>
frame.readUInt32LE(REQUEST_OFFSET.reserved) === 60_040),
'the roster walk never reached the partition primary'
);
} finally {
client.destroy();
await metadataLeader.close();
await second.close();
await third.close();
}
}
);
it('holds a queued command instead of writing it to the node being left',
async () => {
// A refusal sends its caller to re-read the roster, and the drain that
// handed it out keeps going. Written in that window, the next queued
// command goes to the socket the move is about to replace: in flight when
// that happens, it dies with a lost-connection error nobody can act on
// instead of being re-issued on the node the move lands on.
const leader = await startVsrServer((frame, socket) => {
singleNodeHandler(leader.port)(frame, socket);
});
let demotedYet = false;
const demoted = await startVsrServer((frame, socket) => {
const code = frame.readUInt32LE(REQUEST_OFFSET.reserved);
if (code === COMMAND_CODE.GetClusterMetadata) {
socket.write(replyFrame(
Operation.NonReplicated,
demotedYet
? twoNodeMetadataBody(demoted.port, leader.port)
: twoNodeMetadataBody(leader.port, demoted.port)
));
return;
}
if (code === 60_037) {
socket.write(replyFrame(Operation.NonReplicated, Buffer.alloc(0), 58));
return;
}
if (code === 60_038) {
// Accepted and never answered: a command written here is stuck until
// the move replaces the socket under it.
return;
}
singleNodeHandler(demoted.port)(frame, socket);
});
const client = new CommandResponseStream(vsrConfig(demoted.port));
try {
await client.authenticate(vsrConfig(demoted.port).credentials);
demotedYet = true;
// The second command is queued while the first is in flight, which is
// where a command caught in a move comes from.
const refused = client.sendCommand(60_037, Buffer.alloc(0));
const behind = client.sendCommand(60_038, Buffer.alloc(0));
refused.catch(() => undefined);
behind.catch(() => undefined);
const settled = await Promise.race([
Promise.all([refused, behind]).then(() => 'answered'),
new Promise((resolve) => {
setTimeout(() => resolve('stalled'), 10_000).unref();
})
]);
assert.equal(settled, 'answered',
'the command behind the refusal went out on the node being left'
);
assert.ok(
!demoted.frames.some((frame) =>
frame.readUInt32LE(REQUEST_OFFSET.reserved) === 60_038),
'the command behind the refusal was written to the node being left'
);
assert.ok(
leader.frames.some((frame) =>
frame.readUInt32LE(REQUEST_OFFSET.reserved) === 60_038),
'the command behind the refusal never reached the node moved to'
);
} finally {
client.destroy();
await leader.close();
await demoted.close();
}
}
);
it('re-issues a command queued behind a leader move instead of failing it',
async () => {
// A move replaces the socket, which looks like a drop to everything
// waiting in the queue. Nothing queued was written, though, so it belongs
// on the node the client moves to rather than in a lost-connection error
// the caller can do nothing about.
const leader = await startVsrServer((frame, socket) => {
singleNodeHandler(leader.port)(frame, socket);
});
const demoted = await startVsrServer((frame, socket) => {
singleNodeHandler(demoted.port)(frame, socket);
});
const client = new CommandResponseStream(vsrConfig(demoted.port));
try {
await client.authenticate(vsrConfig(demoted.port).credentials);
// Parked the way a command is while something else holds the queue.
const queued = new Promise<CommandResponse>((resolve, reject) => {
execQueue(client).push({
command: 60_034,
payload: Buffer.alloc(0),
handleResponse: true,
deadline: Date.now() + 30_000,
resolve,
reject
});
});
await connectionOf(client).redirect('127.0.0.1', leader.port);
await queued;
const landedOnLeader = leader.frames.some(
(frame) => frame.readUInt32LE(REQUEST_OFFSET.reserved) === 60_034
);
assert.ok(landedOnLeader,
'the queued command never reached the node the client moved to'
);
} finally {
client.destroy();
await leader.close();
await demoted.close();
}
}
);
it('surfaces the refusal rather than a timeout when the budget runs out',
async () => {
const server = await startVsrServer((frame, socket) => {
if (frame.readUInt32LE(REQUEST_OFFSET.reserved) === 60_036) {
socket.write(replyFrame(Operation.NonReplicated, Buffer.alloc(0), 58));
return;
}
singleNodeHandler(server.port)(frame, socket);
});
const client = new CommandResponseStream(vsrConfig(server.port));
const realNow = Date.now;
try {
await client.authenticate(vsrConfig(server.port).credentials);
// The request's budget, the first exchange, the window that hands the
// refusal out, and then a clock 10ms short of the deadline: too little
// to carry another attempt, so the caller has to see the answer the
// server gave rather than the timeout a doomed re-issue would produce.
const times = [0, 1, 2_001, 29_990];
Date.now = () => times.shift() ?? 30_050;
await assert.rejects(
() => client.sendCommand(60_036, Buffer.alloc(0)),
(error: unknown) =>
error instanceof ResponseError &&
error.commandCode === 60_036 &&
error.errorCode === 58
);
} finally {
Date.now = realNow;
client.destroy();
await server.close();
}
}
);
it('does not suppress leader settlement after a failed roster walk',
async () => {
const server = await startVsrServer(
(frame, socket) => singleNodeHandler(server.port)(frame, socket)
);
const client = new CommandResponseStream(vsrConfig(server.port));
try {
await client.authenticate(vsrConfig(server.port).credentials);
const routing = client as unknown as {
walkSettleSuppressed: boolean,
_followLeaderMove: (walkPastLeader: boolean) => Promise<unknown>,
connection: {
nextRosterEndpoint: () => { host: string, port: number },
redirect: (host: string, port: number) => Promise<void>
}
};
routing.connection.nextRosterEndpoint = () => ({
host: '127.0.0.1',
port: server.port + 1
});
routing.connection.redirect = async () => {
throw new Error('redirect failed');
};
assert.deepEqual(await routing._followLeaderMove(true), {
endpoint: { host: '127.0.0.1', port: server.port + 1 },
moved: false
});
assert.equal(routing.walkSettleSuppressed, false,
'a failed walk leaked its one-shot suppression into a later login'
);
} finally {
client.destroy();
await server.close();
}
}
);
it('surfaces the refusal when the move left too little of the budget',
async () => {
// The move itself costs budget: a roster read, an election it waited out,
// a redial. What is left can be positive and still too small to carry
// another exchange, and re-issued into it the request times out -- the
// caller then sees a timeout where the answer was "not admitted".
const leader = await startVsrServer((frame, socket) => {
singleNodeHandler(leader.port)(frame, socket);
});
let demotedYet = false;
const demoted = await startVsrServer((frame, socket) => {
const code = frame.readUInt32LE(REQUEST_OFFSET.reserved);
if (code === COMMAND_CODE.GetClusterMetadata) {
socket.write(replyFrame(
Operation.NonReplicated,
demotedYet
? twoNodeMetadataBody(demoted.port, leader.port)
: twoNodeMetadataBody(leader.port, demoted.port)
));
return;
}
if (code === 60_039) {
socket.write(replyFrame(Operation.NonReplicated, Buffer.alloc(0), 58));
return;
}
singleNodeHandler(demoted.port)(frame, socket);
});
const client = new CommandResponseStream(vsrConfig(demoted.port));
const realNow = Date.now;
let offset = 0;
try {
await client.authenticate(vsrConfig(demoted.port).credentials);
demotedYet = true;
const deadline = realNow() + 30_000;
const connection = connectionOf(client);
const move = connection.redirect.bind(connection);
connection.redirect = async (host: string, port: number) => {
await move(host, port);
// 30ms of budget left the moment the client lands: positive, and
// below the interval one exchange needs.
offset = deadline - realNow() - 30;
};
Date.now = () => realNow() + offset;
await assert.rejects(
() => client.sendCommand(60_039, Buffer.alloc(0), { deadline }),
(error: unknown) =>
error instanceof ResponseError &&
error.commandCode === 60_039 &&
error.errorCode === 58
);
assert.ok(
!leader.frames.some((frame) =>
frame.readUInt32LE(REQUEST_OFFSET.reserved) === 60_039),
'the request was re-issued into a budget too small to answer it'
);
} finally {
Date.now = realNow;
client.destroy();
await leader.close();
await demoted.close();
}
}
);
it('paces the re-issues while the roster still names this node',
async () => {
// Re-issuing is right, spinning is not: the in-connection replay window
// belongs to the request's budget and is spent after the first pass, so
// without a wait the client would hammer the node for the whole budget.
let refusals = 0;
const server = await startVsrServer((frame, socket) => {
if (frame.readUInt32LE(REQUEST_OFFSET.reserved) === 60_035) {
refusals += 1;
socket.write(replyFrame(Operation.NonReplicated, Buffer.alloc(0), 58));
return;
}
singleNodeHandler(server.port)(frame, socket);
});
const client = new CommandResponseStream(vsrConfig(server.port));
try {
await client.authenticate(vsrConfig(server.port).credentials);
const pending = client.sendCommand(60_035, Buffer.alloc(0));
pending.catch(() => undefined);
await new Promise((resolve) => {
setTimeout(resolve, 5_000).unref();
});
// The first 2s window replays on the connection at its own interval;
// every window after it costs one refusal per pace.
assert.ok(refusals < 200,
`the re-issues were not paced: ${refusals} refusals in 5s`
);
} finally {
client.destroy();
await server.close();
}
}
);
it('keeps re-issuing a not-admitted request while the roster still names this node',
async () => {
const server = await startVsrServer((frame, socket) => {
if (frame.readUInt32LE(REQUEST_OFFSET.reserved) === 60_031) {
socket.write(replyFrame(Operation.NonReplicated, Buffer.alloc(0), 58));
return;
}
singleNodeHandler(server.port)(frame, socket);
});
const client = new CommandResponseStream(vsrConfig(server.port));
try {
await client.authenticate(vsrConfig(server.port).credentials);
// A refusal the roster cannot explain is a wait, not a verdict: an
// election may still be in flight, so the request keeps going for its
// whole budget instead of failing after the first re-check window.
const pending = client.sendCommand(60_031, Buffer.alloc(0));
const outcome = await Promise.race([
pending.then(() => 'answered', () => 'gave up'),
new Promise((resolve) => {
setTimeout(() => resolve('still trying'), 4_000).unref();
})
]);
assert.equal(outcome, 'still trying');
} finally {
client.destroy();
await server.close();
}
}
);
it('keeps a typed transient error and session at its retry deadline',
async () => {
const server = await startVsrServer((frame, socket) => {
if (frame.readUInt32LE(REQUEST_OFFSET.reserved) === 60_017) {
socket.write(
replyFrame(Operation.NonReplicated, Buffer.alloc(0), 57)
);
return;
}
singleNodeHandler(server.port)(frame, socket);
});
const client = new CommandResponseStream(vsrConfig(server.port));
const realNow = Date.now;
try {
await client.authenticate(vsrConfig(server.port).credentials);
let sessionResets = 0;
client.on('sessionReset', () => { sessionResets += 1; });
const times = [0, 1, 30_001];
Date.now = () => times.shift() ?? 30_001;
await assert.rejects(
() => client.sendCommand(60_017, Buffer.alloc(0)),
(error: unknown) =>
error instanceof ResponseError &&
error.commandCode === 60_017 &&
error.errorCode === 57
);
assert.equal(client.isAuthenticated, true);
assert.equal(sessionResets, 0);
} finally {
Date.now = realNow;
client.destroy();
await server.close();
}
}
);
it('shares token authentication between concurrent callers', async () => {
const server = await startVsrServer(
(frame, socket) => singleNodeHandler(server.port)(frame, socket)
);
const client = new CommandResponseStream({
...vsrConfig(server.port),
credentials: { token: 'secret' }
});
try {
await Promise.all([
client.sendCommand(60_011, Buffer.alloc(0)),
client.sendCommand(60_012, Buffer.alloc(0))
]);
assert.equal(
server.frames.filter(
(frame) =>
frame.readUInt8(REQUEST_OFFSET.operation) === Operation.Register
).length,
1
);
assert.equal(client.isAuthenticated, true);
} finally {
client.destroy();
await server.close();
}
});
});