| // 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 { LOGIN, LOGIN_WITH_TOKEN, LOGOUT } from '../wire/index.js'; |
| import { UPDATE_USER } from '../wire/user/update-user.command.js'; |
| import { CHANGE_PASSWORD } from '../wire/user/change-password.command.js'; |
| import { POLL_MESSAGES } from '../wire/message/poll-messages.command.js'; |
| import { SEND_MESSAGES, sendMessages } from '../wire/message/send-messages.command.js'; |
| import { GET_OFFSET } from '../wire/offset/get-offset.command.js'; |
| import { STORE_OFFSET } from '../wire/offset/store-offset.command.js'; |
| import { DELETE_OFFSET } from '../wire/offset/delete-offset.command.js'; |
| import { Partitioning } from '../wire/message/partitioning.utils.js'; |
| import { PollingStrategy } from '../wire/message/poll.utils.js'; |
| import { ConsumerKind } from '../wire/offset/offset.utils.js'; |
| import { readWireName } from '../wire/vsr/index.js'; |
| import { BIND_SECRET_BYTES } from '../wire/vsr/register.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 HISTORY_UNAVAILABLE = 87; |
| const TRANSIENT_NOT_ACCEPTED = 58; |
| 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 bindingIdentity = (frame: Buffer): Buffer => frame.subarray(-64, -32); |
| |
| const assertSharedBinding = (register: Buffer, binding: Buffer): void => { |
| assert.deepEqual(bindingIdentity(binding).subarray(0, 16), |
| register.subarray(REQUEST_OFFSET.client, REQUEST_OFFSET.client + 16)); |
| assert.equal(bindingIdentity(binding).readBigUInt64LE(16), TEST_SESSION); |
| const sdkName = readWireName(register, HEADER_SIZE + 4); |
| const sdkVersion = readWireName(register, sdkName.next); |
| assert.deepEqual(binding.subarray(-BIND_SECRET_BYTES), |
| register.subarray(sdkVersion.next, sdkVersion.next + BIND_SECRET_BYTES)); |
| }; |
| |
| const registerReplyBody = (session = TEST_SESSION): 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(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 + 1 + 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.writeUInt8(0, offset); offset += 1; |
| 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, |
| replicaId: number, |
| port: number, |
| role: number, |
| status = 0 |
| ): Buffer => { |
| const ip = Buffer.from('127.0.0.1'); |
| const encoded = Buffer.alloc(1 + 4 + nodeName.length + 4 + ip.length + 8 + 2); |
| let offset = 0; |
| encoded.writeUInt8(replicaId, offset); offset += 1; |
| 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'), 0, leaderPort, leaderRole, leaderStatus), |
| node(Buffer.from('iggy-node-2'), 7, followerPort, 1) |
| ]); |
| }; |
| |
| const threeNodeMetadataBody = ( |
| firstPort: number, |
| secondPort: number, |
| thirdPort: number |
| ): Buffer => { |
| const node = (name: string, replicaId: number, port: number, role: number): Buffer => { |
| const nodeName = Buffer.from(name); |
| const ip = Buffer.from('127.0.0.1'); |
| const encoded = Buffer.alloc(1 + 4 + nodeName.length + 4 + ip.length + 8 + 2); |
| let offset = 0; |
| encoded.writeUInt8(replicaId, offset); offset += 1; |
| 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', 0, firstPort, 0), |
| node('iggy-node-2', 7, secondPort, 1), |
| node('iggy-node-3', 9, 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.BindSession) { |
| socket.write(replyFrame(Operation.NonReplicated, registerReplyBody().subarray(4))); |
| return; |
| } |
| if (code === COMMAND_CODE.GetClusterMetadata) { |
| socket.write( |
| replyFrame(Operation.NonReplicated, singleNodeMetadataBody(port)) |
| ); |
| return; |
| } |
| if (code === COMMAND_CODE.GetPartitionRoute) { |
| socket.write(replyFrame(Operation.NonReplicated, routingBody(frame.readBigUInt64LE(REQUEST_OFFSET.minimumMetadataOp) || 1n))); |
| 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; |
| }; |
| |
| const groupPollPayload = (autocommit = true, partitionId = 0): Buffer => |
| POLL_MESSAGES.serialize({ |
| streamId: 1, |
| topicId: 1, |
| consumer: { kind: ConsumerKind.Group, id: 1 }, |
| partitionId, |
| pollingStrategy: PollingStrategy.Next, |
| count: 1, |
| autocommit |
| }); |
| |
| const routingBody = (watermark: bigint, incarnation = 0n, |
| primaryReplica = 7): Buffer => { |
| const route = Buffer.alloc(25); |
| route.writeBigUInt64LE(incarnation, 0); |
| route.writeBigUInt64LE(watermark, 8); |
| route.writeUInt32LE(4, 20); |
| route.writeUInt8(primaryReplica, 24); |
| return route; |
| }; |
| |
| const countCommand = (server: VsrTestServer, code: number): number => |
| server.frames.filter((frame) => |
| frame.readUInt8(REQUEST_OFFSET.operation) === Operation.NonReplicated && |
| frame.readUInt32LE(REQUEST_OFFSET.reserved) === code).length; |
| |
| const registerCredentialsBody = (frame: Buffer): Buffer => { |
| const sdkName = readWireName(frame, HEADER_SIZE + 4); |
| const sdkVersion = readWireName(frame, sdkName.next); |
| return frame.subarray(sdkVersion.next + BIND_SECRET_BYTES); |
| }; |
| |
| const startPollCluster = async ( |
| primaryOverride?: (frame: Buffer, socket: Socket) => boolean, |
| coordinatorOverride?: (frame: Buffer, socket: Socket) => boolean, |
| transport: 'TCP' | 'TLS' = 'TCP' |
| ) => { |
| const primary = await startVsrServer((frame, socket) => { |
| if (primaryOverride?.(frame, socket)) |
| return; |
| if (frame.readUInt32LE(REQUEST_OFFSET.reserved) === COMMAND_CODE.PollMessages) { |
| socket.write(replyFrame(Operation.NonReplicated, Buffer.from('primary'))); |
| return; |
| } |
| singleNodeHandler(primary.port)(frame, socket); |
| }, transport); |
| const coordinator = await startVsrServer((frame, socket) => { |
| if (coordinatorOverride?.(frame, socket)) |
| return; |
| const code = frame.readUInt32LE(REQUEST_OFFSET.reserved); |
| if (code === COMMAND_CODE.GetClusterMetadata) { |
| socket.write(replyFrame(Operation.NonReplicated, |
| twoNodeMetadataBody(primary.port, coordinator.port))); |
| return; |
| } |
| if (code === COMMAND_CODE.GetPartitionRoute) { |
| socket.write(replyFrame(Operation.NonReplicated, routingBody(frame.readBigUInt64LE(REQUEST_OFFSET.minimumMetadataOp) || 1n))); |
| return; |
| } |
| singleNodeHandler(coordinator.port)(frame, socket); |
| }, transport); |
| const config = { |
| ...vsrConfig(coordinator.port), transport, heartbeatInterval: 0, |
| options: { |
| ...vsrConfig(coordinator.port).options, |
| ...(transport === 'TLS' ? { ca: TLS_CA_CERTIFICATE, servername: 'localhost' } : {}) |
| } |
| }; |
| const client = new CommandResponseStream(config); |
| return { |
| client, primary, coordinator, |
| close: async () => { |
| client.destroy(); |
| await Promise.all([coordinator.close(), primary.close()]); |
| } |
| }; |
| }; |
| |
| const sendTo = (partition: Partitioning): Buffer => SEND_MESSAGES.serialize({ |
| streamId: 1, topicId: 2, messages: [{ payload: 'message' }], partition |
| }); |
| |
| const explicitSend = sendTo(Partitioning.PartitionId(3)); |
| |
| const storeOffsetPayload = STORE_OFFSET.serialize({ |
| streamId: 1, topicId: 2, consumer: { kind: ConsumerKind.Single, id: 1 }, partitionId: 3, offset: 5n |
| }); |
| |
| const deleteOffsetPayload = DELETE_OFFSET.serialize({ |
| streamId: 1, topicId: 2, consumer: { kind: ConsumerKind.Single, id: 1 }, partitionId: 3 |
| }); |
| |
| const CALLER_CONTEXT = { incarnation: 7n, ownerGeneration: 8n, metadataOp: 9n }; |
| |
| const stampedContext = (frame: Buffer) => ({ |
| incarnation: frame.readBigUInt64LE(REQUEST_OFFSET.partitionIncarnation), |
| ownerGeneration: frame.readBigUInt64LE(REQUEST_OFFSET.ownerGeneration), |
| metadataOp: frame.readBigUInt64LE(REQUEST_OFFSET.minimumMetadataOp) |
| }); |
| |
| const framesOf = (server: VsrTestServer, operation: number): Buffer[] => |
| server.frames.filter((frame) => frame.readUInt8(REQUEST_OFFSET.operation) === operation); |
| |
| /** Body of a committed metadata reply whose single result is `code`. */ |
| const metadataResult = (code: number): Buffer => { |
| const body = Buffer.alloc(12); |
| body.writeUInt32LE(1, 0); |
| body.writeUInt32LE(code, 8); |
| return body; |
| }; |
| |
| /** A single node that also commits offset writes. */ |
| const offsetCommittingHandler = (port: number): FrameHandler => (frame, socket) => { |
| const operation = frame.readUInt8(REQUEST_OFFSET.operation); |
| if (operation === Operation.StoreConsumerOffset || operation === Operation.DeleteConsumerOffset) { |
| socket.write(replyFrame(operation, metadataResult(0))); |
| return; |
| } |
| singleNodeHandler(port)(frame, socket); |
| }; |
| |
| const contextServer = async (state: { |
| live: bigint, refuseAll: boolean, contextStatus: number, onSend?: () => void |
| }) => { |
| const server: VsrTestServer = await startVsrServer((frame, socket) => { |
| if (frame.readUInt8(REQUEST_OFFSET.operation) === Operation.SendMessages) { |
| state.onSend?.(); |
| const sent = frame.readBigUInt64LE(REQUEST_OFFSET.partitionIncarnation); |
| socket.write(replyFrame(Operation.SendMessages, Buffer.alloc(0), |
| !state.refuseAll && sent === state.live ? 0 : HISTORY_UNAVAILABLE)); |
| return; |
| } |
| if (frame.readUInt32LE(REQUEST_OFFSET.reserved) === COMMAND_CODE.GetPartitionRoute) { |
| const context = routingBody(0n, 0n, 0); |
| context.writeBigUInt64LE(state.live, 0); |
| socket.write(replyFrame(Operation.NonReplicated, context, state.contextStatus)); |
| return; |
| } |
| singleNodeHandler(server.port)(frame, socket); |
| }); |
| return server; |
| }; |
| |
| describe('unified partition routing', () => { |
| const offset = { streamId: 1, topicId: 2, partitionId: 3, |
| consumer: { kind: ConsumerKind.Single, id: 1 } }; |
| const poll = POLL_MESSAGES.serialize({ ...offset, count: 1, |
| pollingStrategy: PollingStrategy.Next, autocommit: false }); |
| const requests = [ |
| [COMMAND_CODE.SendMessages, explicitSend, Operation.SendMessages], |
| [COMMAND_CODE.PollMessages, poll, Operation.NonReplicated], |
| [COMMAND_CODE.GetOffset, GET_OFFSET.serialize(offset), Operation.NonReplicated], |
| [COMMAND_CODE.StoreOffset, storeOffsetPayload, Operation.StoreConsumerOffset], |
| [COMMAND_CODE.DeleteConsumerOffset, deleteOffsetPayload, Operation.DeleteConsumerOffset] |
| ] as const; |
| |
| for (const [command, payload, operation] of requests) { |
| it(`reroutes command ${command} after89 with the identical request`, async () => { |
| const replacement = await startVsrServer((frame, socket) => { |
| if (frame[REQUEST_OFFSET.operation] === operation && |
| (operation !== Operation.NonReplicated || frame.readUInt32LE(REQUEST_OFFSET.reserved) === command)) { |
| socket.write(replyFrame(operation, operation === Operation.StoreConsumerOffset || |
| operation === Operation.DeleteConsumerOffset ? metadataResult(0) : Buffer.alloc(0))); |
| return; |
| } |
| singleNodeHandler(replacement.port)(frame, socket); |
| }); |
| let routes = 0; |
| const cluster = await startPollCluster((frame, socket) => { |
| if (frame[REQUEST_OFFSET.operation] !== operation || |
| (operation === Operation.NonReplicated && frame.readUInt32LE(REQUEST_OFFSET.reserved) !== command)) |
| return false; |
| socket.write(replyFrame(operation, Buffer.alloc(0), 89)); |
| return true; |
| }, (frame, socket) => { |
| const code = frame.readUInt32LE(REQUEST_OFFSET.reserved); |
| if (code === COMMAND_CODE.GetClusterMetadata) { |
| socket.write(replyFrame(Operation.NonReplicated, |
| threeNodeMetadataBody(cluster.coordinator.port, cluster.primary.port, replacement.port))); |
| return true; |
| } |
| if (code !== COMMAND_CODE.GetPartitionRoute) |
| return false; |
| routes += 1; |
| socket.write(replyFrame(Operation.NonReplicated, |
| routingBody(BigInt(routes), BigInt(routes), routes === 1 ? 7 : 9))); |
| return true; |
| }); |
| try { |
| await cluster.client.sendCommand(command, payload, { deadline: Date.now() + 1_000 }); |
| const matching = (server: VsrTestServer) => framesOf(server, operation).filter((frame) => |
| operation !== Operation.NonReplicated || frame.readUInt32LE(REQUEST_OFFSET.reserved) === command); |
| assert.equal(matching(cluster.primary).length, 1); |
| assert.equal(matching(replacement).length, 1); |
| assert.deepEqual(matching(cluster.primary)[0], matching(replacement)[0]); |
| assert.deepEqual(stampedContext(matching(replacement)[0]), |
| { incarnation: 1n, ownerGeneration: 0n, metadataOp: 1n }); |
| assert.equal(routes, 2); |
| assert.equal(countCommand(cluster.coordinator, COMMAND_CODE.GetClusterMetadata), 1); |
| assert.equal(framesOf(cluster.coordinator, Operation.Register).length, 1); |
| const initial = cluster.coordinator.frames[0]; |
| for (const server of [cluster.primary, replacement]) { |
| const binding = server.frames.find((frame) => |
| frame.readUInt32LE(REQUEST_OFFSET.reserved) === COMMAND_CODE.BindSession)!; |
| assertSharedBinding(initial, binding); |
| } |
| } finally { |
| await cluster.close(); |
| await replacement.close(); |
| } |
| }); |
| } |
| |
| it('stamps the latest metadata acknowledgement on typed send route discovery', async () => { |
| const cluster = await startPollCluster(undefined, (frame, socket) => { |
| if (frame[REQUEST_OFFSET.operation] !== Operation.CreateStream) |
| return false; |
| const response = replyFrame(Operation.CreateStream, metadataResult(0)); |
| response.writeBigUInt64LE(19n, REPLY_OFFSET.commit); |
| socket.write(response); |
| return true; |
| }); |
| try { |
| await cluster.client.sendCommand(COMMAND_CODE.CreateStream, Buffer.alloc(0)); |
| await sendMessages(async () => cluster.client)({ |
| streamId: 1, topicId: 2, messages: [{ payload: 'balanced' }] |
| }); |
| const lookups = cluster.coordinator.frames.filter((frame) => |
| frame.readUInt32LE(REQUEST_OFFSET.reserved) === COMMAND_CODE.GetPartitionRoute); |
| assert.equal(lookups.length, 1); |
| assert.equal(stampedContext(lookups[0]).metadataOp, 19n); |
| } finally { |
| await cluster.close(); |
| } |
| }); |
| |
| for (const command of [COMMAND_CODE.PollMessages, COMMAND_CODE.GetOffset]) { |
| it(`refreshes cached history after observing a newer metadata commit for ${command}`, async () => { |
| let watermark = 1n; |
| let incarnation = 7n; |
| const histories: bigint[] = []; |
| const cluster = await startPollCluster((frame, socket) => { |
| if (frame.readUInt32LE(REQUEST_OFFSET.reserved) !== command) |
| return false; |
| histories.push(stampedContext(frame).incarnation); |
| socket.write(replyFrame(Operation.NonReplicated, Buffer.alloc(0))); |
| return true; |
| }, (frame, socket) => { |
| if (frame[REQUEST_OFFSET.operation] === Operation.UpdateUser) { |
| watermark = 19n; |
| incarnation = 8n; |
| const response = replyFrame(Operation.UpdateUser, metadataResult(0)); |
| response.writeBigUInt64LE(watermark, REPLY_OFFSET.commit); |
| socket.write(response); |
| return true; |
| } |
| if (frame.readUInt32LE(REQUEST_OFFSET.reserved) !== COMMAND_CODE.GetPartitionRoute) |
| return false; |
| socket.write(replyFrame(Operation.NonReplicated, routingBody(watermark, incarnation))); |
| return true; |
| }); |
| try { |
| const payload = command === COMMAND_CODE.PollMessages |
| ? groupPollPayload() : GET_OFFSET.serialize({ streamId: 1, topicId: 2, |
| consumer: { kind: ConsumerKind.Single, id: 1 }, partitionId: 3 }); |
| await cluster.client.sendCommand(command, payload); |
| await cluster.client.sendCommand(COMMAND_CODE.UpdateUser, Buffer.alloc(0)); |
| await cluster.client.sendCommand(command, payload); |
| assert.deepEqual(histories, [7n, 8n]); |
| assert.equal(countCommand(cluster.coordinator, COMMAND_CODE.GetPartitionRoute), 2); |
| const lookups = cluster.coordinator.frames.filter((frame) => |
| frame.readUInt32LE(REQUEST_OFFSET.reserved) === COMMAND_CODE.GetPartitionRoute); |
| assert.equal(stampedContext(lookups[1]).metadataOp, watermark); |
| } finally { |
| await cluster.close(); |
| } |
| }); |
| } |
| |
| it('shares one route and request counter across all five partition commands', async () => { |
| const cluster = await startPollCluster((frame, socket) => { |
| const operation = frame[REQUEST_OFFSET.operation]; |
| if (operation !== Operation.StoreConsumerOffset && operation !== Operation.DeleteConsumerOffset) |
| return false; |
| socket.write(replyFrame(operation, metadataResult(0))); |
| return true; |
| }); |
| try { |
| for (const [command, payload] of requests) |
| await cluster.client.sendCommand(command, payload); |
| assert.equal(countCommand(cluster.coordinator, COMMAND_CODE.GetPartitionRoute), 1); |
| const boundFrames = [...cluster.coordinator.frames, ...cluster.primary.frames] |
| .filter((frame) => frame.readBigUInt64LE(REQUEST_OFFSET.session) === TEST_SESSION); |
| const ids = boundFrames.map((frame) => frame.readBigUInt64LE(REQUEST_OFFSET.request)); |
| assert.equal(new Set(ids).size, ids.length); |
| assert.ok(ids.every((id) => id > 0n)); |
| assert.equal(framesOf(cluster.primary, Operation.Register).length, 0); |
| } finally { |
| await cluster.close(); |
| } |
| }); |
| }); |
| |
| describe('primary auto-commit polling', () => { |
| for (const transport of ['TCP', 'TLS'] as const) { |
| for (const refused of [false, true]) { |
| it(`preserves raw poll responses over ${transport}${refused ? ' after a refusal' : ''}`, async () => { |
| const expected = replyFrame(Operation.NonReplicated, Buffer.from('raw primary')); |
| let polls = 0; |
| const cluster = await startPollCluster((frame, socket) => { |
| if (frame.readUInt32LE(REQUEST_OFFSET.reserved) !== COMMAND_CODE.PollMessages) |
| return false; |
| polls += 1; |
| socket.write(refused && polls === 1 |
| ? replyFrame(Operation.NonReplicated, Buffer.alloc(0), 58) |
| : expected); |
| return true; |
| }, undefined, transport); |
| let resets = 0; |
| cluster.client.on('sessionReset', () => { resets += 1; }); |
| try { |
| for (let poll = 0; poll < 2; poll += 1) { |
| const response = await cluster.client.sendCommand(POLL_MESSAGES.code, groupPollPayload(), |
| { handleResponse: false }); |
| assert.ok(Buffer.isBuffer(response), 'raw polls must return the complete VSR frame'); |
| assert.deepEqual(response, expected); |
| } |
| assert.equal(polls, refused ? 3 : 2); |
| assert.equal(countCommand(cluster.coordinator, COMMAND_CODE.GetPartitionRoute), refused ? 2 : 1); |
| assert.equal(countCommand(cluster.primary, COMMAND_CODE.BindSession), 1); |
| assert.equal(resets, 0); |
| } finally { |
| await cluster.close(); |
| } |
| }); |
| } |
| } |
| |
| for (const transport of ['TCP', 'TLS'] as const) { |
| it(`retains the coordinator session and reuses authenticated primary polls over ${transport}`, async () => { |
| const cluster = await startPollCluster(undefined, undefined, transport); |
| const { client, primary, coordinator } = cluster; |
| let resets = 0; |
| client.on('sessionReset', () => { resets += 1; }); |
| try { |
| const first = await client.sendCommand(POLL_MESSAGES.code, groupPollPayload()); |
| const second = await client.sendCommand(POLL_MESSAGES.code, groupPollPayload()); |
| assert.equal(first.data.toString(), 'primary'); |
| assert.equal(second.data.toString(), 'primary'); |
| assert.equal(countCommand(coordinator, COMMAND_CODE.GetPartitionRoute), 1); |
| assert.equal(countCommand(coordinator, COMMAND_CODE.PollMessages), 0); |
| assert.equal(countCommand(primary, COMMAND_CODE.PollMessages), 2); |
| assert.equal(countCommand(primary, COMMAND_CODE.BindSession), 1); |
| assert.equal(countCommand(primary, COMMAND_CODE.GetClusterMetadata), 0, |
| 'auxiliary login must stay on the partition primary'); |
| assert.equal(primary.frames.filter((frame) => |
| frame[REQUEST_OFFSET.operation] === Operation.Register).length, 0); |
| const route = coordinator.frames.find((frame) => |
| frame.readUInt32LE(REQUEST_OFFSET.reserved) === COMMAND_CODE.GetPartitionRoute)!; |
| const attach = primary.frames.find((frame) => |
| frame.readUInt32LE(REQUEST_OFFSET.reserved) === COMMAND_CODE.BindSession)!; |
| assert.deepEqual(bindingIdentity(attach).subarray(0, 16), |
| route.subarray(REQUEST_OFFSET.client, REQUEST_OFFSET.client + 16)); |
| assert.equal(bindingIdentity(attach).readBigUInt64LE(16), TEST_SESSION); |
| assert.deepEqual(attach.subarray(REQUEST_OFFSET.client, REQUEST_OFFSET.client + 16), |
| route.subarray(REQUEST_OFFSET.client, REQUEST_OFFSET.client + 16), |
| 'the auxiliary must bind the coordinator identity'); |
| assert.equal(resets, 0); |
| } finally { |
| await cluster.close(); |
| } |
| }); |
| } |
| |
| it('routes manual-commit polls to the primary and keeps the standalone path', async () => { |
| const cluster = await startPollCluster(); |
| const server = await startVsrServer((frame, socket) => singleNodeHandler(server.port)(frame, socket)); |
| const standalone = new CommandResponseStream({ ...vsrConfig(server.port), heartbeatInterval: 0 }); |
| try { |
| await cluster.client.sendCommand(POLL_MESSAGES.code, groupPollPayload(false)); |
| await standalone.sendCommand(POLL_MESSAGES.code, groupPollPayload()); |
| assert.equal(countCommand(cluster.coordinator, COMMAND_CODE.PollMessages), 0); |
| assert.equal(countCommand(cluster.primary, COMMAND_CODE.PollMessages), 1); |
| assert.equal(countCommand(cluster.coordinator, COMMAND_CODE.GetPartitionRoute), 1); |
| assert.equal(countCommand(server, COMMAND_CODE.PollMessages), 1); |
| assert.equal(countCommand(server, COMMAND_CODE.GetPartitionRoute), 1); |
| } finally { |
| standalone.destroy(); |
| await cluster.close(); |
| await server.close(); |
| } |
| }); |
| |
| it('refreshes the attachment after a metadata acknowledgement but ignores partition commits', async () => { |
| const cluster = await startPollCluster((frame, socket) => { |
| if (frame[REQUEST_OFFSET.operation] !== Operation.SendMessages) |
| return false; |
| const response = replyFrame(Operation.SendMessages); |
| response.writeBigUInt64LE(900n, REPLY_OFFSET.commit); |
| socket.write(response); |
| return true; |
| }, (frame, socket) => { |
| if (frame[REQUEST_OFFSET.operation] !== Operation.UpdateTopic) |
| return false; |
| const response = replyFrame(Operation.UpdateTopic, Buffer.alloc(4)); |
| response.writeBigUInt64LE(15n, REPLY_OFFSET.commit); |
| socket.write(response); |
| return true; |
| }); |
| try { |
| await cluster.client.sendCommand(POLL_MESSAGES.code, groupPollPayload()); |
| await cluster.client.sendCommand(COMMAND_CODE.SendMessages, explicitSend); |
| await cluster.client.sendCommand(POLL_MESSAGES.code, groupPollPayload()); |
| assert.equal(countCommand(cluster.coordinator, COMMAND_CODE.GetPartitionRoute), 2); |
| await cluster.client.sendCommand(COMMAND_CODE.UpdateTopic, Buffer.alloc(0), { handleResponse: false }); |
| await cluster.client.sendCommand(POLL_MESSAGES.code, groupPollPayload()); |
| const attachments = cluster.primary.frames.filter((frame) => |
| frame.readUInt32LE(REQUEST_OFFSET.reserved) === COMMAND_CODE.BindSession); |
| assert.deepEqual(attachments.map((frame) => bindingIdentity(frame).readBigUInt64LE(24)), [1n, 15n]); |
| assert.equal(countCommand(cluster.coordinator, COMMAND_CODE.GetPartitionRoute), 3); |
| await cluster.client.sendCommand(LOGOUT.code, LOGOUT.serialize()); |
| await cluster.client.sendCommand(POLL_MESSAGES.code, groupPollPayload()); |
| const replacement = cluster.primary.frames.filter((frame) => |
| frame.readUInt32LE(REQUEST_OFFSET.reserved) === COMMAND_CODE.BindSession).at(-1)!; |
| assert.equal(bindingIdentity(replacement).readBigUInt64LE(24), 15n, |
| 'a new coordinator session must retain previously acknowledged metadata'); |
| } finally { |
| await cluster.close(); |
| } |
| }); |
| |
| it('refreshes a not-admitted route without moving the coordinator', async () => { |
| let polls = 0; |
| const cluster = await startPollCluster((frame, socket) => { |
| if (frame.readUInt32LE(REQUEST_OFFSET.reserved) !== COMMAND_CODE.PollMessages) |
| return false; |
| polls += 1; |
| if (polls > 1) |
| return false; |
| socket.write(replyFrame(Operation.NonReplicated, Buffer.alloc(0), 58)); |
| return true; |
| }); |
| let resets = 0; |
| cluster.client.on('sessionReset', () => { resets += 1; }); |
| try { |
| await cluster.client.sendCommand(POLL_MESSAGES.code, groupPollPayload()); |
| assert.equal(polls, 2); |
| assert.equal(countCommand(cluster.coordinator, COMMAND_CODE.GetPartitionRoute), 2); |
| assert.equal(countCommand(cluster.primary, COMMAND_CODE.BindSession), 1); |
| assert.equal(resets, 0); |
| } finally { |
| await cluster.close(); |
| } |
| }); |
| |
| for (const refusing of [COMMAND_CODE.GetPartitionRoute, COMMAND_CODE.BindSession, |
| COMMAND_CODE.PollMessages]) { |
| it(`reports not accepted after command ${refusing} refuses the whole poll budget`, async () => { |
| const refuse = (frame: Buffer, socket: Socket): boolean => { |
| if (frame.readUInt32LE(REQUEST_OFFSET.reserved) !== refusing) |
| return false; |
| socket.write(replyFrame(Operation.NonReplicated, Buffer.alloc(0), 58)); |
| return true; |
| }; |
| const cluster = await startPollCluster(refuse, refuse); |
| let resets = 0; |
| cluster.client.on('sessionReset', () => { resets += 1; }); |
| try { |
| await assert.rejects(cluster.client.sendCommand(POLL_MESSAGES.code, groupPollPayload(), |
| { deadline: Date.now() + 1_000 }), (error: unknown) => |
| error instanceof ResponseError && error.errorCode === 58); |
| assert.ok(countCommand(cluster.coordinator, COMMAND_CODE.GetPartitionRoute) >= 2); |
| if (refusing === COMMAND_CODE.GetPartitionRoute) |
| assert.equal(cluster.primary.frames.length, 0); |
| if (refusing === COMMAND_CODE.BindSession) |
| assert.equal(countCommand(cluster.primary, COMMAND_CODE.PollMessages), 0); |
| assert.equal(resets, 0); |
| } finally { |
| await cluster.close(); |
| } |
| }); |
| } |
| |
| it('refreshes the roster once when the primary has no TCP endpoint', async () => { |
| const cluster = await startPollCluster(undefined, (frame, socket) => { |
| if (frame.readUInt32LE(REQUEST_OFFSET.reserved) !== COMMAND_CODE.GetClusterMetadata) |
| return false; |
| socket.write(replyFrame(Operation.NonReplicated, twoNodeMetadataBody(0, cluster.coordinator.port))); |
| return true; |
| }); |
| try { |
| await assert.rejects(cluster.client.sendCommand(POLL_MESSAGES.code, groupPollPayload(), |
| { deadline: Date.now() + 500 }), |
| (error: unknown) => error instanceof ResponseError && error.errorCode === 58); |
| assert.equal(cluster.primary.frames.length, 0); |
| assert.equal(countCommand(cluster.coordinator, COMMAND_CODE.GetClusterMetadata), 2); |
| } finally { |
| await cluster.close(); |
| } |
| }); |
| |
| for (const standalone of [false, true]) { |
| for (const failure of ['empty reply', 'empty roster', 'unavailable']) { |
| it(`requires valid topology after ${failure} before ${standalone ? 'standalone' : 'cluster'} polling`, async () => { |
| const clusterName = Buffer.from('cluster-without-nodes'); |
| const emptyRoster = Buffer.alloc(4 + clusterName.length + 4); |
| emptyRoster.writeUInt32LE(clusterName.length, 0); |
| clusterName.copy(emptyRoster, 4); |
| let rejectMetadata = true; |
| let coordinatorPort = 0; |
| const cluster = await startPollCluster(undefined, (frame, socket) => { |
| if (frame.readUInt32LE(REQUEST_OFFSET.reserved) !== COMMAND_CODE.GetClusterMetadata) |
| return false; |
| if (rejectMetadata) { |
| socket.write(replyFrame(Operation.NonReplicated, |
| failure === 'empty roster' ? emptyRoster : Buffer.alloc(0), |
| failure === 'unavailable' ? 5 : 0)); |
| return true; |
| } |
| if (standalone) { |
| socket.write(replyFrame(Operation.NonReplicated, singleNodeMetadataBody(coordinatorPort))); |
| return true; |
| } |
| return false; |
| }); |
| coordinatorPort = cluster.coordinator.port; |
| let resets = 0; |
| cluster.client.on('sessionReset', () => { resets += 1; }); |
| try { |
| await cluster.client.authenticate(vsrConfig(coordinatorPort).credentials); |
| await assert.rejects(cluster.client.sendCommand(POLL_MESSAGES.code, groupPollPayload())); |
| assert.equal(countCommand(cluster.coordinator, COMMAND_CODE.PollMessages), 0); |
| assert.equal(countCommand(cluster.coordinator, COMMAND_CODE.GetPartitionRoute), 0); |
| assert.equal(cluster.primary.frames.length, 0); |
| rejectMetadata = false; |
| await cluster.client.sendCommand(POLL_MESSAGES.code, groupPollPayload()); |
| rejectMetadata = true; |
| const refresh = cluster.client.sendCommand(COMMAND_CODE.GetClusterMetadata, Buffer.alloc(0)); |
| if (failure === 'unavailable') |
| await assert.rejects(refresh, (error: unknown) => error instanceof ResponseError && error.errorCode === 5); |
| else |
| await refresh; |
| const reads = countCommand(cluster.coordinator, COMMAND_CODE.GetClusterMetadata); |
| await cluster.client.sendCommand(POLL_MESSAGES.code, groupPollPayload()); |
| assert.equal(countCommand(cluster.coordinator, COMMAND_CODE.GetClusterMetadata), reads); |
| assert.equal(countCommand(cluster.coordinator, COMMAND_CODE.PollMessages), standalone ? 2 : 0); |
| assert.equal(countCommand(cluster.coordinator, COMMAND_CODE.GetPartitionRoute), 1); |
| assert.equal(countCommand(cluster.primary, COMMAND_CODE.PollMessages), standalone ? 0 : 2); |
| assert.equal(resets, 0); |
| } finally { |
| await cluster.close(); |
| } |
| }); |
| } |
| } |
| |
| it('refreshes a stalled auxiliary TLS dial within the shared poll budget', async () => { |
| const held: Socket[] = []; |
| const silent = createServer((socket) => { |
| held.push(socket); |
| socket.resume(); |
| }); |
| silent.listen(0, '127.0.0.1'); |
| await once(silent, 'listening'); |
| const silentPort = (silent.address() as AddressInfo).port; |
| let routes = 0; |
| const cluster = await startPollCluster(undefined, (frame, socket) => { |
| const code = frame.readUInt32LE(REQUEST_OFFSET.reserved); |
| if (code === COMMAND_CODE.GetClusterMetadata) { |
| socket.write(replyFrame(Operation.NonReplicated, |
| threeNodeMetadataBody(cluster.coordinator.port, cluster.primary.port, silentPort))); |
| return true; |
| } |
| if (code !== COMMAND_CODE.GetPartitionRoute || ++routes > 1) |
| return false; |
| socket.write(replyFrame(Operation.NonReplicated, routingBody(1n, 0n, 9))); |
| return true; |
| }, 'TLS'); |
| let resets = 0; |
| cluster.client.on('sessionReset', () => { resets += 1; }); |
| try { |
| const response = await cluster.client.sendCommand(POLL_MESSAGES.code, groupPollPayload(), |
| { deadline: Date.now() + 5_000 }); |
| assert.equal(response.data.toString(), 'primary'); |
| assert.equal(routes, 2); |
| assert.equal(held.length, 1); |
| assert.equal(held[0].destroyed, true); |
| assert.equal(countCommand(cluster.primary, COMMAND_CODE.PollMessages), 1); |
| assert.equal(resets, 0); |
| } finally { |
| await cluster.close(); |
| held.forEach((socket) => socket.destroy()); |
| await new Promise<void>((resolve) => silent.close(() => resolve())); |
| } |
| }); |
| |
| for (const operation of [Operation.TruncatePartition, Operation.DeleteSegments]) { |
| it(`raises the attachment floor for a DeleteSegments acknowledgement with operation ${operation}`, async () => { |
| const cluster = await startPollCluster(undefined, (frame, socket) => { |
| if (frame[REQUEST_OFFSET.operation] !== Operation.DeleteSegments) |
| return false; |
| const response = replyFrame(operation, |
| operation === Operation.TruncatePartition ? Buffer.alloc(4) : Buffer.alloc(0)); |
| response.writeBigUInt64LE(50n, REPLY_OFFSET.commit); |
| socket.write(response); |
| return true; |
| }); |
| try { |
| await cluster.client.sendCommand(POLL_MESSAGES.code, groupPollPayload()); |
| await cluster.client.sendCommand(COMMAND_CODE.DeleteSegments, Buffer.alloc(0)); |
| await cluster.client.sendCommand(POLL_MESSAGES.code, groupPollPayload()); |
| const attachments = cluster.primary.frames.filter((frame) => |
| frame.readUInt32LE(REQUEST_OFFSET.reserved) === COMMAND_CODE.BindSession); |
| assert.deepEqual(attachments.map((frame) => bindingIdentity(frame).readBigUInt64LE(24)), [1n, 50n]); |
| assert.equal(cluster.primary.frames.filter((frame) => frame[REQUEST_OFFSET.operation] === Operation.Register).length, 0); |
| } finally { |
| await cluster.close(); |
| } |
| }); |
| } |
| |
| it('rechecks metadata after waiting behind another primary poll', async () => { |
| let firstPoll!: (socket: Socket) => void; |
| const admitted = new Promise<Socket>((resolve) => { firstPoll = resolve; }); |
| let polls = 0; |
| const cluster = await startPollCluster((frame, socket) => { |
| if (frame.readUInt32LE(REQUEST_OFFSET.reserved) !== COMMAND_CODE.PollMessages || ++polls !== 1) |
| return false; |
| firstPoll(socket); |
| return true; |
| }, (frame, socket) => { |
| if (frame[REQUEST_OFFSET.operation] !== Operation.UpdateTopic) |
| return false; |
| const response = replyFrame(Operation.UpdateTopic, Buffer.alloc(4)); |
| response.writeBigUInt64LE(21n, REPLY_OFFSET.commit); |
| socket.write(response); |
| return true; |
| }); |
| try { |
| const first = cluster.client.sendCommand(POLL_MESSAGES.code, groupPollPayload()); |
| const socket = await admitted; |
| const second = cluster.client.sendCommand(POLL_MESSAGES.code, groupPollPayload()); |
| await cluster.client.sendCommand(COMMAND_CODE.UpdateTopic, Buffer.alloc(0)); |
| assert.equal(polls, 1, 'concurrent polls must share a serialized primary connection'); |
| socket.write(replyFrame(Operation.NonReplicated, Buffer.from('primary'))); |
| await Promise.all([first, second]); |
| assert.equal(polls, 2); |
| const attachments = cluster.primary.frames.filter((frame) => |
| frame.readUInt32LE(REQUEST_OFFSET.reserved) === COMMAND_CODE.BindSession); |
| assert.deepEqual(attachments.map((frame) => bindingIdentity(frame).readBigUInt64LE(24)), [1n, 21n]); |
| } finally { |
| await cluster.close(); |
| } |
| }); |
| |
| for (const failure of ['disconnect', 'not committed', 'timeout', 'partial reply', 'stale session', 'unauthenticated'] as const) { |
| it(`does not replay an uncertain primary poll after ${failure}`, async () => { |
| let polls = 0; |
| const cluster = await startPollCluster((frame, socket) => { |
| if (frame.readUInt32LE(REQUEST_OFFSET.reserved) !== COMMAND_CODE.PollMessages) |
| return false; |
| polls += 1; |
| if (polls > 1) |
| return false; |
| if (failure === 'disconnect') socket.destroy(); |
| if (failure === 'not committed') |
| socket.write(replyFrame(Operation.NonReplicated, Buffer.alloc(0), 57)); |
| if (failure === 'stale session') |
| socket.write(replyFrame(Operation.NonReplicated, Buffer.alloc(0), 30)); |
| if (failure === 'unauthenticated') |
| socket.write(replyFrame(Operation.NonReplicated, Buffer.alloc(0), 40)); |
| if (failure === 'partial reply') |
| socket.write(replyFrame(Operation.NonReplicated, Buffer.from('partial')).subarray(0, HEADER_SIZE + 1)); |
| return true; |
| }); |
| let resets = 0; |
| cluster.client.on('sessionReset', () => { resets += 1; }); |
| try { |
| await assert.rejects(cluster.client.sendCommand(POLL_MESSAGES.code, groupPollPayload(), |
| { deadline: Date.now() + 300 }), (error: unknown) => |
| error instanceof ResponseError && error.errorCode === 57); |
| assert.equal(polls, 1); |
| assert.equal(resets, 0); |
| await cluster.client.sendCommand(POLL_MESSAGES.code, groupPollPayload()); |
| assert.equal(polls, 2); |
| assert.equal(countCommand(cluster.primary, COMMAND_CODE.BindSession), failure === 'not committed' ? 1 : 2); |
| } finally { |
| await cluster.close(); |
| } |
| }); |
| } |
| |
| for (const status of [30, 40]) { |
| it(`rebuilds an auxiliary rejected with session status ${status} before poll admission`, async () => { |
| let attachments = 0; |
| const cluster = await startPollCluster((frame, socket) => { |
| if (frame.readUInt32LE(REQUEST_OFFSET.reserved) !== COMMAND_CODE.BindSession || ++attachments > 1) |
| return false; |
| socket.write(replyFrame(Operation.NonReplicated, Buffer.alloc(0), status)); |
| return true; |
| }); |
| let resets = 0; |
| cluster.client.on('sessionReset', () => { resets += 1; }); |
| try { |
| await cluster.client.sendCommand(POLL_MESSAGES.code, groupPollPayload()); |
| assert.equal(attachments, 2); |
| assert.equal(countCommand(cluster.primary, COMMAND_CODE.PollMessages), 1); |
| assert.equal(cluster.primary.frames.filter((frame) => frame[REQUEST_OFFSET.operation] === Operation.Register).length, 0); |
| assert.equal(resets, 0); |
| } finally { |
| await cluster.close(); |
| } |
| }); |
| |
| it(`returns control session status ${status} without rerouting the lookup`, async () => { |
| const registered = new Set<Socket>(); |
| let routes = 0; |
| const cluster = await startPollCluster(undefined, (frame, socket) => { |
| if (frame[REQUEST_OFFSET.operation] === Operation.Register || |
| frame.readUInt32LE(REQUEST_OFFSET.reserved) === COMMAND_CODE.BindSession) { |
| assert.equal(registered.has(socket), false, |
| 'session recovery must replace the rejected control transport'); |
| registered.add(socket); |
| } |
| if (frame.readUInt32LE(REQUEST_OFFSET.reserved) !== COMMAND_CODE.GetPartitionRoute || ++routes > 1) |
| return false; |
| socket.write(replyFrame(Operation.NonReplicated, Buffer.alloc(0), status)); |
| return true; |
| }); |
| try { |
| await assert.rejects(cluster.client.sendCommand(POLL_MESSAGES.code, groupPollPayload()), |
| (error: unknown) => error instanceof ResponseError && error.errorCode === status); |
| assert.equal(routes, 1); |
| assert.equal(countCommand(cluster.primary, COMMAND_CODE.PollMessages), 0); |
| await cluster.client.sendCommand(POLL_MESSAGES.code, groupPollPayload()); |
| assert.equal(registered.size, 2); |
| assert.equal(countCommand(cluster.primary, COMMAND_CODE.PollMessages), 1); |
| } finally { |
| await cluster.close(); |
| } |
| }); |
| } |
| |
| it('binds primary connections after credential changes without another login', async () => { |
| const cluster = await startPollCluster(undefined, (frame, socket) => { |
| const operation = frame[REQUEST_OFFSET.operation]; |
| if (operation !== Operation.UpdateUser && operation !== Operation.ChangePassword) |
| return false; |
| socket.write(replyFrame(operation, Buffer.alloc(4))); |
| return true; |
| }); |
| try { |
| await cluster.client.sendCommand(LOGIN.code, LOGIN.serialize({ username: 'manual', password: 'first' })); |
| await cluster.client.sendCommand(UPDATE_USER.code, UPDATE_USER.serialize({ userId: 7, username: 'renamed' })); |
| await cluster.client.sendCommand(CHANGE_PASSWORD.code, CHANGE_PASSWORD.serialize({ |
| userId: 7, currentPassword: 'first', newPassword: 'second' |
| })); |
| await cluster.client.sendCommand(POLL_MESSAGES.code, groupPollPayload()); |
| const login = cluster.coordinator.frames.find((frame) => frame[REQUEST_OFFSET.operation] === Operation.Register)!; |
| const credentials = registerCredentialsBody(login); |
| const username = readWireName(credentials, 0); |
| const password = readWireName(credentials, username.next); |
| assert.equal(username.value, 'manual'); |
| assert.equal(password.value, 'first'); |
| const binding = cluster.primary.frames.find((frame) => |
| frame.readUInt32LE(REQUEST_OFFSET.reserved) === COMMAND_CODE.BindSession)!; |
| assertSharedBinding(login, binding); |
| assert.equal(cluster.primary.frames.filter((frame) => |
| frame[REQUEST_OFFSET.operation] === Operation.Register).length, 0); |
| await cluster.client.sendCommand(LOGOUT.code, LOGOUT.serialize()); |
| await cluster.client.sendCommand(POLL_MESSAGES.code, groupPollPayload()); |
| const replacement = cluster.coordinator.frames.filter((frame) => |
| frame[REQUEST_OFFSET.operation] === Operation.Register).at(-1)!; |
| assert.equal(readWireName(registerCredentialsBody(replacement), 0).value, 'iggy'); |
| assertSharedBinding(replacement, cluster.primary.frames.filter((frame) => |
| frame.readUInt32LE(REQUEST_OFFSET.reserved) === COMMAND_CODE.BindSession).at(-1)!); |
| } finally { |
| await cluster.close(); |
| } |
| }); |
| |
| it('binds auxiliaries with the proof from PAT registration', async () => { |
| const cluster = await startPollCluster(); |
| try { |
| await cluster.client.sendCommand(LOGIN_WITH_TOKEN.code, LOGIN_WITH_TOKEN.serialize({ token: 'manual-token' })); |
| await cluster.client.sendCommand(POLL_MESSAGES.code, groupPollPayload()); |
| const login = cluster.coordinator.frames.find((frame) => frame[REQUEST_OFFSET.operation] === Operation.Register)!; |
| const sdkName = readWireName(login, HEADER_SIZE + 4); |
| const sdkVersion = readWireName(login, sdkName.next); |
| assert.equal(readWireName(login, sdkVersion.next + BIND_SECRET_BYTES).value, 'manual-token'); |
| assertSharedBinding(login, cluster.primary.frames.find((frame) => |
| frame.readUInt32LE(REQUEST_OFFSET.reserved) === COMMAND_CODE.BindSession)!); |
| } finally { |
| await cluster.close(); |
| } |
| }); |
| |
| for (const manual of [ |
| { command: LOGIN.code, payload: LOGIN.serialize({ username: 'manual', password: 'first' }), identity: 'manual' }, |
| { command: LOGIN_WITH_TOKEN.code, payload: LOGIN_WITH_TOKEN.serialize({ token: 'manual-token' }), identity: 'manual-token' } |
| ]) { |
| it(`retains ${manual.identity} across recovery and restores configured credentials after logout`, async () => { |
| let coordinatorSocket!: Socket; |
| const cluster = await startPollCluster(undefined, (_frame, socket) => { |
| coordinatorSocket = socket; |
| return false; |
| }); |
| try { |
| await cluster.client.sendCommand(manual.command, manual.payload); |
| await cluster.client.sendCommand(POLL_MESSAGES.code, groupPollPayload()); |
| const reset = once(cluster.client, 'sessionReset'); |
| coordinatorSocket.destroy(); |
| await reset; |
| await cluster.client.sendCommand(POLL_MESSAGES.code, groupPollPayload()); |
| for (const server of [cluster.coordinator]) { |
| const login = server.frames.filter((frame) => frame[REQUEST_OFFSET.operation] === Operation.Register).at(-1)!; |
| assert.equal(readWireName(registerCredentialsBody(login), 0).value, manual.identity); |
| assertSharedBinding(login, cluster.primary.frames.filter((frame) => |
| frame.readUInt32LE(REQUEST_OFFSET.reserved) === COMMAND_CODE.BindSession).at(-1)!); |
| } |
| await cluster.client.sendCommand(LOGOUT.code, LOGOUT.serialize()); |
| await cluster.client.sendCommand(POLL_MESSAGES.code, groupPollPayload()); |
| for (const server of [cluster.coordinator]) { |
| const login = server.frames.filter((frame) => frame[REQUEST_OFFSET.operation] === Operation.Register).at(-1)!; |
| const credentials = registerCredentialsBody(login); |
| const username = readWireName(credentials, 0); |
| assert.equal(username.value, 'iggy'); |
| assert.equal(readWireName(credentials, username.next).value, 'iggy'); |
| assertSharedBinding(login, cluster.primary.frames.filter((frame) => |
| frame.readUInt32LE(REQUEST_OFFSET.reserved) === COMMAND_CODE.BindSession).at(-1)!); |
| } |
| } finally { |
| await cluster.close(); |
| } |
| }); |
| } |
| |
| it('updates the configured identity after its password and username change', async () => { |
| const cluster = await startPollCluster(undefined, (frame, socket) => { |
| const operation = frame[REQUEST_OFFSET.operation]; |
| if (operation !== Operation.UpdateUser && operation !== Operation.ChangePassword) |
| return false; |
| socket.write(replyFrame(operation, Buffer.alloc(4))); |
| return true; |
| }); |
| try { |
| await cluster.client.sendCommand(POLL_MESSAGES.code, groupPollPayload()); |
| await cluster.client.sendCommand(UPDATE_USER.code, UPDATE_USER.serialize({ userId: 7, username: 'renamed' })); |
| await cluster.client.sendCommand(CHANGE_PASSWORD.code, CHANGE_PASSWORD.serialize({ |
| userId: 7, currentPassword: 'iggy', newPassword: 'second' |
| })); |
| await cluster.client.sendCommand(LOGOUT.code, LOGOUT.serialize()); |
| await cluster.client.sendCommand(POLL_MESSAGES.code, groupPollPayload()); |
| for (const server of [cluster.coordinator]) { |
| const login = server.frames.filter((frame) => frame[REQUEST_OFFSET.operation] === Operation.Register).at(-1)!; |
| const credentials = registerCredentialsBody(login); |
| const username = readWireName(credentials, 0); |
| assert.equal(username.value, 'renamed'); |
| assert.equal(readWireName(credentials, username.next).value, 'second'); |
| assertSharedBinding(login, cluster.primary.frames.filter((frame) => |
| frame.readUInt32LE(REQUEST_OFFSET.reserved) === COMMAND_CODE.BindSession).at(-1)!); |
| } |
| } finally { |
| await cluster.close(); |
| } |
| }); |
| |
| it('closes a waiting primary exchange on coordinator logout without replay', async () => { |
| let admitted!: () => void; |
| const pendingAdmission = new Promise<void>((resolve) => { admitted = resolve; }); |
| const cluster = await startPollCluster((frame) => { |
| if (frame.readUInt32LE(REQUEST_OFFSET.reserved) !== COMMAND_CODE.PollMessages) |
| return false; |
| admitted(); |
| return true; |
| }); |
| try { |
| const polling = cluster.client.sendCommand(POLL_MESSAGES.code, groupPollPayload()); |
| const failed = assert.rejects(polling, (error: unknown) => |
| error instanceof ResponseError && error.errorCode === 57); |
| await pendingAdmission; |
| await cluster.client.sendCommand(LOGOUT.code, LOGOUT.serialize()); |
| await failed; |
| assert.equal(countCommand(cluster.primary, COMMAND_CODE.PollMessages), 1); |
| assert.equal(cluster.client.isAuthenticated, false); |
| } finally { |
| await cluster.close(); |
| } |
| }); |
| }); |
| |
| describe('VSR client socket', () => { |
| for (const refusal of [0, 30, 40, 57, 58]) { |
| it(`resumes or replaces a lost connection only after bind verdict ${refusal}`, async () => { |
| let registrations = 0; |
| let sends = 0; |
| let bindStatus = refusal; |
| const server = await startVsrServer((frame, socket) => { |
| const operation = frame.readUInt8(REQUEST_OFFSET.operation); |
| const code = frame.readUInt32LE(REQUEST_OFFSET.reserved); |
| if (operation === Operation.Register) { |
| registrations += 1; |
| socket.write(replyFrame(operation, registerReplyBody(TEST_SESSION + BigInt(registrations - 1)))); |
| } else if (code === COMMAND_CODE.BindSession) { |
| socket.write(replyFrame(operation, bindStatus === 0 ? registerReplyBody().subarray(4) : undefined, bindStatus)); |
| } else if (code === COMMAND_CODE.GetClusterMetadata) { |
| socket.write(replyFrame(operation, singleNodeMetadataBody(server.port))); |
| } else if (code === COMMAND_CODE.GetPartitionRoute) { |
| socket.write(replyFrame(operation, routingBody(0n, 0n, 0))); |
| } else if (operation === Operation.SendMessages) { |
| sends += 1; |
| if (sends === 1) |
| socket.destroy(); |
| else |
| socket.write(replyFrame(operation, Buffer.alloc(4))); |
| } |
| }); |
| const config = vsrConfig(server.port); |
| const client = new CommandResponseStream(config); |
| try { |
| await client.authenticate(config.credentials); |
| await assert.rejects(client.sendCommand(COMMAND_CODE.SendMessages, explicitSend)); |
| if (refusal === 57 || refusal === 58) { |
| await assert.rejects(client.authenticate(config.credentials), (error: unknown) => |
| error instanceof ResponseError && error.errorCode === refusal); |
| assert.equal(registrations, 1, 'lagging metadata must not replace the logical session'); |
| bindStatus = 0; |
| } |
| await client.authenticate(config.credentials); |
| assert.equal(sends, 1, 'authentication must not replay the uncertain mutation'); |
| const terminal = refusal === 30 || refusal === 40; |
| assert.equal(registrations, terminal ? 2 : 1); |
| const binding = server.frames.find((frame) => |
| frame.readUInt32LE(REQUEST_OFFSET.reserved) === COMMAND_CODE.BindSession)!; |
| assertSharedBinding(server.frames[0], binding); |
| await client.sendCommand(COMMAND_CODE.SendMessages, explicitSend); |
| const writes = server.frames.filter((frame) => frame.readUInt8(REQUEST_OFFSET.operation) === Operation.SendMessages); |
| assert.equal(writes.length, 2); |
| assert.equal(writes[1].readBigUInt64LE(REQUEST_OFFSET.session), terminal ? TEST_SESSION + 1n : TEST_SESSION); |
| const nextRequest = writes[1].readBigUInt64LE(REQUEST_OFFSET.request); |
| assert.ok(nextRequest > 0n); |
| if (!terminal) |
| assert.ok(nextRequest > writes[0].readBigUInt64LE(REQUEST_OFFSET.request)); |
| assert.equal(writes[0].subarray(REQUEST_OFFSET.client, REQUEST_OFFSET.client + 16) |
| .equals(writes[1].subarray(REQUEST_OFFSET.client, REQUEST_OFFSET.client + 16)), !terminal); |
| } finally { |
| client.destroy(); |
| await server.close(); |
| } |
| }); |
| } |
| |
| 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 || |
| frame.readUInt32LE(REQUEST_OFFSET.reserved) === COMMAND_CODE.BindSession) { |
| singleNodeHandler(intermediate.port)(frame, socket); |
| 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.NonReplicated, Operation.NonReplicated] |
| ); |
| assertSharedBinding(follower.frames[0], intermediate.frames[0]); |
| 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 || |
| frame.readUInt32LE(REQUEST_OFFSET.reserved) === COMMAND_CODE.BindSession) { |
| singleNodeHandler(survivor.port)(frame, socket); |
| 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)}`); |
| |
| assert.equal(countCommand(survivor, COMMAND_CODE.BindSession), 1); |
| assertSharedBinding(primary.frames[0], survivor.frames[0]); |
| 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.NonReplicated, |
| Operation.NonReplicated |
| ]); |
| assertSharedBinding(follower.frames[0], leader.frames[0]); |
| 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 || |
| frame.readUInt32LE(REQUEST_OFFSET.reserved) === COMMAND_CODE.BindSession) { |
| singleNodeHandler(second.port)(frame, socket); |
| 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(); |
| } |
| } |
| ); |
| |
| for (const refusal of [58, 89, 41, 40, 30, 87, 4000]) { |
| it(`keeps an uncertain write uncertain after a later refusal ${refusal}`, async () => { |
| const EXPIRED_REQUEST_TIME = 30_001; |
| let now = 0; |
| let attempts = 0; |
| const server = await startVsrServer((frame, socket) => { |
| if (frame.readUInt8(REQUEST_OFFSET.operation) === Operation.SendMessages) { |
| attempts += 1; |
| now = attempts === 1 ? 1 : EXPIRED_REQUEST_TIME; |
| socket.write(replyFrame( |
| Operation.SendMessages, Buffer.alloc(0), attempts === 1 ? 57 : refusal |
| )); |
| return; |
| } |
| if (frame.readUInt32LE(REQUEST_OFFSET.reserved) === COMMAND_CODE.GetPartitionRoute) { |
| socket.write(replyFrame(Operation.NonReplicated, routingBody(0n, 0n, 0))); |
| 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; }); |
| Date.now = () => now; |
| await assert.rejects( |
| () => client.sendCommand(COMMAND_CODE.SendMessages, explicitSend), |
| (error: unknown) => |
| error instanceof ResponseError && |
| error.commandCode === COMMAND_CODE.SendMessages && |
| error.errorCode === 57 |
| ); |
| const writes = server.frames.filter( |
| (frame) => frame.readUInt8(REQUEST_OFFSET.operation) === Operation.SendMessages |
| ); |
| assert.equal(writes.length, 2); |
| assert.deepEqual(writes[1], writes[0]); |
| assert.equal(client.isAuthenticated, true); |
| assert.equal(sessionResets, 0); |
| } finally { |
| Date.now = realNow; |
| client.destroy(); |
| await server.close(); |
| } |
| }); |
| } |
| |
| it('reuses a send context and refreshes it once after a history refusal', async () => { |
| const state = { live: 1n, refuseAll: false, contextStatus: 0 }; |
| const server = await contextServer(state); |
| const client = new CommandResponseStream(vsrConfig(server.port)); |
| const incarnations = () => server.frames |
| .filter((frame) => frame.readUInt8(REQUEST_OFFSET.operation) === Operation.SendMessages) |
| .map((frame) => frame.readBigUInt64LE(REQUEST_OFFSET.partitionIncarnation)); |
| try { |
| await client.sendCommand(COMMAND_CODE.SendMessages, explicitSend); |
| await client.sendCommand(COMMAND_CODE.SendMessages, explicitSend); |
| assert.equal(countCommand(server, COMMAND_CODE.GetPartitionRoute), 1); |
| state.live = 2n; |
| await client.sendCommand(COMMAND_CODE.SendMessages, explicitSend); |
| assert.equal(countCommand(server, COMMAND_CODE.GetPartitionRoute), 2); |
| assert.deepEqual(incarnations(), [1n, 1n, 1n, 2n]); |
| state.refuseAll = true; |
| await assert.rejects(() => client.sendCommand(COMMAND_CODE.SendMessages, explicitSend), |
| (error: unknown) => error instanceof ResponseError && |
| error.commandCode === COMMAND_CODE.SendMessages && error.errorCode === HISTORY_UNAVAILABLE); |
| assert.equal(countCommand(server, COMMAND_CODE.GetPartitionRoute), 3); |
| assert.equal(incarnations().length, 6); |
| } finally { |
| client.destroy(); |
| await server.close(); |
| } |
| }); |
| |
| it('reports a refused context query as an error of the send', async () => { |
| const server = await contextServer({ live: 1n, refuseAll: false, contextStatus: 2010 }); |
| const client = new CommandResponseStream(vsrConfig(server.port)); |
| try { |
| await assert.rejects(() => client.sendCommand(COMMAND_CODE.SendMessages, explicitSend), |
| (error: unknown) => error instanceof ResponseError && |
| error.commandCode === COMMAND_CODE.SendMessages && error.errorCode === 2010); |
| } finally { |
| client.destroy(); |
| await server.close(); |
| } |
| }); |
| |
| it('keeps an eviction during the send context query typed', async () => { |
| const server = await startVsrServer((frame, socket) => { |
| if (frame.readUInt32LE(REQUEST_OFFSET.reserved) === COMMAND_CODE.GetPartitionRoute) { |
| socket.write(evictionFrame(EvictionReason.NoSession)); |
| return; |
| } |
| singleNodeHandler(server.port)(frame, socket); |
| }); |
| const client = new CommandResponseStream(vsrConfig(server.port)); |
| try { |
| await assert.rejects(() => client.sendCommand(COMMAND_CODE.SendMessages, explicitSend), |
| (error: unknown) => error instanceof VsrEvictionError && error.errorCode === 40); |
| } finally { |
| client.destroy(); |
| await server.close(); |
| } |
| }); |
| |
| it('surfaces a history refusal the remaining budget cannot refresh', async () => { |
| const realNow = Date.now; |
| const deadline = realNow() + 30_000; |
| let offset = 0; |
| const server = await contextServer({ |
| live: 1n, refuseAll: true, contextStatus: 0, |
| // 10ms of budget left when the refusal comes back: too little to carry a |
| // context query and another send. |
| onSend: () => { offset = deadline - realNow() - 10; } |
| }); |
| const client = new CommandResponseStream(vsrConfig(server.port)); |
| try { |
| await client.authenticate(vsrConfig(server.port).credentials); |
| Date.now = () => realNow() + offset; |
| await assert.rejects( |
| () => client.sendCommand(COMMAND_CODE.SendMessages, explicitSend, { deadline }), |
| (error: unknown) => error instanceof ResponseError && |
| error.commandCode === COMMAND_CODE.SendMessages && error.errorCode === HISTORY_UNAVAILABLE); |
| assert.equal(countCommand(server, COMMAND_CODE.GetPartitionRoute), 1); |
| } finally { |
| Date.now = realNow; |
| client.destroy(); |
| await server.close(); |
| } |
| }); |
| |
| it('keeps the captured context of an offset store re-issued after a leader move', async () => { |
| // The store captures incarnation 1 and then waits behind a command the old |
| // node never answers. The move re-issues it on a node whose route already |
| // reports incarnation 2: an offset taken from 1 must not commit under 2. |
| const leader = await startVsrServer((frame, socket) => { |
| if (frame.readUInt32LE(REQUEST_OFFSET.reserved) === COMMAND_CODE.GetPartitionRoute) { |
| socket.write(replyFrame(Operation.NonReplicated, routingBody(1n, 2n))); |
| return; |
| } |
| offsetCommittingHandler(leader.port)(frame, socket); |
| }); |
| const demoted = await startVsrServer((frame, socket) => { |
| const code = frame.readUInt32LE(REQUEST_OFFSET.reserved); |
| if (code === COMMAND_CODE.GetPartitionRoute) { |
| socket.write(replyFrame(Operation.NonReplicated, routingBody(1n, 1n))); |
| return; |
| } |
| if (code === 60_038) { |
| void connectionOf(client).redirect('127.0.0.1', leader.port); |
| return; |
| } |
| singleNodeHandler(demoted.port)(frame, socket); |
| }); |
| const client = new CommandResponseStream(vsrConfig(demoted.port)); |
| try { |
| await client.authenticate(vsrConfig(demoted.port).credentials); |
| const store = client.sendCommand(COMMAND_CODE.StoreOffset, storeOffsetPayload); |
| client.sendCommand(60_038, Buffer.alloc(0)).catch(() => undefined); |
| await store; |
| const stores = framesOf(leader, Operation.StoreConsumerOffset); |
| assert.equal(stores.length, 1); |
| assert.equal(stampedContext(stores[0]).incarnation, 1n); |
| assert.equal(countCommand(leader, COMMAND_CODE.GetPartitionRoute), 0); |
| assert.equal(framesOf(demoted, Operation.StoreConsumerOffset).length, 0); |
| } finally { |
| client.destroy(); |
| await leader.close(); |
| await demoted.close(); |
| } |
| }); |
| |
| it('refreshes a send context at most once when a leader move re-issues the send', async () => { |
| const leader = await contextServer({ live: 0n, refuseAll: true, contextStatus: 0 }); |
| let contextQueries = 0; |
| const demoted = await startVsrServer((frame, socket) => { |
| if (frame.readUInt8(REQUEST_OFFSET.operation) === Operation.SendMessages) { |
| socket.write(replyFrame(Operation.SendMessages, Buffer.alloc(0), HISTORY_UNAVAILABLE)); |
| return; |
| } |
| const code = frame.readUInt32LE(REQUEST_OFFSET.reserved); |
| if (code === COMMAND_CODE.GetPartitionRoute) { |
| contextQueries += 1; |
| // The refresh: park the refreshed send behind a command that this |
| // node never answers, so the move finds it in the queue. |
| if (contextQueries === 2) |
| client.sendCommand(60_038, Buffer.alloc(0)).catch(() => undefined); |
| const context = routingBody(0n, 0n, 0); |
| context.writeBigUInt64LE(BigInt(contextQueries), 0); |
| socket.write(replyFrame(Operation.NonReplicated, context)); |
| return; |
| } |
| if (code === 60_038) { |
| void connectionOf(client).redirect('127.0.0.1', leader.port); |
| return; |
| } |
| singleNodeHandler(demoted.port)(frame, socket); |
| }); |
| const client = new CommandResponseStream(vsrConfig(demoted.port)); |
| try { |
| await client.authenticate(vsrConfig(demoted.port).credentials); |
| await assert.rejects(() => client.sendCommand(COMMAND_CODE.SendMessages, explicitSend), |
| (error: unknown) => error instanceof ResponseError && |
| error.commandCode === COMMAND_CODE.SendMessages && error.errorCode === HISTORY_UNAVAILABLE); |
| assert.equal(contextQueries, 2); |
| assert.equal(countCommand(leader, COMMAND_CODE.GetPartitionRoute), 0); |
| assert.deepEqual(framesOf(leader, Operation.SendMessages) |
| .map((frame) => stampedContext(frame).incarnation), [2n]); |
| } finally { |
| client.destroy(); |
| await leader.close(); |
| await demoted.close(); |
| } |
| }); |
| |
| for (const partition of [Partitioning.Balanced, Partitioning.MessageKey('key')]) { |
| it(`refuses a raw send with partitioning kind ${partition.kind} before writing it`, async () => { |
| const server = await startVsrServer((frame, socket) => singleNodeHandler(server.port)(frame, socket)); |
| const client = new CommandResponseStream(vsrConfig(server.port)); |
| try { |
| await assert.rejects(() => client.sendCommand(COMMAND_CODE.SendMessages, sendTo(partition)), |
| (error: unknown) => error instanceof ResponseError && |
| error.commandCode === COMMAND_CODE.SendMessages && error.errorCode === 5 && |
| error.message.includes('explicit partition id') && error.message.includes('typed send API')); |
| assert.equal(framesOf(server, Operation.SendMessages).length, 0); |
| assert.equal(countCommand(server, COMMAND_CODE.GetPartitionRoute), 0); |
| } finally { |
| client.destroy(); |
| await server.close(); |
| } |
| }); |
| } |
| |
| for (const [name, code, payload, operation] of [ |
| ['an explicit-partition send', COMMAND_CODE.SendMessages, explicitSend, Operation.SendMessages], |
| ['an offset store', COMMAND_CODE.StoreOffset, storeOffsetPayload, Operation.StoreConsumerOffset], |
| ['an offset delete', COMMAND_CODE.DeleteConsumerOffset, deleteOffsetPayload, Operation.DeleteConsumerOffset], |
| ['a standalone poll', COMMAND_CODE.PollMessages, groupPollPayload(false), Operation.NonReplicated] |
| ] as const) { |
| it(`stamps the caller's context on ${name} while looking up its primary`, async () => { |
| const server = await startVsrServer((frame, socket) => offsetCommittingHandler(server.port)(frame, socket)); |
| const client = new CommandResponseStream(vsrConfig(server.port)); |
| try { |
| await client.sendCommand(code, payload, { context: CALLER_CONTEXT }); |
| const requests = framesOf(server, operation).filter((frame) => |
| operation !== Operation.NonReplicated || frame.readUInt32LE(REQUEST_OFFSET.reserved) === code); |
| assert.equal(requests.length, 1); |
| assert.deepEqual(stampedContext(requests[0]), CALLER_CONTEXT); |
| assert.equal(countCommand(server, COMMAND_CODE.GetPartitionRoute), 1); |
| } finally { |
| client.destroy(); |
| await server.close(); |
| } |
| }); |
| } |
| |
| it("stamps the caller's context on a primary poll", async () => { |
| const cluster = await startPollCluster(); |
| try { |
| await cluster.client.sendCommand(POLL_MESSAGES.code, groupPollPayload(), { context: CALLER_CONTEXT }); |
| const polls = cluster.primary.frames.filter((frame) => |
| frame.readUInt32LE(REQUEST_OFFSET.reserved) === COMMAND_CODE.PollMessages); |
| assert.equal(polls.length, 1); |
| assert.deepEqual(stampedContext(polls[0]), CALLER_CONTEXT); |
| } finally { |
| await cluster.close(); |
| } |
| }); |
| |
| it('keeps a caller context on a send refused as history unavailable', async () => { |
| const server = await contextServer({ live: 1n, refuseAll: true, contextStatus: 0 }); |
| const client = new CommandResponseStream(vsrConfig(server.port)); |
| try { |
| await assert.rejects( |
| () => client.sendCommand(COMMAND_CODE.SendMessages, explicitSend, { context: CALLER_CONTEXT }), |
| (error: unknown) => error instanceof ResponseError && error.errorCode === HISTORY_UNAVAILABLE); |
| assert.deepEqual(framesOf(server, Operation.SendMessages).map(stampedContext), [CALLER_CONTEXT]); |
| assert.equal(countCommand(server, COMMAND_CODE.GetPartitionRoute), 1); |
| } finally { |
| client.destroy(); |
| await server.close(); |
| } |
| }); |
| |
| it('retries primary lifecycle refusals with new ids and the captured context while the coordinator proceeds', async () => { |
| const order: string[] = []; |
| let refusals = 2; |
| let routes = 0; |
| let firstRefusal!: () => void; |
| const refused = new Promise<void>((resolve) => { firstRefusal = resolve; }); |
| const otherCommand = 60_041; |
| const cluster = await startPollCluster((frame, socket) => { |
| if (frame.readUInt8(REQUEST_OFFSET.operation) !== Operation.SendMessages) |
| return false; |
| order.push('send'); |
| socket.write(replyFrame(Operation.SendMessages, Buffer.alloc(0), refusals-- > 0 ? 88 : 0)); |
| firstRefusal(); |
| return true; |
| }, (frame, socket) => { |
| const code = frame.readUInt32LE(REQUEST_OFFSET.reserved); |
| if (code === COMMAND_CODE.GetPartitionRoute) { |
| routes += 1; |
| socket.write(replyFrame(Operation.NonReplicated, routingBody(BigInt(routes), BigInt(routes)))); |
| return true; |
| } |
| if (code === otherCommand) |
| order.push('other'); |
| return false; |
| }); |
| try { |
| await cluster.client.authenticate(vsrConfig(cluster.coordinator.port).credentials); |
| const started = Date.now(); |
| const send = cluster.client.sendCommand(COMMAND_CODE.SendMessages, explicitSend); |
| await refused; |
| await cluster.client.sendCommand(otherCommand, Buffer.alloc(0)); |
| await send; |
| const sends = framesOf(cluster.primary, Operation.SendMessages); |
| assert.equal(sends.length, 3); |
| assert.equal(new Set(sends.map((frame) => frame.readBigUInt64LE(REQUEST_OFFSET.request))).size, 3); |
| assert.deepEqual(sends.map(stampedContext), Array(3).fill({ |
| incarnation: 1n, ownerGeneration: 0n, metadataOp: 1n |
| }), 'a refreshed route must not replace the captured operation context'); |
| assert.deepEqual(order, ['send', 'other', 'send', 'send']); |
| assert.ok(Date.now() - started >= 145, 'the pauses grow from 50 ms to 100 ms'); |
| assert.equal(countCommand(cluster.primary, COMMAND_CODE.GetClusterMetadata), 0); |
| } finally { |
| await cluster.close(); |
| } |
| }); |
| |
| for (const code of [COMMAND_CODE.GetPartitionRoute, COMMAND_CODE.BindSession]) { |
| it(`does not retry a lifecycle refusal from routing command ${code}`, async () => { |
| const override = (frame: Buffer, socket: Socket): boolean => { |
| if (frame.readUInt32LE(REQUEST_OFFSET.reserved) !== code) |
| return false; |
| socket.write(replyFrame(frame.readUInt8(REQUEST_OFFSET.operation), Buffer.alloc(0), 88)); |
| return true; |
| }; |
| const cluster = await startPollCluster(override, override); |
| try { |
| await cluster.client.authenticate(vsrConfig(cluster.coordinator.port).credentials); |
| await assert.rejects( |
| () => cluster.client.sendCommand(COMMAND_CODE.SendMessages, explicitSend, |
| { deadline: Date.now() + 200 }), |
| (error: unknown) => error instanceof ResponseError && |
| error.commandCode === COMMAND_CODE.SendMessages && error.errorCode === 88); |
| assert.equal(countCommand(code === COMMAND_CODE.GetPartitionRoute |
| ? cluster.coordinator : cluster.primary, code), 1); |
| assert.equal(framesOf(cluster.primary, Operation.SendMessages).length, 0); |
| } finally { |
| await cluster.close(); |
| } |
| }); |
| } |
| |
| it('returns a primary lifecycle refusal within the original request deadline', async () => { |
| const cluster = await startPollCluster((frame, socket) => { |
| if (frame.readUInt8(REQUEST_OFFSET.operation) !== Operation.SendMessages) |
| return false; |
| socket.write(replyFrame(Operation.SendMessages, Buffer.alloc(0), 88)); |
| return true; |
| }); |
| try { |
| await cluster.client.authenticate(vsrConfig(cluster.coordinator.port).credentials); |
| const deadline = Date.now() + 400; |
| await assert.rejects( |
| () => cluster.client.sendCommand(COMMAND_CODE.SendMessages, explicitSend, { deadline }), |
| (error: unknown) => error instanceof ResponseError && |
| error.commandCode === COMMAND_CODE.SendMessages && error.errorCode === 88); |
| assert.ok(Date.now() <= deadline, 'the next exponential pause must fit the original deadline'); |
| assert.ok(framesOf(cluster.primary, Operation.SendMessages).length >= 2); |
| } finally { |
| await cluster.close(); |
| } |
| }); |
| |
| it('retries a lifecycle refusal as new requests while other commands proceed', async () => { |
| let refusals = 2; |
| const server = await startVsrServer((frame, socket) => { |
| if (frame.readUInt8(REQUEST_OFFSET.operation) === Operation.CreateStream) { |
| socket.write(replyFrame(Operation.CreateStream, metadataResult(refusals-- > 0 ? 88 : 0))); |
| return; |
| } |
| singleNodeHandler(server.port)(frame, socket); |
| }); |
| const client = new CommandResponseStream(vsrConfig(server.port)); |
| try { |
| await client.authenticate(vsrConfig(server.port).credentials); |
| const started = Date.now(); |
| const create = client.sendCommand(COMMAND_CODE.CreateStream, Buffer.alloc(0)); |
| const other = client.sendCommand(60_040, Buffer.alloc(0)); |
| await Promise.all([create, other]); |
| const elapsed = Date.now() - started; |
| const creates = framesOf(server, Operation.CreateStream); |
| assert.equal(creates.length, 3); |
| assert.equal(new Set(creates.map((frame) => |
| frame.readBigUInt64LE(REQUEST_OFFSET.request))).size, 3, 'each retry is a new request'); |
| assert.ok(elapsed >= 145, `the pauses grow from 50 ms, took ${elapsed} ms`); |
| const order = server.frames.map((frame) => frame.readUInt8(REQUEST_OFFSET.operation) === Operation.CreateStream |
| ? 'create' : frame.readUInt32LE(REQUEST_OFFSET.reserved)).filter((entry) => entry === 'create' || entry === 60_040); |
| assert.deepEqual(order, ['create', 60_040, 'create', 'create'], 'the pause must not hold the queue'); |
| } finally { |
| client.destroy(); |
| await server.close(); |
| } |
| }); |
| |
| it('returns the lifecycle refusal once the request deadline leaves no room for a retry', async () => { |
| const server = await startVsrServer((frame, socket) => { |
| if (frame.readUInt8(REQUEST_OFFSET.operation) === Operation.CreateStream) { |
| socket.write(replyFrame(Operation.CreateStream, metadataResult(88))); |
| return; |
| } |
| singleNodeHandler(server.port)(frame, socket); |
| }); |
| const client = new CommandResponseStream(vsrConfig(server.port)); |
| try { |
| await client.authenticate(vsrConfig(server.port).credentials); |
| const deadline = Date.now() + 400; |
| await assert.rejects( |
| () => client.sendCommand(COMMAND_CODE.CreateStream, Buffer.alloc(0), { deadline }), |
| (error: unknown) => error instanceof ResponseError && |
| error.commandCode === COMMAND_CODE.CreateStream && error.errorCode === 88); |
| assert.ok(Date.now() <= deadline, 'the last pause must fit the budget'); |
| assert.ok(framesOf(server, Operation.CreateStream).length >= 2); |
| } finally { |
| client.destroy(); |
| await server.close(); |
| } |
| }); |
| |
| it('drops the cached send contexts of a topic after a history refusal', async () => { |
| const state = { live: 1n, refuseAll: false, contextStatus: 0 }; |
| const server = await contextServer(state); |
| const client = new CommandResponseStream(vsrConfig(server.port)); |
| const otherPartition = sendTo(Partitioning.PartitionId(4)); |
| try { |
| await client.sendCommand(COMMAND_CODE.SendMessages, explicitSend); |
| await client.sendCommand(COMMAND_CODE.SendMessages, otherPartition); |
| assert.equal(countCommand(server, COMMAND_CODE.GetPartitionRoute), 2); |
| state.live = 2n; |
| await client.sendCommand(COMMAND_CODE.SendMessages, explicitSend); |
| assert.equal(countCommand(server, COMMAND_CODE.GetPartitionRoute), 3); |
| await client.sendCommand(COMMAND_CODE.SendMessages, otherPartition); |
| assert.equal(countCommand(server, COMMAND_CODE.GetPartitionRoute), 4, |
| 'a refusal for one partition drops its whole topic'); |
| assert.deepEqual(framesOf(server, Operation.SendMessages).map((frame) => |
| stampedContext(frame).incarnation), [1n, 1n, 1n, 2n, 2n]); |
| } finally { |
| client.destroy(); |
| await server.close(); |
| } |
| }); |
| |
| for (const code of [COMMAND_CODE.DeleteStream, COMMAND_CODE.DeleteTopic, |
| COMMAND_CODE.CreatePartitions, COMMAND_CODE.DeletePartitions]) { |
| it(`drops every cached send context after the client's own command ${code}`, async () => { |
| const server = await contextServer({ live: 1n, refuseAll: false, contextStatus: 0 }); |
| const client = new CommandResponseStream(vsrConfig(server.port)); |
| let resets = 0; |
| client.on('topicDiscoveryReset', () => { resets += 1; }); |
| try { |
| await client.sendCommand(COMMAND_CODE.SendMessages, explicitSend); |
| await client.sendCommand(code, Buffer.alloc(0), { handleResponse: false }); |
| await client.sendCommand(COMMAND_CODE.SendMessages, explicitSend); |
| assert.equal(countCommand(server, COMMAND_CODE.GetPartitionRoute), 2); |
| assert.equal(resets, 1, 'typed sends read the partition count again'); |
| } finally { |
| client.destroy(); |
| await server.close(); |
| } |
| }); |
| } |
| |
| for (const [name, code, operation, payload] of [ |
| ['store', COMMAND_CODE.StoreOffset, Operation.StoreConsumerOffset, storeOffsetPayload], |
| ['delete', COMMAND_CODE.DeleteConsumerOffset, Operation.DeleteConsumerOffset, deleteOffsetPayload] |
| ] as const) { |
| it(`writes an offset ${name} on the partition primary and keeps the main connection`, async () => { |
| const cluster = await startPollCluster( |
| (frame, socket) => { |
| if (frame.readUInt8(REQUEST_OFFSET.operation) !== operation) |
| return false; |
| socket.write(replyFrame(operation, metadataResult(0))); |
| return true; |
| }, |
| (frame, socket) => { |
| if (frame.readUInt8(REQUEST_OFFSET.operation) !== operation) |
| return false; |
| socket.write(replyFrame(operation, Buffer.alloc(0), TRANSIENT_NOT_ACCEPTED)); |
| return true; |
| }); |
| try { |
| await cluster.client.sendCommand(code, payload, { deadline: Date.now() + 2_000 }); |
| assert.equal(framesOf(cluster.coordinator, operation).length, 0); |
| assert.equal(framesOf(cluster.primary, operation).length, 1); |
| const rosterReads = countCommand(cluster.coordinator, COMMAND_CODE.GetClusterMetadata); |
| await cluster.client.sendCommand(COMMAND_CODE.GetClusterMetadata, Buffer.alloc(0)); |
| assert.equal(countCommand(cluster.coordinator, COMMAND_CODE.GetClusterMetadata), rosterReads + 1, |
| 'the main connection stays on its node'); |
| assert.equal(framesOf(cluster.coordinator, Operation.Register).length, 1, 'the session stays'); |
| assert.equal(framesOf(cluster.primary, Operation.Register).length, 0); |
| } finally { |
| await cluster.close(); |
| } |
| }); |
| } |
| |
| it('takes the request ids of primary offset writes from the session counter', async () => { |
| // The partition deduplicates by client id and request id, so an id the |
| // session already used would answer a write with the cached reply of |
| // another. Reading routes also consumes ids from the same counter. |
| let stores = 0; |
| const cluster = await startPollCluster( |
| (frame, socket) => { |
| if (frame.readUInt8(REQUEST_OFFSET.operation) !== Operation.StoreConsumerOffset) |
| return false; |
| stores += 1; |
| socket.write(replyFrame(Operation.StoreConsumerOffset, metadataResult(0), |
| stores === 1 ? HISTORY_UNAVAILABLE : 0)); |
| return true; |
| }, |
| (frame, socket) => { |
| if (frame.readUInt8(REQUEST_OFFSET.operation) !== Operation.CreateStream) |
| return false; |
| socket.write(replyFrame(Operation.CreateStream, metadataResult(0))); |
| return true; |
| }); |
| try { |
| await cluster.client.sendCommand(COMMAND_CODE.CreateStream, Buffer.alloc(0)); |
| await assert.rejects(cluster.client.sendCommand(COMMAND_CODE.StoreOffset, storeOffsetPayload), |
| (error: unknown) => error instanceof ResponseError && error.errorCode === HISTORY_UNAVAILABLE); |
| await cluster.client.sendCommand(COMMAND_CODE.StoreOffset, storeOffsetPayload); |
| assert.equal(countCommand(cluster.primary, COMMAND_CODE.BindSession), 1, |
| 'a history refusal retains the authenticated connection'); |
| const requests = [ |
| ...framesOf(cluster.coordinator, Operation.CreateStream), |
| ...framesOf(cluster.primary, Operation.StoreConsumerOffset) |
| ].map((frame) => frame.readBigUInt64LE(REQUEST_OFFSET.request)); |
| assert.equal(new Set(requests).size, requests.length); |
| assert.ok(requests[0] < requests[1] && requests[1] < requests[2]); |
| } finally { |
| await cluster.close(); |
| } |
| }); |
| |
| for (const callerContext of [undefined, CALLER_CONTEXT]) { |
| it(`keeps one ${callerContext ? 'caller' : 'route'} context across primary retries of an offset store`, async () => { |
| let routes = 0; |
| let writes = 0; |
| const cluster = await startPollCluster( |
| (frame, socket) => { |
| if (frame.readUInt8(REQUEST_OFFSET.operation) !== Operation.StoreConsumerOffset) |
| return false; |
| writes += 1; |
| socket.write(writes === 1 |
| ? replyFrame(Operation.StoreConsumerOffset, Buffer.alloc(0), TRANSIENT_NOT_ACCEPTED) |
| : replyFrame(Operation.StoreConsumerOffset, metadataResult(0))); |
| return true; |
| }, |
| (frame, socket) => { |
| if (frame.readUInt32LE(REQUEST_OFFSET.reserved) !== COMMAND_CODE.GetPartitionRoute) |
| return false; |
| routes += 1; |
| socket.write(replyFrame(Operation.NonReplicated, |
| routingBody(1n, BigInt(routes)))); |
| return true; |
| }); |
| let resets = 0; |
| cluster.client.on('sessionReset', () => { resets += 1; }); |
| try { |
| await cluster.client.sendCommand(COMMAND_CODE.StoreOffset, storeOffsetPayload, |
| { context: callerContext }); |
| assert.equal(routes, 2, 'a refused write routes again'); |
| assert.equal(resets, 0, 'a primary refusal leaves the main connection alone'); |
| assert.equal(framesOf(cluster.coordinator, Operation.Register).length, 1); |
| assert.deepEqual(framesOf(cluster.primary, Operation.StoreConsumerOffset).map(stampedContext), |
| Array(2).fill(callerContext ?? { incarnation: 1n, ownerGeneration: 0n, metadataOp: 1n })); |
| } finally { |
| await cluster.close(); |
| } |
| }); |
| } |
| |
| it('refreshes standalone routes after metadata commits', async () => { |
| const server = await startVsrServer((frame, socket) => { |
| if (frame.readUInt8(REQUEST_OFFSET.operation) === Operation.CreateStream) { |
| const reply = replyFrame(Operation.CreateStream, metadataResult(0)); |
| reply.writeBigUInt64LE(5n, REPLY_OFFSET.commit); |
| socket.write(reply); |
| return; |
| } |
| offsetCommittingHandler(server.port)(frame, socket); |
| }); |
| const client = new CommandResponseStream(vsrConfig(server.port)); |
| try { |
| for (let call = 0; call < 2; call += 1) { |
| await client.sendCommand(POLL_MESSAGES.code, groupPollPayload(false)); |
| await client.sendCommand(COMMAND_CODE.StoreOffset, storeOffsetPayload); |
| await client.sendCommand(COMMAND_CODE.DeleteConsumerOffset, deleteOffsetPayload); |
| } |
| assert.equal(countCommand(server, COMMAND_CODE.GetPartitionRoute), 2); |
| await client.sendCommand(COMMAND_CODE.CreateStream, Buffer.alloc(0)); |
| await client.sendCommand(POLL_MESSAGES.code, groupPollPayload(false)); |
| await client.sendCommand(COMMAND_CODE.StoreOffset, storeOffsetPayload); |
| assert.equal(countCommand(server, COMMAND_CODE.GetPartitionRoute), 4); |
| } finally { |
| client.destroy(); |
| await server.close(); |
| } |
| }); |
| |
| it('surfaces a refusal under a stale route once and routes the next call again', async () => { |
| let live = 1n; |
| const server = await startVsrServer((frame, socket) => { |
| const code = frame.readUInt32LE(REQUEST_OFFSET.reserved); |
| if (code === COMMAND_CODE.GetPartitionRoute) { |
| socket.write(replyFrame(Operation.NonReplicated, routingBody(1n, live))); |
| return; |
| } |
| if (code === COMMAND_CODE.PollMessages) { |
| socket.write(replyFrame(Operation.NonReplicated, Buffer.alloc(0), |
| stampedContext(frame).incarnation === live ? 0 : HISTORY_UNAVAILABLE)); |
| return; |
| } |
| singleNodeHandler(server.port)(frame, socket); |
| }); |
| const client = new CommandResponseStream(vsrConfig(server.port)); |
| try { |
| await client.sendCommand(POLL_MESSAGES.code, groupPollPayload(false)); |
| live = 2n; |
| await assert.rejects(() => client.sendCommand(POLL_MESSAGES.code, groupPollPayload(false)), |
| (error: unknown) => error instanceof ResponseError && error.errorCode === HISTORY_UNAVAILABLE); |
| assert.equal(countCommand(server, COMMAND_CODE.GetPartitionRoute), 1, 'no silent retry'); |
| await client.sendCommand(POLL_MESSAGES.code, groupPollPayload(false)); |
| assert.equal(countCommand(server, COMMAND_CODE.GetPartitionRoute), 2); |
| assert.deepEqual(server.frames |
| .filter((frame) => frame.readUInt32LE(REQUEST_OFFSET.reserved) === COMMAND_CODE.PollMessages) |
| .map((frame) => stampedContext(frame).incarnation), [1n, 1n, 2n]); |
| } finally { |
| client.destroy(); |
| await server.close(); |
| } |
| }); |
| |
| it('rediscovers an evicted route when the partition cache is full', async () => { |
| const server = await contextServer({ live: 1n, refuseAll: false, contextStatus: 0 }); |
| const client = new CommandResponseStream(vsrConfig(server.port)); |
| const partitions = Array.from({ length: 4097 }, (_, partitionId) => sendTo(Partitioning.PartitionId(partitionId))); |
| try { |
| await Promise.all(partitions.map((payload) => client.sendCommand(COMMAND_CODE.SendMessages, payload))); |
| await client.sendCommand(COMMAND_CODE.SendMessages, partitions[0]); |
| assert.equal(countCommand(server, COMMAND_CODE.GetPartitionRoute), partitions.length + 1); |
| } finally { |
| 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(); |
| } |
| }); |
| }); |