@clickup/ent-framework
Version:
A PostgreSQL graph-database-alike library with microsharding and row-level security
128 lines (108 loc) • 3.86 kB
text/typescript
import delay from "delay";
import waitForExpect from "wait-for-expect";
import { MASTER, type Shard } from "../../abstract/Shard";
import { maybeCall, runInVoid } from "../../internal/misc";
import { escapeIdent } from "../helpers/escapeIdent";
import { PgSchema } from "../PgSchema";
import type { TestPgClient } from "./test-utils";
import {
TEST_CONFIG,
recreateTestTables,
shardRun,
testCluster,
TCPProxyServer,
TEST_ISLANDS,
} from "./test-utils";
jest.useFakeTimers({ advanceTimers: true });
const schema = new PgSchema(
'pg-schema.connect-timeout"table',
{
id: { type: String, autoInsert: "id_gen()" },
name: { type: String },
},
[],
);
const TABLE_BAK = `${schema.name}_bak`;
let shard: Shard<TestPgClient>;
let connStuckServer: TCPProxyServer;
let connStuckTestConfig: typeof TEST_CONFIG;
beforeEach(async () => {
testCluster.options.runOnShardErrorRetryCount = 2;
testCluster.options.runOnShardErrorRediscoverClusterDelayMs = 100;
testCluster.options.reloadIslandsIntervalMs = 20000; // intentionally large
testCluster.options.shardsDiscoverIntervalMs = 100000;
testCluster.options.islands = TEST_ISLANDS;
await testCluster.rediscover();
await recreateTestTables([
{
CREATE: [
`DROP TABLE IF EXISTS ${escapeIdent(TABLE_BAK)} CASCADE`,
`CREATE TABLE %T(
id bigint NOT NULL PRIMARY KEY,
name text NOT NULL
)`,
],
SCHEMA: schema,
SHARD_AFFINITY: [],
},
]);
shard = await testCluster.randomShard();
connStuckServer = new TCPProxyServer({
host: TEST_CONFIG.host!,
port: TEST_CONFIG.port!,
delayOnConnect: 1000000,
});
connStuckTestConfig = {
...TEST_CONFIG,
isAlwaysLaggingReplica: true,
...(await connStuckServer.hostPort()),
connectionTimeoutMillis: 30000,
};
});
afterEach(async () => {
await connStuckServer.destroy();
});
test("when connection gets stuck during background rediscovery, it does not slowdown queries", async () => {
const errorSpy = jest.mocked(
connStuckTestConfig.loggers.swallowedErrorLogger,
);
testCluster.options.islands = [
{ no: 0, nodes: [TEST_CONFIG, connStuckTestConfig] },
];
await jest.advanceTimersByTimeAsync(
maybeCall(testCluster.options.reloadIslandsIntervalMs),
);
await connStuckServer.waitForAtLeastConnections(1);
expect(await shardRun(shard, schema.insert({ name: "abc" }))).toBeTruthy();
expect(errorSpy).not.toBeCalled();
jest.advanceTimersByTime(connStuckTestConfig.connectionTimeoutMillis!);
await waitForExpect(() =>
expect("" + errorSpy.mock.lastCall?.[0].error).toContain("timeout"),
);
await testCluster.islands();
});
test("when rediscovery is triggered by a failed query, and connection gets stuck, then the query waits until rediscovery finishes", async () => {
const master = await shard.client(MASTER);
await master.rows("ALTER TABLE %T RENAME TO %T", schema.name, TABLE_BAK);
const errorSpy = jest.mocked(
connStuckTestConfig.loggers.swallowedErrorLogger,
);
testCluster.options.islands = [
{ no: 0, nodes: [TEST_CONFIG, connStuckTestConfig] },
];
// The query below fails and thus triggers rediscovery.
let shardRunResult: unknown = undefined;
runInVoid(
shardRun(shard, schema.insert({ name: "abc" }))
.then(() => (shardRunResult = true))
.catch((e) => (shardRunResult = e)),
);
await connStuckServer.waitForAtLeastConnections(1);
await master.rows("ALTER TABLE %T RENAME TO %T", TABLE_BAK, schema.name);
await delay(1000);
expect(shardRunResult).toBeUndefined();
jest.advanceTimersByTime(connStuckTestConfig.connectionTimeoutMillis!);
await waitForExpect(() => expect(shardRunResult).toEqual(true));
expect(errorSpy.mock.calls).toHaveLength(1);
expect("" + errorSpy.mock.calls[0][0].error).toContain("timeout");
});