Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 164 additions & 2 deletions test/discovery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ import { AlternatorDynamoDBClient, routing } from "../src/index.js";
import { AlternatorDynamoDBClient as EdgeAlternatorDynamoDBClient } from "../src/edge.js";
import { RecordingHandler } from "./helpers.js";
import { ListTablesCommand } from "@aws-sdk/client-dynamodb";
import { createServer, type Server } from "node:http";
import type { AddressInfo } from "node:net";
import { NodeHttpHandler } from "@smithy/node-http-handler";
import { Agent, createServer, type Server } from "node:http";
import type { LookupAddress } from "node:dns";
import type { AddressInfo, LookupFunction } from "node:net";

const missingDatacenterQuery = { dc: "__alternator_client_missing_dc__" };
const missingRackQuery = { rack: "__alternator_client_missing_rack__" };
Expand Down Expand Up @@ -382,6 +384,143 @@ describe("Alternator discovery", () => {
}
});

it("discovers and routes through an IPv6 literal entrypoint", async () => {
const hostHeaders: string[] = [];
const server = createServer((request, response) => {
hostHeaders.push(request.headers.host ?? "");
response.setHeader("content-type", "application/x-amz-json-1.0");
response.end(JSON.stringify(request.url === "/localnodes" ? ["::1"] : { TableNames: [] }));
});
const address = await listen(server, "::1");
const client = new AlternatorDynamoDBClient({
seeds: [address.address],
port: address.port,
discovery: { background: false, timeoutMs: 500 },
maxAttempts: 1,
});

try {
await expect(client.alternator.refreshNodes()).resolves.toEqual([
{
host: "::1",
scheme: "http",
port: address.port,
url: `http://[::1]:${address.port}`,
},
]);
await expect(client.send(new ListTablesCommand({}))).resolves.toMatchObject({
TableNames: [],
});
expect(hostHeaders).toEqual([
`[::1]:${address.port}`,
`[::1]:${address.port}`,
]);
} finally {
client.destroy();
server.closeAllConnections?.();
await close(server);
}
});

it.each([
{
name: "an A-only",
listenHost: "127.0.0.1",
addresses: [
{ address: "127.0.0.1", family: 4 },
],
},
{
name: "an AAAA-only",
listenHost: "::1",
addresses: [
{ address: "::1", family: 6 },
],
},
{
name: "AAAA to A",
listenHost: "127.0.0.1",
addresses: [
{ address: "::1", family: 6 },
{ address: "127.0.0.1", family: 4 },
],
},
{
name: "A to AAAA",
listenHost: "::1",
addresses: [
{ address: "127.0.0.1", family: 4 },
{ address: "::1", family: 6 },
],
},
])("falls back from $name DNS records for discovery and routing", async ({ listenHost, addresses }) => {
let requests = 0;
const server = createServer((request, response) => {
requests += 1;
response.setHeader("content-type", "application/x-amz-json-1.0");
response.end(JSON.stringify(request.url === "/localnodes" ? ["dual.test"] : { TableNames: [] }));
});
const address = await listen(server, listenHost);
const requestHandler = dualStackHandler(addresses);
const client = new AlternatorDynamoDBClient({
seeds: ["dual.test"],
port: address.port,
requestHandler,
discovery: { background: false, timeoutMs: 500 },
maxAttempts: 1,
});

try {
await expect(client.alternator.refreshNodes()).resolves.toEqual([
{
host: "dual.test",
scheme: "http",
port: address.port,
url: `http://dual.test:${address.port}`,
},
]);
await expect(client.send(new ListTablesCommand({}))).resolves.toMatchObject({
TableNames: [],
});
expect(requests).toBe(2);
} finally {
client.destroy();
server.closeAllConnections?.();
await close(server);
}
});

it("returns promptly and keeps its seed when all DNS records are unavailable", async () => {
const server = createServer();
const address = await listen(server);
await close(server);
const client = new AlternatorDynamoDBClient({
seeds: ["dual.test"],
port: address.port,
requestHandler: dualStackHandler([
{ address: "::1", family: 6 },
{ address: "127.0.0.1", family: 4 },
]),
discovery: { background: false, timeoutMs: 100 },
maxAttempts: 1,
});

try {
const startedAt = Date.now();
await expect(client.alternator.refreshNodes()).resolves.toEqual([
Comment thread
dkropachev marked this conversation as resolved.
{
host: "dual.test",
scheme: "http",
port: address.port,
url: `http://dual.test:${address.port}`,
},
]);
expect(Date.now() - startedAt).toBeLessThan(500);
} finally {
client.destroy();
}
});

it("bounds draining non-terminating non-2xx discovery bodies", async () => {
let requests = 0;
const server = createServer((request, response) => {
Expand Down Expand Up @@ -497,3 +636,26 @@ function close(server: Server): Promise<void> {
});
});
}

function dualStackHandler(addresses: LookupAddress[]): NodeHttpHandler {
const lookup: LookupFunction = (_hostname, options, callback) => {
if (options.all) {
callback(null, addresses);
return;
}
const first = addresses[0];
if (!first) {
callback(new Error("no DNS records"), "", 0);
return;
}
callback(null, first.address, first.family);
};
return new NodeHttpHandler({
httpAgent: new Agent({
keepAlive: false,
lookup,
autoSelectFamily: true,
autoSelectFamilyAttemptTimeout: 10,
}),
});
}